From b0ba1d276453ccd2cab56a63f9af2d66070fcdb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Georg=20Mai=C3=9Fer?= Date: Sat, 7 Dec 2024 21:44:23 +0100 Subject: [PATCH] GH-751 Add another option in booking settings to allow cancel without limit. --- classes/booking_option.php | 1 + classes/observer.php | 6 +- lang/de/booking.php | 4 + lang/en/booking.php | 6 +- mod_form.php | 22 ++- .../condition_allowupdate_test.php | 178 +++++++++++++++++- 6 files changed, 202 insertions(+), 15 deletions(-) diff --git a/classes/booking_option.php b/classes/booking_option.php index ba8f0118a..4757bbe79 100755 --- a/classes/booking_option.php +++ b/classes/booking_option.php @@ -3122,6 +3122,7 @@ public static function return_cancel_until_date($optionid) { $allowupdatedays = $bookingsettings->allowupdatedays; if ( !empty($bookingsettings->cancelrelativedate) && + $bookingsettings->cancelrelativedate == 1 && isset($allowupdatedays) && $allowupdatedays != 10000 && !empty($starttime) diff --git a/classes/observer.php b/classes/observer.php index df586ffa7..43bddef4e 100755 --- a/classes/observer.php +++ b/classes/observer.php @@ -159,10 +159,14 @@ public static function bookinganswer_cancelled(\mod_booking\event\bookinganswer_ * @throws dml_exception */ public static function bookingoption_cancelled(\mod_booking\event\bookingoption_cancelled $event) { - rules_info::$eventstoexecute[] = function () use ($event) { $optionid = $event->objectid; $settings = singleton_service::get_instance_of_booking_option_settings($optionid); + + // We don't test. + if (PHPUNIT_TEST && empty($settings->cmid)) { + return; + } $bookingoption = singleton_service::get_instance_of_booking_option($settings->cmid, $optionid); $bookinganswer = singleton_service::get_instance_of_booking_answers($settings); diff --git a/lang/de/booking.php b/lang/de/booking.php index 33281610a..af720fce2 100755 --- a/lang/de/booking.php +++ b/lang/de/booking.php @@ -600,6 +600,7 @@ $string['campaignstart'] = 'Beginn der Kampagne'; $string['campaignstart_help'] = 'Wann soll die Kampagne starten?'; $string['campaigntype'] = 'Kampagnentyp'; +$string['cancancelbookabsolute'] = 'Stornodatum mit fixem Datum setzen'; $string['cancancelbookallow'] = 'Teilnehmer:innen dürfen Buchungen selbst stornieren'; $string['cancancelbookdays'] = 'Nutzer:innen können nur bis n Tage vor Kursstart stornieren. Negative Werte meinen n Tage NACH Kursstart.'; $string['cancancelbookdays:bookingclosingtime'] = 'Nutzer:innen können nur bis n Tage vor Anmeldeschluss (Buchungsende) stornieren. Negative Werte meinen n Tage NACH Anmeldeschluss.'; @@ -607,6 +608,9 @@ $string['cancancelbookdays:semesterstart'] = 'Nutzer:innen können nur bis n Tage vor Semesterbeginn stornieren. Negative Werte meinen n Tage NACH Semesterbeginn.'; $string['cancancelbookdaysno'] = 'Kein Limit'; $string['cancancelbookrelative'] = 'Stornodatum relativ zu {$a} setzen'; +$string['cancancelbooksetting'] = 'Stornobedingen definieren'; +$string['cancancelbooksetting_help'] = 'Diese Einstellungen können durch die Einstellugnen in den einzelnen Buchungsoptionen überschrieben werden.'; +$string['cancancelbookunlimited'] = 'Stornieren ohne limit möglich.'; $string['cancel'] = 'Abbrechen'; $string['cancelallusers'] = 'Alle gebuchten Teilnehmer:innen stornieren'; $string['cancelbooking'] = 'Buchung stornieren'; diff --git a/lang/en/booking.php b/lang/en/booking.php index ba8e67c3b..48afd3209 100755 --- a/lang/en/booking.php +++ b/lang/en/booking.php @@ -612,13 +612,17 @@ $string['campaignstart'] = 'Start of campaign'; $string['campaignstart_help'] = 'When does the campaign start?'; $string['campaigntype'] = 'Campaign type'; +$string['cancancelbookabsolute'] = 'Set cancellation date with a fixed date'; $string['cancancelbookallow'] = 'Allow users to cancel their booking themselves'; $string['cancancelbookdays'] = 'Disallow users to cancel their booking n days before start. Minus means, that users can still cancel n days AFTER course start.'; $string['cancancelbookdays:bookingclosingtime'] = 'Disallow users to cancel their booking n days before registration end (booking closing time). Minus means, that users can still cancel n days AFTER registration end.'; $string['cancancelbookdays:bookingopeningtime'] = 'Disallow users to cancel their booking n days before registration start (booking opening time). Minus means, that users can still cancel n days AFTER registration start.'; $string['cancancelbookdays:semesterstart'] = 'Disallow users to cancel their booking n days before semester start. Minus means, that users can still cancel n days AFTER semester start.'; $string['cancancelbookdaysno'] = "Don't limit"; -$string['cancancelbookrelative'] = 'Set cancelling date relative to {$a}'; +$string['cancancelbookrelative'] = 'Set cancellation date relative to {$a}'; +$string['cancancelbooksetting'] = 'Define cancellation conditions'; +$string['cancancelbooksetting_help'] = 'These settings can be overriden by more special settings in the booking options'; +$string['cancancelbookunlimited'] = 'Cancellation without limits is possible.'; $string['cancel'] = 'Cancel'; $string['cancelallusers'] = 'Cancel all booked users'; $string['cancelbooking'] = 'Cancel booking'; diff --git a/mod_form.php b/mod_form.php index e30e161ec..36719522a 100755 --- a/mod_form.php +++ b/mod_form.php @@ -779,15 +779,29 @@ public function definition() { // Cancel date is either absolute or relative to defined start. $strid = 'cdo:' . $canceldependenton; $a = get_string($strid, 'mod_booking'); - $mform->addElement('advcheckbox', 'cancelrelativedate', get_string('cancancelbookrelative', 'mod_booking', $a)); + + $canceloptions = [ + 0 => get_string('cancancelbookabsolute', 'mod_booking'), + 1 => get_string('cancancelbookrelative', 'mod_booking', $a), + 2 => get_string('cancancelbookunlimited', 'mod_booking'), + ]; + + $mform->addElement( + 'select', + 'cancelrelativedate', + get_string('cancancelbooksetting', 'booking'), + $canceloptions + ); + $mform->addHelpButton('cancelrelativedate', 'cancancelbooksetting', 'mod_booking'); + $mform->hideIf('cancelrelativedate', 'cancancelbook', 'eq', 0); - $mform->hideIf('cancelrelativedate', 'disablecancel', 'eq', 1); + $mform->hideIf('cancelrelativedate', 'disablecancel', 'neq', 0); $mform->setDefault('cancelrelativedate', (int)booking::get_value_of_json_by_key($bookingid, 'cancelrelativedate') ?? 1); $mform->addElement('date_time_selector', 'allowupdatetimestamp', get_string('canceldateabsolute', 'mod_booking')); $mform->hideIf('allowupdatetimestamp', 'cancancelbook', 'eq', 0); - $mform->hideIf('allowupdatetimestamp', 'cancelrelativedate', 'eq', 1); + $mform->hideIf('allowupdatetimestamp', 'cancelrelativedate', 'neq', 0); $mform->setDefault('allowupdatetimestamp', booking::get_value_of_json_by_key($bookingid, 'allowupdatetimestamp') ?? ''); @@ -795,7 +809,7 @@ public function definition() { $extraopts = array_combine(range(-100, 100), range(-100, 100)); $opts = $opts + $extraopts; $mform->addElement('select', 'allowupdatedays', $cancancelbookdaysstring, $opts); - $mform->hideIf('allowupdatedays', 'cancelrelativedate', 'eq', 0); + $mform->hideIf('allowupdatedays', 'cancelrelativedate', 'neq', 1); $mform->setDefault('allowupdatedays', 10000); // One million means "no limit". $mform->disabledIf('allowupdatedays', 'cancancelbook', 'eq', 0); diff --git a/tests/bo_availability/condition_allowupdate_test.php b/tests/bo_availability/condition_allowupdate_test.php index e002b42a0..30650aeee 100644 --- a/tests/bo_availability/condition_allowupdate_test.php +++ b/tests/bo_availability/condition_allowupdate_test.php @@ -118,7 +118,7 @@ public function test_booking_bookit_allowupdate(array $bdata): void { $this->setUser($student1); // Try to book again with user1. - list($id, $isavailable, $description) = $boinfo->is_available($settings->id, $student1->id, true); + [$id, $isavailable, $description] = $boinfo->is_available($settings->id, $student1->id, true); $this->assertEquals(MOD_BOOKING_BO_COND_ISCANCELLED, $id); // Now we undo cancel of the booking option. @@ -126,10 +126,161 @@ public function test_booking_bookit_allowupdate(array $bdata): void { // Try to book again with user1. $this->setUser($student1); - list($id, $isavailable, $description) = $boinfo->is_available($settings->id, $student1->id, true); + [$id, $isavailable, $description] = $boinfo->is_available($settings->id, $student1->id, true); $this->assertEquals(MOD_BOOKING_BO_COND_OPTIONHASSTARTED, $id); } + /** + * Test cancelation with all cancelrelativedate options. + * + * @covers \condition\iscancelled::is_available + * @covers \condition\hasstarted::is_available + * @param array $bdata + * @throws \coding_exception + * @throws \dml_exception + * + * @dataProvider booking_common_settings_provider + */ + public function test_booking_bookit_cancelrelativedate(array $bdata): void { + global $DB, $CFG; + + singleton_service::destroy_instance(); + + $bdata['cancancelbook'] = 0; + $bdata['allowupdate'] = 1; + + // Setup test data. + $course = $this->getDataGenerator()->create_course(['enablecompletion' => 1]); + + // Create users. + $admin = $this->getDataGenerator()->create_user(); + $student1 = $this->getDataGenerator()->create_user(); + $student2 = $this->getDataGenerator()->create_user(); + $teacher = $this->getDataGenerator()->create_user(); + $bookingmanager = $this->getDataGenerator()->create_user(); // Booking manager. + + $bdata['course'] = $course->id; + $bdata['bookingmanager'] = $bookingmanager->username; + + $bdata['cancancelbook'] = 0; + + $bookings = []; + // Booking option where cancel is not allowed. + $bookings[0] = $this->getDataGenerator()->create_module('booking', $bdata); + + // Booking option where cancel is allowed. + $bdata['cancancelbook'] = 1; + $bookings[1] = $this->getDataGenerator()->create_module('booking', $bdata); + + // Booking option where cancel is allowed until precise date (5 days from now). + // So allowed now. + $bdata['cancancelbook'] = 1; + $bdata['allowupdatetimestamp'] = strtotime('now + 5 days'); + $bdata['cancelrelativedate'] = 0; + $bookings[2] = $this->getDataGenerator()->create_module('booking', $bdata); + + // Booking option where cancel is allowed until precise date (5 days from now). + // So not allowed now. + $bdata['cancancelbook'] = 1; + $bdata['allowupdatetimestamp'] = strtotime('now - 5 days'); + $bdata['cancelrelativedate'] = 0; + $bookings[3] = $this->getDataGenerator()->create_module('booking', $bdata); + + // Booking option where cancel is allowed only 20 days before coursestart. + // So, not allowed now. + $bdata['cancancelbook'] = 1; + $bdata['cancelrelativedate'] = 1; + $bdata['allowupdatedays'] = 20; + $bookings[4] = $this->getDataGenerator()->create_module('booking', $bdata); + + // Booking option where cancel is allowed only 2 days before coursestart. + // So, allowed now. + $bdata['cancancelbook'] = 1; + $bdata['cancelrelativedate'] = 1; + $bdata['allowupdatedays'] = 2; + $bookings[5] = $this->getDataGenerator()->create_module('booking', $bdata); + + // Booking option where cancel is without limit. + // So, allowed now. + $bdata['cancancelbook'] = 1; + $bdata['cancelrelativedate'] = 2; + $bookings[6] = $this->getDataGenerator()->create_module('booking', $bdata); + + $this->setAdminUser(); + + $this->getDataGenerator()->enrol_user($admin->id, $course->id); + $this->getDataGenerator()->enrol_user($student1->id, $course->id); + $this->getDataGenerator()->enrol_user($student2->id, $course->id); + $this->getDataGenerator()->enrol_user($teacher->id, $course->id); + $this->getDataGenerator()->enrol_user($bookingmanager->id, $course->id); + + $record1 = new stdClass(); + $record1->text = 'Test option1'; + $record1->courseid = 0; + $record1->maxanswers = 2; + $record1->coursestarttime = strtotime('now + 10 days'); + $record1->courseendtime = strtotime('now + 12 days'); + + $record2 = new stdClass(); + $record2->text = 'Test option1'; + $record2->courseid = 0; + $record2->maxanswers = 2; + $record2->coursestarttime = strtotime('now - 10 days'); + $record2->courseendtime = strtotime('now + 12 days'); + + /** @var mod_booking_generator $plugingenerator */ + $plugingenerator = self::getDataGenerator()->get_plugin_generator('mod_booking'); + + $expectedresults1 = [ + 0 => MOD_BOOKING_BO_COND_ALREADYBOOKED, + 1 => MOD_BOOKING_BO_COND_CONFIRMCANCEL, + 2 => MOD_BOOKING_BO_COND_CONFIRMCANCEL, + 3 => MOD_BOOKING_BO_COND_ALREADYBOOKED, + 4 => MOD_BOOKING_BO_COND_ALREADYBOOKED, + 5 => MOD_BOOKING_BO_COND_CONFIRMCANCEL, + 6 => MOD_BOOKING_BO_COND_CONFIRMCANCEL, + ]; + + $expectedresults2 = [ + 0 => MOD_BOOKING_BO_COND_ALREADYBOOKED, + 1 => MOD_BOOKING_BO_COND_CONFIRMCANCEL, + 2 => MOD_BOOKING_BO_COND_CONFIRMCANCEL, + 3 => MOD_BOOKING_BO_COND_ALREADYBOOKED, + 4 => MOD_BOOKING_BO_COND_ALREADYBOOKED, + 5 => MOD_BOOKING_BO_COND_ALREADYBOOKED, + 6 => MOD_BOOKING_BO_COND_CONFIRMCANCEL, + ]; + + foreach ($bookings as $key => $booking) { + $this->setAdminUser(); + $record1->bookingid = $booking->id; + $record2->bookingid = $booking->id; + $option1 = $plugingenerator->create_option($record1); + $settings1 = singleton_service::get_instance_of_booking_option_settings($option1->id); + $boinfo1 = new bo_info($settings1); + + $option2 = $plugingenerator->create_option($record2); + $settings2 = singleton_service::get_instance_of_booking_option_settings($option2->id); + $boinfo2 = new bo_info($settings2); + + // Try to book again with user1. + $this->setUser($student1); + $result = booking_bookit::bookit('option', $settings1->id, $student1->id); + $result = booking_bookit::bookit('option', $settings1->id, $student1->id); + + $result = booking_bookit::bookit('option', $settings1->id, $student1->id); + [$id, $isavailable, $description] = $boinfo1->is_available($settings1->id, $student1->id, true); + $this->assertEquals($expectedresults1[$key], $id); + + $result = booking_bookit::bookit('option', $settings2->id, $student1->id); + $result = booking_bookit::bookit('option', $settings2->id, $student1->id); + + $result = booking_bookit::bookit('option', $settings2->id, $student1->id); + [$id, $isavailable, $description] = $boinfo1->is_available($settings2->id, $student1->id, true); + $this->assertEquals($expectedresults2[$key], $id); + } + } + /** * Test isbookable, bookitbutton, alreadybooked. * @@ -183,7 +334,7 @@ public function test_booking_bookit_isbookable(array $bdata): void { $this->setUser($student1); // Try to book again with user1. - list($id, $isavailable, $description) = $boinfo1->is_available($settings1->id, $student1->id, true); + [$id, $isavailable, $description] = $boinfo1->is_available($settings1->id, $student1->id, true); $this->assertEquals(MOD_BOOKING_BO_COND_ISBOOKABLE, $id); // Now we enable option1 back. @@ -195,13 +346,13 @@ public function test_booking_bookit_isbookable(array $bdata): void { // Try to book again with user1. $this->setUser($student1); - list($id, $isavailable, $description) = $boinfo1->is_available($settings1->id, $student1->id, true); + [$id, $isavailable, $description] = $boinfo1->is_available($settings1->id, $student1->id, true); $this->assertEquals(MOD_BOOKING_BO_COND_BOOKITBUTTON, $id); // That was just for fun. Now we make sure the student1 will be booked. $result = booking_bookit::bookit('option', $settings1->id, $student1->id); $result = booking_bookit::bookit('option', $settings1->id, $student1->id); - list($id, $isavailable, $description) = $boinfo1->is_available($settings1->id, $student1->id, true); + [$id, $isavailable, $description] = $boinfo1->is_available($settings1->id, $student1->id, true); $this->assertEquals(MOD_BOOKING_BO_COND_ALREADYBOOKED, $id); } @@ -297,13 +448,13 @@ public function test_booking_bookit_subbookings(array $bdata): void { $this->setUser($student1); // Validate that subboking is available and non-bloking. - list($id, $isavailable, $description) = $boinfo1->is_available($settings1->id, $student1->id, false); + [$id, $isavailable, $description] = $boinfo1->is_available($settings1->id, $student1->id, false); $this->assertEquals(MOD_BOOKING_BO_COND_SUBBOOKING, $id); // Book option1 by student1. $result = booking_bookit::bookit('option', $settings1->id, $student1->id); $result = booking_bookit::bookit('option', $settings1->id, $student1->id); - list($id, $isavailable, $description) = $boinfo1->is_available($settings1->id, $student1->id, true); + [$id, $isavailable, $description] = $boinfo1->is_available($settings1->id, $student1->id, true); $this->assertEquals(MOD_BOOKING_BO_COND_ALREADYBOOKED, $id); // Set blocking subbooking. @@ -338,13 +489,13 @@ public function test_booking_bookit_subbookings(array $bdata): void { // Try to book option2 with student2. $this->setUser($student2); // Validate that subboking is available and non-bloking. - list($id, $isavailable, $description) = $boinfo2->is_available($settings2->id, $student2->id, false); + [$id, $isavailable, $description] = $boinfo2->is_available($settings2->id, $student2->id, false); $this->assertEquals(MOD_BOOKING_BO_COND_SUBBOOKINGBLOCKS, $id); // Book option2 by student2. $result = booking_bookit::bookit('option', $settings2->id, $student2->id); $result = booking_bookit::bookit('option', $settings2->id, $student2->id); - list($id, $isavailable, $description) = $boinfo2->is_available($settings2->id, $student2->id, true); + [$id, $isavailable, $description] = $boinfo2->is_available($settings2->id, $student2->id, true); $this->assertEquals(MOD_BOOKING_BO_COND_ALREADYBOOKED, $id); // TODO: how to make subboking in code? @@ -378,4 +529,13 @@ public static function booking_common_settings_provider(): array { ]; return ['bdata' => [$bdata]]; } + + /** + * Mandatory clean-up after each test. + */ + public function tearDown(): void { + parent::tearDown(); + // Mandatory clean-up. + singleton_service::destroy_instance(); + } }