From 9aac9f70748c016419cc037d5213d643a1e8d2e2 Mon Sep 17 00:00:00 2001 From: Ryan Wyllie Date: Thu, 5 Oct 2017 08:07:07 +0000 Subject: [PATCH] MDL-60062 quiz: add support for drag drop of calendar events --- mod/quiz/lang/en/quiz.php | 1 + mod/quiz/lib.php | 172 +++++ mod/quiz/locallib.php | 39 ++ .../tests/calendar_event_modified_test.php | 638 ++++++++++++++++++ mod/quiz/tests/locallib_test.php | 144 ++++ 5 files changed, 994 insertions(+) create mode 100644 mod/quiz/tests/calendar_event_modified_test.php diff --git a/mod/quiz/lang/en/quiz.php b/mod/quiz/lang/en/quiz.php index 7ef3a14683a..4b61fe42eac 100644 --- a/mod/quiz/lang/en/quiz.php +++ b/mod/quiz/lang/en/quiz.php @@ -565,6 +565,7 @@ $string['onlyteachersexport'] = 'Only teachers can export questions'; $string['onlyteachersimport'] = 'Only teachers with editing rights can import questions'; $string['onthispage'] = 'This page'; $string['open'] = 'Not answered'; +$string['openafterclose'] = 'Could not update the quiz. You have specified an open date after the close date.'; $string['openclosedatesupdated'] = 'Quiz open and close dates updated'; $string['optional'] = 'optional'; $string['orderandpaging'] = 'Order and paging'; diff --git a/mod/quiz/lib.php b/mod/quiz/lib.php index 499f0386c29..4ee34de8e63 100644 --- a/mod/quiz/lib.php +++ b/mod/quiz/lib.php @@ -2253,3 +2253,175 @@ function mod_quiz_get_completion_active_rule_descriptions($cm) { } return $descriptions; } + +/** + * Returns the min and max values for the timestart property of a quiz + * activity event. + * + * The min and max values will be the timeopen and timeclose properties + * of the quiz, respectively, if they are set. + * + * If either value isn't set then null will be returned instead to + * indicate that there is no cutoff for that value. + * + * 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 $quiz The module instance to get the range from + * @return array + */ +function mod_quiz_core_calendar_get_valid_event_timestart_range(\calendar_event $event, \stdClass $quiz = null) { + global $CFG, $DB; + require_once($CFG->dirroot . '/mod/quiz/locallib.php'); + + // No restrictions on override events. + if (quiz_is_overriden_calendar_event($event)) { + return [null, null]; + } + + if (!$quiz) { + $quiz = $DB->get_record('quiz', ['id' => $event->instance]); + } + + $mindate = null; + $maxdate = null; + + if ($event->eventtype == QUIZ_EVENT_TYPE_OPEN) { + if (!empty($quiz->timeclose)) { + $maxdate = [ + $quiz->timeclose, + get_string('openafterclose', 'quiz') + ]; + } + } else if ($event->eventtype == QUIZ_EVENT_TYPE_CLOSE) { + if (!empty($quiz->timeopen)) { + $mindate = [ + $quiz->timeopen, + get_string('closebeforeopen', 'quiz') + ]; + } + } + + return [$mindate, $maxdate]; +} + +/** + * This function will check that the given event is valid for it's + * corresponding quiz module. + * + * An exception is thrown if the event fails validation. + * + * @throws \moodle_exception + * @param \calendar_event $event + * @return bool + */ +function mod_quiz_core_calendar_validate_event_timestart(\calendar_event $event) { + global $DB; + + if (!isset($event->instance)) { + return; + } + + // Something weird going on. The event is for a different module so + // we should ignore it. + if ($event->modulename != 'quiz') { + return; + } + + // 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. + $quiz = $DB->get_record('quiz', ['id' => $event->instance], '*', MUST_EXIST); + $timestart = $event->timestart; + list($min, $max) = mod_quiz_core_calendar_get_valid_event_timestart_range($event, $quiz); + + if ($min && $timestart < $min[0]) { + throw new \moodle_exception($min[1]); + } + + if ($max && $timestart > $max[0]) { + throw new \moodle_exception($max[1]); + } +} + +/** + * This function will update the quiz module according to the + * event that has been modified. + * + * It will set the timeopen or timeclose value of the quiz instance + * according to the type of event provided. + * + * @throws \moodle_exception + * @param \calendar_event $event + */ +function mod_quiz_core_calendar_event_timestart_updated(\calendar_event $event) { + global $CFG, $DB; + require_once($CFG->dirroot . '/mod/quiz/locallib.php'); + + // We don't update the activity if it's an override event that has + // been modified. + if (quiz_is_overriden_calendar_event($event)) { + return; + } + + $courseid = $event->courseid; + $modulename = $event->modulename; + $instanceid = $event->instance; + $modified = false; + $closedatechanged = false; + + // Something weird going on. The event is for a different module so + // we should ignore it. + if ($modulename != 'quiz') { + 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 == QUIZ_EVENT_TYPE_OPEN) { + // If the event is for the quiz activity opening then we should + // set the start time of the quiz activity to be the new start + // time of the event. + $quiz = $DB->get_record('quiz', ['id' => $instanceid], '*', MUST_EXIST); + + if ($quiz->timeopen != $event->timestart) { + $quiz->timeopen = $event->timestart; + $modified = true; + } + } else if ($event->eventtype == QUIZ_EVENT_TYPE_CLOSE) { + // If the event is for the quiz activity closing then we should + // set the end time of the quiz activity to be the new start + // time of the event. + $quiz = $DB->get_record('quiz', ['id' => $instanceid], '*', MUST_EXIST); + + if ($quiz->timeclose != $event->timestart) { + $quiz->timeclose = $event->timestart; + $modified = true; + $closedatechanged = true; + } + } + + if ($modified) { + $quiz->timemodified = time(); + $DB->update_record('quiz', $quiz); + + if ($closedatechanged) { + quiz_update_open_attempts(array('quizid' => $quiz->id)); + } + + // Delete any previous preview attempts. + quiz_delete_previews($quiz); + quiz_update_events($quiz); + $event = \core\event\course_module_updated::create_from_cm($coursemodule, $context); + $event->trigger(); + } +} diff --git a/mod/quiz/locallib.php b/mod/quiz/locallib.php index c1164f60ad9..36bca77dd7a 100644 --- a/mod/quiz/locallib.php +++ b/mod/quiz/locallib.php @@ -2316,3 +2316,42 @@ function quiz_prepare_and_start_new_attempt(quiz $quizobj, $attemptnumber, $last return $attempt; } + +/** + * Check if the given calendar_event is either a user or group override + * event for quiz. + * + * @param calendar_event $event The calendar event to check + * @return bool + */ +function quiz_is_overriden_calendar_event(\calendar_event $event) { + global $DB; + + if (!isset($event->modulename)) { + return false; + } + + if ($event->modulename != 'quiz') { + return false; + } + + if (!isset($event->instance)) { + return false; + } + + if (!isset($event->userid) && !isset($event->groupid)) { + return false; + } + + $overrideparams = [ + 'quiz' => $event->instance + ]; + + if (isset($event->groupid)) { + $overrideparams['groupid'] = $event->groupid; + } else if (isset($event->userid)) { + $overrideparams['userid'] = $event->userid; + } + + return $DB->record_exists('quiz_overrides', $overrideparams); +} diff --git a/mod/quiz/tests/calendar_event_modified_test.php b/mod/quiz/tests/calendar_event_modified_test.php new file mode 100644 index 00000000000..ac36dd5b203 --- /dev/null +++ b/mod/quiz/tests/calendar_event_modified_test.php @@ -0,0 +1,638 @@ +. + +/** + * Unit tests for the calendar event modification callbacks used + * for dragging and dropping quiz calendar events in the calendar + * UI. + * + * @package mod_quiz + * @category test + * @copyright 2017 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU Public License + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/mod/quiz/lib.php'); + +/** + * @copyright 2017 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU Public License + */ +class mod_quiz_calendar_event_modified_testcase extends advanced_testcase { + + /** + * Create an instance of the quiz activity. + * + * @param array $properties Properties to set on the activity + * @return stdClass Quiz activity instance + */ + protected function create_quiz_instance(array $properties) { + global $DB; + + $generator = $this->getDataGenerator(); + + if (empty($properties['course'])) { + $course = $generator->create_course(); + $courseid = $course->id; + } else { + $courseid = $properties['course']; + } + + $quizgenerator = $generator->get_plugin_generator('mod_quiz'); + $quiz = $quizgenerator->create_instance(array_merge(['course' => $courseid], $properties)); + + if (isset($properties['timemodified'])) { + // The generator overrides the timemodified value to set it as + // the current time even if a value is provided so we need to + // make sure it's set back to the requested value. + $quiz->timemodified = $properties['timemodified']; + $DB->update_record('quiz', $quiz); + } + + return $quiz; + } + + /** + * Create a calendar event for a quiz activity instance. + * + * @param stdClass $quiz The activity instance + * @param array $eventproperties Properties to set on the calendar event + * @return calendar_event + */ + protected function create_quiz_calendar_event(\stdClass $quiz, array $eventproperties) { + $defaultproperties = [ + 'name' => 'Test event', + 'description' => '', + 'format' => 1, + 'courseid' => $quiz->course, + 'groupid' => 0, + 'userid' => 2, + 'modulename' => 'quiz', + 'instance' => $quiz->id, + 'eventtype' => QUIZ_EVENT_TYPE_OPEN, + 'timestart' => time(), + 'timeduration' => 86400, + 'visible' => 1 + ]; + + return new \calendar_event(array_merge($defaultproperties, $eventproperties)); + } + + /** + * You can't create a quiz module event when the module doesn't exist. + */ + public function test_mod_quiz_core_calendar_validate_event_timestart_no_activity() { + global $CFG; + + $this->resetAfterTest(true); + $this->setAdminUser(); + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + + $event = new \calendar_event([ + 'name' => 'Test event', + 'description' => '', + 'format' => 1, + 'courseid' => $course->id, + 'groupid' => 0, + 'userid' => 2, + 'modulename' => 'quiz', + 'instance' => 1234, + 'eventtype' => QUIZ_EVENT_TYPE_OPEN, + 'timestart' => time(), + 'timeduration' => 86400, + 'visible' => 1 + ]); + + $this->expectException('moodle_exception'); + mod_quiz_core_calendar_validate_event_timestart($event); + } + + /** + * A QUIZ_EVENT_TYPE_OPEN must be before the close time of the quiz activity. + */ + public function test_mod_quiz_core_calendar_validate_event_timestart_valid_open_event() { + global $DB; + + $this->resetAfterTest(true); + $this->setAdminUser(); + $timeopen = time(); + $timeclose = $timeopen + DAYSECS; + $quiz = $this->create_quiz_instance(['timeopen' => $timeopen, 'timeclose' => $timeclose]); + $event = $this->create_quiz_calendar_event($quiz, [ + 'eventtype' => QUIZ_EVENT_TYPE_OPEN, + 'timestart' => $timeopen + ]); + + mod_quiz_core_calendar_validate_event_timestart($event); + // The function above will throw an exception if the event is + // invalid. + $this->assertTrue(true); + } + + /** + * A QUIZ_EVENT_TYPE_OPEN can not have a start time set after the close time + * of the quiz activity. + */ + public function test_mod_quiz_core_calendar_validate_event_timestart_invalid_open_event() { + global $DB; + + $this->resetAfterTest(true); + $this->setAdminUser(); + $timeopen = time(); + $timeclose = $timeopen + DAYSECS; + $quiz = $this->create_quiz_instance(['timeopen' => $timeopen, 'timeclose' => $timeclose]); + $event = $this->create_quiz_calendar_event($quiz, [ + 'eventtype' => QUIZ_EVENT_TYPE_OPEN, + 'timestart' => $timeclose + 1 + ]); + + $this->expectException('moodle_exception'); + mod_quiz_core_calendar_validate_event_timestart($event); + } + + /** + * A QUIZ_EVENT_TYPE_CLOSE must be after the open time of the quiz activity. + */ + public function test_mod_quiz_core_calendar_validate_event_timestart_valid_close_event() { + global $DB; + + $this->resetAfterTest(true); + $this->setAdminUser(); + $timeopen = time(); + $timeclose = $timeopen + DAYSECS; + $quiz = $this->create_quiz_instance(['timeopen' => $timeopen, 'timeclose' => $timeclose]); + $event = $this->create_quiz_calendar_event($quiz, [ + 'eventtype' => QUIZ_EVENT_TYPE_OPEN, + 'timestart' => $timeclose + ]); + + mod_quiz_core_calendar_validate_event_timestart($event); + // The function above will throw an exception if the event isn't + // valid. + $this->assertTrue(true); + } + + /** + * A QUIZ_EVENT_TYPE_CLOSE can not have a start time set before the open time + * of the quiz activity. + */ + public function test_mod_quiz_core_calendar_validate_event_timestart_invalid_close_event() { + global $DB; + + $this->resetAfterTest(true); + $this->setAdminUser(); + $timeopen = time(); + $timeclose = $timeopen + DAYSECS; + $quiz = $this->create_quiz_instance(['timeopen' => $timeopen, 'timeclose' => $timeclose]); + $event = $this->create_quiz_calendar_event($quiz, [ + 'eventtype' => QUIZ_EVENT_TYPE_CLOSE, + 'timestart' => $timeopen - 1 + ]); + + $this->expectException('moodle_exception'); + mod_quiz_core_calendar_validate_event_timestart($event); + } + + /** + * An unkown event type should not change the quiz instance. + */ + public function test_mod_quiz_core_calendar_event_timestart_updated_unknown_event() { + global $DB; + + $this->resetAfterTest(true); + $this->setAdminUser(); + $timeopen = time(); + $timeclose = $timeopen + DAYSECS; + $quiz = $this->create_quiz_instance(['timeopen' => $timeopen, 'timeclose' => $timeclose]); + $event = $this->create_quiz_calendar_event($quiz, [ + 'eventtype' => QUIZ_EVENT_TYPE_OPEN . "SOMETHING ELSE", + 'timestart' => 1 + ]); + + mod_quiz_core_calendar_event_timestart_updated($event); + + $quiz = $DB->get_record('quiz', ['id' => $quiz->id]); + $this->assertEquals($timeopen, $quiz->timeopen); + $this->assertEquals($timeclose, $quiz->timeclose); + } + + /** + * A QUIZ_EVENT_TYPE_OPEN event should update the timeopen property of + * the quiz activity. + */ + public function test_mod_quiz_core_calendar_event_timestart_updated_open_event() { + global $DB; + + $this->resetAfterTest(true); + $this->setAdminUser(); + $timeopen = time(); + $timeclose = $timeopen + DAYSECS; + $timemodified = 1; + $newtimeopen = $timeopen - DAYSECS; + $quiz = $this->create_quiz_instance([ + 'timeopen' => $timeopen, + 'timeclose' => $timeclose, + 'timemodified' => $timemodified + ]); + $event = $this->create_quiz_calendar_event($quiz, [ + 'eventtype' => QUIZ_EVENT_TYPE_OPEN, + 'timestart' => $newtimeopen + ]); + + mod_quiz_core_calendar_event_timestart_updated($event); + + $quiz = $DB->get_record('quiz', ['id' => $quiz->id]); + // Ensure the timeopen property matches the event timestart. + $this->assertEquals($newtimeopen, $quiz->timeopen); + // Ensure the timeclose isn't changed. + $this->assertEquals($timeclose, $quiz->timeclose); + // Ensure the timemodified property has been changed. + $this->assertNotEquals($timemodified, $quiz->timemodified); + } + + /** + * A QUIZ_EVENT_TYPE_CLOSE event should update the timeclose property of + * the quiz activity. + */ + public function test_mod_quiz_core_calendar_event_timestart_updated_close_event() { + global $DB; + + $this->resetAfterTest(true); + $this->setAdminUser(); + $timeopen = time(); + $timeclose = $timeopen + DAYSECS; + $timemodified = 1; + $newtimeclose = $timeclose + DAYSECS; + $quiz = $this->create_quiz_instance([ + 'timeopen' => $timeopen, + 'timeclose' => $timeclose, + 'timemodified' => $timemodified + ]); + $event = $this->create_quiz_calendar_event($quiz, [ + 'eventtype' => QUIZ_EVENT_TYPE_CLOSE, + 'timestart' => $newtimeclose + ]); + + mod_quiz_core_calendar_event_timestart_updated($event); + + $quiz = $DB->get_record('quiz', ['id' => $quiz->id]); + // Ensure the timeclose property matches the event timestart. + $this->assertEquals($newtimeclose, $quiz->timeclose); + // Ensure the timeopen isn't changed. + $this->assertEquals($timeopen, $quiz->timeopen); + // Ensure the timemodified property has been changed. + $this->assertNotEquals($timemodified, $quiz->timemodified); + } + + /** + * A QUIZ_EVENT_TYPE_OPEN event should not update the timeopen property of + * the quiz activity if it's an override. + */ + public function test_mod_quiz_core_calendar_event_timestart_updated_open_event_override() { + global $DB; + + $this->resetAfterTest(true); + $this->setAdminUser(); + $user = $this->getDataGenerator()->create_user(); + $timeopen = time(); + $timeclose = $timeopen + DAYSECS; + $timemodified = 1; + $newtimeopen = $timeopen - DAYSECS; + $quiz = $this->create_quiz_instance([ + 'timeopen' => $timeopen, + 'timeclose' => $timeclose, + 'timemodified' => $timemodified + ]); + $event = $this->create_quiz_calendar_event($quiz, [ + 'userid' => $user->id, + 'eventtype' => QUIZ_EVENT_TYPE_OPEN, + 'timestart' => $newtimeopen + ]); + $record = (object) [ + 'quiz' => $quiz->id, + 'userid' => $user->id + ]; + + $DB->insert_record('quiz_overrides', $record); + + mod_quiz_core_calendar_event_timestart_updated($event); + + $quiz = $DB->get_record('quiz', ['id' => $quiz->id]); + // Ensure the timeopen property doesn't change. + $this->assertEquals($timeopen, $quiz->timeopen); + // Ensure the timeclose isn't changed. + $this->assertEquals($timeclose, $quiz->timeclose); + // Ensure the timemodified property has not been changed. + $this->assertEquals($timemodified, $quiz->timemodified); + } + + /** + * If a student somehow finds a way to update the quiz calendar event + * then the callback should not update the quiz activity otherwise that + * would be a security issue. + */ + public function test_student_role_cant_update_quiz_activity() { + global $DB; + + $this->resetAfterTest(); + $this->setAdminUser(); + + $generator = $this->getDataGenerator(); + $user = $generator->create_user(); + $course = $generator->create_course(); + $context = context_course::instance($course->id); + $roleid = $generator->create_role(); + $now = time(); + $timeopen = (new DateTime())->setTimestamp($now); + $newtimeopen = (new DateTime())->setTimestamp($now)->modify('+1 day'); + $quiz = $this->create_quiz_instance([ + 'course' => $course->id, + 'timeopen' => $timeopen->getTimestamp() + ]); + + $generator->enrol_user($user->id, $course->id, 'student'); + $generator->role_assign($roleid, $user->id, $context->id); + + $event = $this->create_quiz_calendar_event($quiz, [ + 'eventtype' => QUIZ_EVENT_TYPE_OPEN, + 'timestart' => $timeopen->getTimestamp() + ]); + + assign_capability('moodle/course:manageactivities', CAP_PROHIBIT, $roleid, $context, true); + + $this->setUser($user); + + mod_quiz_core_calendar_event_timestart_updated($event); + + $newquiz = $DB->get_record('quiz', ['id' => $quiz->id]); + // The time open shouldn't have changed even though we updated the calendar + // event. + $this->assertEquals($timeopen->getTimestamp(), $newquiz->timeopen); + } + + /** + * A teacher with the capability to modify a quiz module should be + * able to update the quiz activity dates by changing the calendar + * event. + */ + public function test_teacher_role_can_update_quiz_activity() { + global $DB; + + $this->resetAfterTest(); + $this->setAdminUser(); + + $generator = $this->getDataGenerator(); + $user = $generator->create_user(); + $course = $generator->create_course(); + $context = context_course::instance($course->id); + $roleid = $generator->create_role(); + $now = time(); + $timeopen = (new DateTime())->setTimestamp($now); + $newtimeopen = (new DateTime())->setTimestamp($now)->modify('+1 day'); + $quiz = $this->create_quiz_instance([ + 'course' => $course->id, + 'timeopen' => $timeopen->getTimestamp() + ]); + + $generator->enrol_user($user->id, $course->id, 'teacher'); + $generator->role_assign($roleid, $user->id, $context->id); + + $event = $this->create_quiz_calendar_event($quiz, [ + 'eventtype' => QUIZ_EVENT_TYPE_OPEN, + 'timestart' => $newtimeopen->getTimestamp() + ]); + + assign_capability('moodle/course:manageactivities', CAP_ALLOW, $roleid, $context, true); + + $this->setUser($user); + + // Trigger and capture the event. + $sink = $this->redirectEvents(); + + mod_quiz_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'); + }); + + $newquiz = $DB->get_record('quiz', ['id' => $quiz->id]); + // The should be updated along with the event because the user has sufficient + // capabilities. + $this->assertEquals($newtimeopen->getTimestamp(), $newquiz->timeopen); + // 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_quiz_core_calendar_get_valid_event_timestart_range_unknown_event() { + global $DB; + + $this->resetAfterTest(true); + $this->setAdminUser(); + $timeopen = time(); + $timeclose = $timeopen + DAYSECS; + $quiz = $this->create_quiz_instance([ + 'timeopen' => $timeopen, + 'timeclose' => $timeclose + ]); + $event = $this->create_quiz_calendar_event($quiz, [ + 'eventtype' => QUIZ_EVENT_TYPE_OPEN . "SOMETHING ELSE", + 'timestart' => 1 + ]); + + list ($min, $max) = mod_quiz_core_calendar_get_valid_event_timestart_range($event, $quiz); + $this->assertNull($min); + $this->assertNull($max); + } + + /** + * The open event should be limited by the quiz's timeclose property, if it's set. + */ + public function test_mod_quiz_core_calendar_get_valid_event_timestart_range_open_event() { + global $DB; + + $this->resetAfterTest(true); + $this->setAdminUser(); + $timeopen = time(); + $timeclose = $timeopen + DAYSECS; + $quiz = $this->create_quiz_instance([ + 'timeopen' => $timeopen, + 'timeclose' => $timeclose + ]); + $event = $this->create_quiz_calendar_event($quiz, [ + 'eventtype' => QUIZ_EVENT_TYPE_OPEN, + 'timestart' => 1 + ]); + + // The max limit should be bounded by the timeclose value. + list ($min, $max) = mod_quiz_core_calendar_get_valid_event_timestart_range($event, $quiz); + + $this->assertNull($min); + $this->assertEquals($timeclose, $max[0]); + + // No timeclose value should result in no upper limit. + $quiz->timeclose = 0; + list ($min, $max) = mod_quiz_core_calendar_get_valid_event_timestart_range($event, $quiz); + + $this->assertNull($min); + $this->assertNull($max); + } + + /** + * An override event should not have any limits. + */ + public function test_mod_quiz_core_calendar_get_valid_event_timestart_range_override_event() { + global $DB; + + $this->resetAfterTest(true); + $this->setAdminUser(); + $generator = $this->getDataGenerator(); + $user = $generator->create_user(); + $course = $generator->create_course(); + $timeopen = time(); + $timeclose = $timeopen + DAYSECS; + $quiz = $this->create_quiz_instance([ + 'course' => $course->id, + 'timeopen' => $timeopen, + 'timeclose' => $timeclose + ]); + $event = $this->create_quiz_calendar_event($quiz, [ + 'userid' => $user->id, + 'eventtype' => QUIZ_EVENT_TYPE_OPEN, + 'timestart' => 1 + ]); + $record = (object) [ + 'quiz' => $quiz->id, + 'userid' => $user->id + ]; + + $DB->insert_record('quiz_overrides', $record); + + list ($min, $max) = mod_quiz_core_calendar_get_valid_event_timestart_range($event, $quiz); + + $this->assertNull($min); + $this->assertNull($max); + } + + /** + * The close event should be limited by the quiz's timeopen property, if it's set. + */ + public function test_mod_quiz_core_calendar_get_valid_event_timestart_range_close_event() { + global $DB; + + $this->resetAfterTest(true); + $this->setAdminUser(); + $timeopen = time(); + $timeclose = $timeopen + DAYSECS; + $quiz = $this->create_quiz_instance([ + 'timeopen' => $timeopen, + 'timeclose' => $timeclose + ]); + $event = $this->create_quiz_calendar_event($quiz, [ + 'eventtype' => QUIZ_EVENT_TYPE_CLOSE, + 'timestart' => 1, + ]); + + // The max limit should be bounded by the timeclose value. + list ($min, $max) = mod_quiz_core_calendar_get_valid_event_timestart_range($event, $quiz); + + $this->assertEquals($timeopen, $min[0]); + $this->assertNull($max); + + // No timeclose value should result in no upper limit. + $quiz->timeopen = 0; + list ($min, $max) = mod_quiz_core_calendar_get_valid_event_timestart_range($event, $quiz); + + $this->assertNull($min); + $this->assertNull($max); + } + + /** + * When the close date event is changed and it results in the time close value of + * the quiz being updated then the open quiz attempts should also be updated. + */ + public function test_core_calendar_event_timestart_updated_update_quiz_attempt() { + global $DB; + + $this->resetAfterTest(); + $this->setAdminUser(); + + $generator = $this->getDataGenerator(); + $teacher = $generator->create_user(); + $student = $generator->create_user(); + $course = $generator->create_course(); + $context = context_course::instance($course->id); + $roleid = $generator->create_role(); + $now = time(); + $timelimit = 600; + $timeopen = (new DateTime())->setTimestamp($now); + $timeclose = (new DateTime())->setTimestamp($now)->modify('+1 day'); + // The new close time being earlier than the time open + time limit should + // result in an update to the quiz attempts. + $newtimeclose = $timeopen->getTimestamp() + $timelimit - 10; + $quiz = $this->create_quiz_instance([ + 'course' => $course->id, + 'timeopen' => $timeopen->getTimestamp(), + 'timeclose' => $timeclose->getTimestamp(), + 'timelimit' => $timelimit + ]); + + $generator->enrol_user($student->id, $course->id, 'student'); + $generator->enrol_user($teacher->id, $course->id, 'teacher'); + $generator->role_assign($roleid, $teacher->id, $context->id); + + $event = $this->create_quiz_calendar_event($quiz, [ + 'eventtype' => QUIZ_EVENT_TYPE_CLOSE, + 'timestart' => $newtimeclose + ]); + + assign_capability('moodle/course:manageactivities', CAP_ALLOW, $roleid, $context, true); + + $attemptid = $DB->insert_record( + 'quiz_attempts', + [ + 'quiz' => $quiz->id, + 'userid' => $student->id, + 'state' => 'inprogress', + 'timestart' => $timeopen->getTimestamp(), + 'timecheckstate' => 0, + 'layout' => '', + 'uniqueid' => 1 + ] + ); + + $this->setUser($teacher); + + mod_quiz_core_calendar_event_timestart_updated($event); + + $quiz = $DB->get_record('quiz', ['id' => $quiz->id]); + $attempt = $DB->get_record('quiz_attempts', ['id' => $attemptid]); + // When the close date is changed so that it's earlier than the time open + // plus the time limit of the quiz then the attempt's timecheckstate should + // be updated to the new time close date of the quiz. + $this->assertEquals($newtimeclose, $attempt->timecheckstate); + $this->assertEquals($newtimeclose, $quiz->timeclose); + } +} diff --git a/mod/quiz/tests/locallib_test.php b/mod/quiz/tests/locallib_test.php index 8fa40e6c516..081c7577867 100644 --- a/mod/quiz/tests/locallib_test.php +++ b/mod/quiz/tests/locallib_test.php @@ -152,4 +152,148 @@ class mod_quiz_locallib_testcase extends advanced_testcase { $completiondata = $completion->get_data($cm); $this->assertEquals(1, $completiondata->completionstate); } + + /** + * Return false when there are not overrides for this quiz instance. + */ + public function test_quiz_is_overriden_calendar_event_no_override() { + global $CFG, $DB; + + $this->resetAfterTest(); + $this->setAdminUser(); + + $generator = $this->getDataGenerator(); + $user = $generator->create_user(); + $course = $generator->create_course(); + $quizgenerator = $generator->get_plugin_generator('mod_quiz'); + $quiz = $quizgenerator->create_instance(['course' => $course->id]); + + $event = new \calendar_event((object)[ + 'modulename' => 'quiz', + 'instance' => $quiz->id, + 'userid' => $user->id + ]); + + $this->assertFalse(quiz_is_overriden_calendar_event($event)); + } + + /** + * Return false if the given event isn't an quiz module event. + */ + public function test_quiz_is_overriden_calendar_event_no_module_event() { + global $CFG, $DB; + + $this->resetAfterTest(); + $this->setAdminUser(); + + $generator = $this->getDataGenerator(); + $user = $generator->create_user(); + $course = $generator->create_course(); + $quizgenerator = $generator->get_plugin_generator('mod_quiz'); + $quiz = $quizgenerator->create_instance(['course' => $course->id]); + + $event = new \calendar_event((object)[ + 'userid' => $user->id + ]); + + $this->assertFalse(quiz_is_overriden_calendar_event($event)); + } + + /** + * Return false if there is overrides for this use but they belong to another quiz + * instance. + */ + public function test_quiz_is_overriden_calendar_event_different_quiz_instance() { + global $CFG, $DB; + + $this->resetAfterTest(); + $this->setAdminUser(); + + $generator = $this->getDataGenerator(); + $user = $generator->create_user(); + $course = $generator->create_course(); + $quizgenerator = $generator->get_plugin_generator('mod_quiz'); + $quiz = $quizgenerator->create_instance(['course' => $course->id]); + $quiz2 = $quizgenerator->create_instance(['course' => $course->id]); + + $event = new \calendar_event((object) [ + 'modulename' => 'quiz', + 'instance' => $quiz->id, + 'userid' => $user->id + ]); + + $record = (object) [ + 'quiz' => $quiz2->id, + 'userid' => $user->id + ]; + + $DB->insert_record('quiz_overrides', $record); + + $this->assertFalse(quiz_is_overriden_calendar_event($event)); + } + + /** + * Return true if there is a user override for this event and quiz instance. + */ + public function test_quiz_is_overriden_calendar_event_user_override() { + global $CFG, $DB; + + $this->resetAfterTest(); + $this->setAdminUser(); + + $generator = $this->getDataGenerator(); + $user = $generator->create_user(); + $course = $generator->create_course(); + $quizgenerator = $generator->get_plugin_generator('mod_quiz'); + $quiz = $quizgenerator->create_instance(['course' => $course->id]); + + $event = new \calendar_event((object) [ + 'modulename' => 'quiz', + 'instance' => $quiz->id, + 'userid' => $user->id + ]); + + $record = (object) [ + 'quiz' => $quiz->id, + 'userid' => $user->id + ]; + + $DB->insert_record('quiz_overrides', $record); + + $this->assertTrue(quiz_is_overriden_calendar_event($event)); + } + + /** + * Return true if there is a group override for the event and quiz instance. + */ + public function test_quiz_is_overriden_calendar_event_group_override() { + global $CFG, $DB; + + $this->resetAfterTest(); + $this->setAdminUser(); + + $generator = $this->getDataGenerator(); + $user = $generator->create_user(); + $course = $generator->create_course(); + $quizgenerator = $generator->get_plugin_generator('mod_quiz'); + $quiz = $quizgenerator->create_instance(['course' => $course->id]); + $group = $this->getDataGenerator()->create_group(array('courseid' => $quiz->course)); + $groupid = $group->id; + $userid = $user->id; + + $event = new \calendar_event((object) [ + 'modulename' => 'quiz', + 'instance' => $quiz->id, + 'groupid' => $groupid + ]); + + $record = (object) [ + 'quiz' => $quiz->id, + 'groupid' => $groupid + ]; + + $DB->insert_record('quiz_overrides', $record); + + $this->assertTrue(quiz_is_overriden_calendar_event($event)); + } } -- 2.17.1