From 8c4b939cfec3b6ee5bf90d37bd2c19fa05513009 Mon Sep 17 00:00:00 2001 From: Ryan Wyllie Date: Thu, 28 Sep 2017 06:56:40 +0000 Subject: [PATCH] MDL-60058 choice: implement timestart range callback for calendar UI --- mod/choice/lib.php | 113 +++++++++++++++++++---- mod/choice/tests/lib_test.php | 164 +++++++++++++++++++++++++++++++++- 2 files changed, 256 insertions(+), 21 deletions(-) diff --git a/mod/choice/lib.php b/mod/choice/lib.php index 85441c326fc..8f8513231a8 100644 --- a/mod/choice/lib.php +++ b/mod/choice/lib.php @@ -1240,6 +1240,55 @@ function mod_choice_core_calendar_provide_event_action(calendar_event $event, ); } +/** + * This function calculates the minimum and maximum cutoff values for the timestart of + * the given event. + * + * It will return an array with two values, the first being the minimum cutoff value and + * the second being the maximum cutoff value. Either or both values can be null, which + * indicates there is no minimum or maximum, respectively. + * + * If a cutoff is required then the function must return an array containing the cutoff + * timestamp and error string to display to the user if the cutoff value is violated. + * + * A minimum and maximum cutoff return value will look like: + * [ + * [1505704373, 'The date must be after this date'], + * [1506741172, 'The date must be before this date'] + * ] + * + * @param calendar_event $event The calendar event to get the time range for + * @param stdClass|null $instance The module instance to get the range from + */ +function mod_choice_core_calendar_get_valid_event_timestart_range(\calendar_event $event, \stdClass $choice = null) { + global $DB; + + if (!$choice) { + $choice = $DB->get_record('choice', ['id' => $event->instance]); + } + + $mindate = null; + $maxdate = null; + + if ($event->eventtype == CHOICE_EVENT_TYPE_OPEN) { + if (!empty($choice->timeclose)) { + $maxdate = [ + $choice->timeclose, + get_string('openafterclose', 'choice') + ]; + } + } else if ($event->eventtype == CHOICE_EVENT_TYPE_CLOSE) { + if (!empty($choice->timeopen)) { + $mindate = [ + $choice->timeopen, + get_string('closebeforeopen', 'choice') + ]; + } + } + + return [$mindate, $maxdate]; +} + /** * This function will check that the given event is valid for it's * corresponding choice module. @@ -1253,23 +1302,23 @@ function mod_choice_core_calendar_provide_event_action(calendar_event $event, function mod_choice_core_calendar_validate_event_timestart(\calendar_event $event) { global $DB; - $record = $DB->get_record('choice', ['id' => $event->instance], '*', MUST_EXIST); - - if ($event->eventtype == CHOICE_EVENT_TYPE_OPEN) { - // The start time of the open event can't be equal to or after the - // close time of the choice activity. - if (!empty($record->timeclose) && $event->timestart > $record->timeclose) { - throw new \moodle_exception('openafterclose', 'choice'); - } - } else if ($event->eventtype == CHOICE_EVENT_TYPE_CLOSE) { - // The start time of the close event can't be equal to or earlier than the - // open time of the choice activity. - if (!empty($record->timeopen) && $event->timestart < $record->timeopen) { - throw new \moodle_exception('closebeforeopen', 'choice'); - } + if (!isset($event->instance)) { + return; } - return true; + // We need to read from the DB directly because course module may + // currently be getting created so it won't be in mod info yet. + $instance = $DB->get_record('choice', ['id' => $event->instance], '*', MUST_EXIST); + $timestart = $event->timestart; + list($min, $max) = mod_choice_core_calendar_get_valid_event_timestart_range($event, $instance); + + if ($min && $timestart < $min[0]) { + throw new \moodle_exception($min[1]); + } + + if ($max && $timestart > $max[0]) { + throw new \moodle_exception($max[1]); + } } /** @@ -1285,29 +1334,55 @@ function mod_choice_core_calendar_validate_event_timestart(\calendar_event $even function mod_choice_core_calendar_event_timestart_updated(\calendar_event $event) { global $DB; + $courseid = $event->courseid; + $modulename = $event->modulename; + $instanceid = $event->instance; + $modified = false; + + // Something weird going on. The event is for a different module so + // we should ignore it. + if ($modulename != 'choice') { + return; + } + + $coursemodule = get_fast_modinfo($courseid)->instances[$modulename][$instanceid]; + $context = context_module::instance($coursemodule->id); + + // The user does not have the capability to modify this activity. + if (!has_capability('moodle/course:manageactivities', $context)) { + return; + } + if ($event->eventtype == CHOICE_EVENT_TYPE_OPEN) { // If the event is for the choice activity opening then we should // set the start time of the choice activity to be the new start // time of the event. - $record = $DB->get_record('choice', ['id' => $event->instance], '*', MUST_EXIST); + $record = $DB->get_record('choice', ['id' => $instanceid], '*', MUST_EXIST); if ($record->timeopen != $event->timestart) { $record->timeopen = $event->timestart; $record->timemodified = time(); - $DB->update_record('choice', $record); + $modified = true; } } else if ($event->eventtype == CHOICE_EVENT_TYPE_CLOSE) { // If the event is for the choice activity closing then we should // set the end time of the choice activity to be the new start // time of the event. - $record = $DB->get_record('choice', ['id' => $event->instance], '*', MUST_EXIST); + $record = $DB->get_record('choice', ['id' => $instanceid], '*', MUST_EXIST); if ($record->timeclose != $event->timestart) { $record->timeclose = $event->timestart; $record->timemodified = time(); - $DB->update_record('choice', $record); + $modified = true; } } + + if ($modified) { + // Persist the instance changes. + $DB->update_record('choice', $record); + $event = \core\event\course_module_updated::create_from_cm($coursemodule, $context); + $event->trigger(); + } } /** diff --git a/mod/choice/tests/lib_test.php b/mod/choice/tests/lib_test.php index 5d1c19cc841..92ee1bf316e 100644 --- a/mod/choice/tests/lib_test.php +++ b/mod/choice/tests/lib_test.php @@ -554,7 +554,10 @@ class mod_choice_lib_testcase extends externallib_advanced_testcase { 'visible' => 1 ]); - $this->assertTrue(mod_choice_core_calendar_validate_event_timestart($event)); + mod_choice_core_calendar_validate_event_timestart($event); + // The function above will throw an exception if the event is + // invalid. + $this->assertTrue(true); } /** @@ -630,7 +633,10 @@ class mod_choice_lib_testcase extends externallib_advanced_testcase { 'visible' => 1 ]); - $this->assertTrue(mod_choice_core_calendar_validate_event_timestart($event)); + mod_choice_core_calendar_validate_event_timestart($event); + // The function above will throw an exception if the event isn't + // valid. + $this->assertTrue(true); } /** @@ -754,8 +760,16 @@ class mod_choice_lib_testcase extends externallib_advanced_testcase { 'visible' => 1 ]); + // Trigger and capture the event when adding a contact. + $sink = $this->redirectEvents(); + mod_choice_core_calendar_event_timestart_updated($event); + $triggeredevents = $sink->get_events(); + $moduleupdatedevents = array_filter($triggeredevents, function($e) { + return is_a($e, 'core\event\course_module_updated'); + }); + $choice = $DB->get_record('choice', ['id' => $choice->id]); // Ensure the timeopen property matches the event timestart. $this->assertEquals($newtimeopen, $choice->timeopen); @@ -763,6 +777,9 @@ class mod_choice_lib_testcase extends externallib_advanced_testcase { $this->assertEquals($timeclose, $choice->timeclose); // Ensure the timemodified property has been changed. $this->assertNotEquals($timemodified, $choice->timemodified); + // Confirm that a module updated event is fired when the module + // is changed. + $this->assertNotEmpty($moduleupdatedevents); } /** @@ -804,8 +821,16 @@ class mod_choice_lib_testcase extends externallib_advanced_testcase { 'visible' => 1 ]); + // Trigger and capture the event when adding a contact. + $sink = $this->redirectEvents(); + mod_choice_core_calendar_event_timestart_updated($event); + $triggeredevents = $sink->get_events(); + $moduleupdatedevents = array_filter($triggeredevents, function($e) { + return is_a($e, 'core\event\course_module_updated'); + }); + $choice = $DB->get_record('choice', ['id' => $choice->id]); // Ensure the timeclose property matches the event timestart. $this->assertEquals($newtimeclose, $choice->timeclose); @@ -813,5 +838,140 @@ class mod_choice_lib_testcase extends externallib_advanced_testcase { $this->assertEquals($timeopen, $choice->timeopen); // Ensure the timemodified property has been changed. $this->assertNotEquals($timemodified, $choice->timemodified); + // Confirm that a module updated event is fired when the module + // is changed. + $this->assertNotEmpty($moduleupdatedevents); + } + + /** + * An unkown event type should not have any limits + */ + public function test_mod_choice_core_calendar_get_valid_event_timestart_range_unknown_event() { + global $CFG, $DB; + require_once($CFG->dirroot . "/calendar/lib.php"); + + $this->resetAfterTest(true); + $this->setAdminUser(); + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $timeopen = time(); + $timeclose = $timeopen + DAYSECS; + $choice = new \stdClass(); + $choice->timeopen = $timeopen; + $choice->timeclose = $timeclose; + + // Create a valid event. + $event = new \calendar_event([ + 'name' => 'Test event', + 'description' => '', + 'format' => 1, + 'courseid' => $course->id, + 'groupid' => 0, + 'userid' => 2, + 'modulename' => 'choice', + 'instance' => 1, + 'eventtype' => CHOICE_EVENT_TYPE_OPEN . "SOMETHING ELSE", + 'timestart' => 1, + 'timeduration' => 86400, + 'visible' => 1 + ]); + + list ($min, $max) = mod_choice_core_calendar_get_valid_event_timestart_range($event, $choice); + $this->assertNull($min); + $this->assertNull($max); + } + + /** + * The open event should be limited by the choice's timeclose property, if it's set. + */ + public function test_mod_choice_core_calendar_get_valid_event_timestart_range_open_event() { + global $CFG, $DB; + require_once($CFG->dirroot . "/calendar/lib.php"); + + $this->resetAfterTest(true); + $this->setAdminUser(); + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $timeopen = time(); + $timeclose = $timeopen + DAYSECS; + $choice = new \stdClass(); + $choice->timeopen = $timeopen; + $choice->timeclose = $timeclose; + + // Create a valid event. + $event = new \calendar_event([ + 'name' => 'Test event', + 'description' => '', + 'format' => 1, + 'courseid' => $course->id, + 'groupid' => 0, + 'userid' => 2, + 'modulename' => 'choice', + 'instance' => 1, + 'eventtype' => CHOICE_EVENT_TYPE_OPEN, + 'timestart' => 1, + 'timeduration' => 86400, + 'visible' => 1 + ]); + + // The max limit should be bounded by the timeclose value. + list ($min, $max) = mod_choice_core_calendar_get_valid_event_timestart_range($event, $choice); + + $this->assertNull($min); + $this->assertEquals($timeclose, $max[0]); + + // No timeclose value should result in no upper limit. + $choice->timeclose = 0; + list ($min, $max) = mod_choice_core_calendar_get_valid_event_timestart_range($event, $choice); + + $this->assertNull($min); + $this->assertNull($max); + } + + /** + * The close event should be limited by the choice's timeopen property, if it's set. + */ + public function test_mod_choice_core_calendar_get_valid_event_timestart_range_close_event() { + global $CFG, $DB; + require_once($CFG->dirroot . "/calendar/lib.php"); + + $this->resetAfterTest(true); + $this->setAdminUser(); + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $timeopen = time(); + $timeclose = $timeopen + DAYSECS; + $choice = new \stdClass(); + $choice->timeopen = $timeopen; + $choice->timeclose = $timeclose; + + // Create a valid event. + $event = new \calendar_event([ + 'name' => 'Test event', + 'description' => '', + 'format' => 1, + 'courseid' => $course->id, + 'groupid' => 0, + 'userid' => 2, + 'modulename' => 'choice', + 'instance' => 1, + 'eventtype' => CHOICE_EVENT_TYPE_CLOSE, + 'timestart' => 1, + 'timeduration' => 86400, + 'visible' => 1 + ]); + + // The max limit should be bounded by the timeclose value. + list ($min, $max) = mod_choice_core_calendar_get_valid_event_timestart_range($event, $choice); + + $this->assertEquals($timeopen, $min[0]); + $this->assertNull($max); + + // No timeclose value should result in no upper limit. + $choice->timeopen = 0; + list ($min, $max) = mod_choice_core_calendar_get_valid_event_timestart_range($event, $choice); + + $this->assertNull($min); + $this->assertNull($max); } }