diff --git a/mod/quiz/attemptlib.php b/mod/quiz/attemptlib.php index d20cf0d07f9..94eba2db16c 100644 --- a/mod/quiz/attemptlib.php +++ b/mod/quiz/attemptlib.php @@ -2135,6 +2135,7 @@ class quiz_attempt { $this->quba->set_max_mark($newslot, 0); $this->quba->set_question_attempt_metadata($newslot, 'originalslot', $slot); question_engine::save_questions_usage_by_activity($this->quba); + $this->fire_attempt_question_restarted_event($slot, $newquestion->id); $transaction->allow_commit(); } @@ -2152,6 +2153,7 @@ class quiz_attempt { $this->quba->process_all_autosaves($timestamp); question_engine::save_questions_usage_by_activity($this->quba); + $this->fire_attempt_autosaved_event(); $transaction->allow_commit(); } @@ -2416,14 +2418,12 @@ class quiz_attempt { } } - // Don't log - we will end with a redirect to a page that is logged. - if (!$finishattempt) { // Just process the responses for this page and go to the next page. if (!$toolate) { try { $this->process_submitted_actions($timenow, $becomingoverdue); - + $this->fire_attempt_updated_event(); } catch (question_out_of_sequence_exception $e) { throw new moodle_exception('submissionoutofsequencefriendlymessage', 'question', $this->attempt_url(null, $thispage)); @@ -2531,7 +2531,8 @@ class quiz_attempt { 'courseid' => $this->get_courseid(), 'context' => context_module::instance($this->get_cmid()), 'other' => array( - 'quizid' => $this->get_quizid() + 'quizid' => $this->get_quizid(), + 'page' => $this->get_currentpage() ) ); $event = \mod_quiz\event\attempt_viewed::create($params); @@ -2539,6 +2540,73 @@ class quiz_attempt { $event->trigger(); } + /** + * Trigger the attempt_updated event. + * + * @return void + */ + public function fire_attempt_updated_event(): void { + $params = [ + 'objectid' => $this->get_attemptid(), + 'relateduserid' => $this->get_userid(), + 'courseid' => $this->get_courseid(), + 'context' => context_module::instance($this->get_cmid()), + 'other' => [ + 'quizid' => $this->get_quizid(), + 'page' => $this->get_currentpage() + ] + ]; + $event = \mod_quiz\event\attempt_updated::create($params); + $event->add_record_snapshot('quiz_attempts', $this->get_attempt()); + $event->trigger(); + } + + /** + * Trigger the attempt_autosaved event. + * + * @return void + */ + public function fire_attempt_autosaved_event(): void { + $params = [ + 'objectid' => $this->get_attemptid(), + 'relateduserid' => $this->get_userid(), + 'courseid' => $this->get_courseid(), + 'context' => context_module::instance($this->get_cmid()), + 'other' => [ + 'quizid' => $this->get_quizid(), + 'page' => $this->get_currentpage() + ] + ]; + $event = \mod_quiz\event\attempt_autosaved::create($params); + $event->add_record_snapshot('quiz_attempts', $this->get_attempt()); + $event->trigger(); + } + + /** + * Trigger the attempt_question_restarted event. + * + * @param int $slot Slot number + * @param int $newquestionid New question id. + * @return void + */ + public function fire_attempt_question_restarted_event(int $slot, int $newquestionid): void { + $params = [ + 'objectid' => $this->get_attemptid(), + 'relateduserid' => $this->get_userid(), + 'courseid' => $this->get_courseid(), + 'context' => context_module::instance($this->get_cmid()), + 'other' => [ + 'quizid' => $this->get_quizid(), + 'page' => $this->get_currentpage(), + 'slot' => $slot, + 'newquestionid' => $newquestionid + ] + ]; + $event = \mod_quiz\event\attempt_question_restarted::create($params); + $event->add_record_snapshot('quiz_attempts', $this->get_attempt()); + $event->trigger(); + } + /** * Trigger the attempt_summary_viewed event. * diff --git a/mod/quiz/classes/event/attempt_autosaved.php b/mod/quiz/classes/event/attempt_autosaved.php new file mode 100644 index 00000000000..87833b5bfc1 --- /dev/null +++ b/mod/quiz/classes/event/attempt_autosaved.php @@ -0,0 +1,131 @@ +. + +/** + * The mod_quiz attempt auto-saved event. + * + * @package mod_quiz + * @copyright 2021 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_quiz\event; + +/** + * The mod_quiz attempt auto-saved event class. + * + * @property-read array $other { + * Extra information about event. + * + * - int quizid: the id of the quiz. + * - int page: the page number of attempt. + * } + * + * @package mod_quiz + * @copyright 2021 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class attempt_autosaved extends \core\event\base { + + /** + * Init method. + */ + protected function init() { + $this->data['objecttable'] = 'quiz_attempts'; + $this->data['crud'] = 'u'; + $this->data['edulevel'] = self::LEVEL_PARTICIPATING; + } + + /** + * Returns localised general event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventattemptautosaved', 'mod_quiz'); + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + $pagenumber = $this->other['page'] + 1; + + return "The user with id '$this->userid' is working on page " . + "'{$pagenumber}' of the attempt " . + "with id '$this->objectid' for the quiz with course module id '$this->contextinstanceid', " . + "and their latest responses have been saved automatically."; + } + + /** + * Returns relevant URL. + * + * @return \moodle_url + */ + public function get_url() { + return new \moodle_url('/mod/quiz/review.php', [ + 'attempt' => $this->objectid, + 'page' => $this->other['page'] + ]); + } + + /** + * Custom validation. + * + * @throws \coding_exception + * @return void + */ + protected function validate_data() { + parent::validate_data(); + + if (!isset($this->relateduserid)) { + throw new \coding_exception('The \'relateduserid\' must be set.'); + } + + if (!isset($this->other['quizid'])) { + throw new \coding_exception('The \'quizid\' value must be set in other.'); + } + + if (!isset($this->other['page'])) { + throw new \coding_exception('The \'page\' value must be set in other.'); + } + } + + /** + * This is used when restoring course logs where it is required that we + * map the information in 'other' to it's new value in the new course. + * + * @return array List of mapping of other ids. + */ + public static function get_objectid_mapping() { + return ['db' => 'quiz_attempts', 'restore' => 'quiz_attempt']; + } + + /** + * This is used when restoring course logs where it is required that we + * map the information in 'other' to it's new value in the new course. + * + * @return array List of mapping of other ids. + */ + public static function get_other_mapping() { + $othermapped = []; + $othermapped['quizid'] = ['db' => 'quiz', 'restore' => 'quiz']; + + return $othermapped; + } +} diff --git a/mod/quiz/classes/event/attempt_question_restarted.php b/mod/quiz/classes/event/attempt_question_restarted.php new file mode 100644 index 00000000000..412df7e5d25 --- /dev/null +++ b/mod/quiz/classes/event/attempt_question_restarted.php @@ -0,0 +1,140 @@ +. + +/** + * The mod_quiz attempt question restarted event. + * + * @package mod_quiz + * @copyright 2021 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_quiz\event; + +/** + * The mod_quiz attempt question restarted event class. + * + * @property-read array $other { + * Extra information about event. + * + * - int quizid: the id of the quiz. + * - int page: the page number of attempt. + * } + * + * @package mod_quiz + * @copyright 2021 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class attempt_question_restarted extends \core\event\base { + + /** + * Init method. + */ + protected function init() { + $this->data['objecttable'] = 'quiz_attempts'; + $this->data['crud'] = 'u'; + $this->data['edulevel'] = self::LEVEL_PARTICIPATING; + } + + /** + * Returns localised general event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventattemptquestionrestarted', 'mod_quiz'); + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + $pagenumber = $this->other['page'] + 1; + + return "The user with id '$this->userid' has restarted question at slot '{$this->other['slot']}' on page " . + "'{$pagenumber}' of the attempt with id '$this->objectid' belonging to the user " . + "with id '$this->relateduserid' for the quiz with course module id '$this->contextinstanceid', " . + "and the new question id is '{$this->other['newquestionid']}'."; + } + + /** + * Returns relevant URL. + * + * @return \moodle_url + */ + public function get_url() { + return new \moodle_url('/mod/quiz/review.php', [ + 'attempt' => $this->objectid, + 'page' => $this->other['page'] + ]); + } + + /** + * Custom validation. + * + * @throws \coding_exception + * @return void + */ + protected function validate_data() { + parent::validate_data(); + + if (!isset($this->relateduserid)) { + throw new \coding_exception('The \'relateduserid\' must be set.'); + } + + if (!isset($this->other['quizid'])) { + throw new \coding_exception('The \'quizid\' value must be set in other.'); + } + + if (!isset($this->other['page'])) { + throw new \coding_exception('The \'page\' value must be set in other.'); + } + + if (!isset($this->other['slot'])) { + throw new \coding_exception('The \'slot\' value must be set in other.'); + } + + if (!isset($this->other['newquestionid'])) { + throw new \coding_exception('The \'newquestionid\' value must be set in other.'); + } + } + + /** + * This is used when restoring course logs where it is required that we + * map the information in 'other' to it's new value in the new course. + * + * @return array List of mapping of other ids. + */ + public static function get_objectid_mapping() { + return ['db' => 'quiz_attempts', 'restore' => 'quiz_attempt']; + } + + /** + * This is used when restoring course logs where it is required that we + * map the information in 'other' to it's new value in the new course. + * + * @return array List of mapping of other ids. + */ + public static function get_other_mapping() { + $othermapped = []; + $othermapped['quizid'] = ['db' => 'quiz', 'restore' => 'quiz']; + $othermapped['newquestionid'] = ['db' => 'question', 'restore' => 'question']; + + return $othermapped; + } +} diff --git a/mod/quiz/classes/event/attempt_updated.php b/mod/quiz/classes/event/attempt_updated.php new file mode 100644 index 00000000000..fdd0b0f9c43 --- /dev/null +++ b/mod/quiz/classes/event/attempt_updated.php @@ -0,0 +1,130 @@ +. + +/** + * The mod_quiz attempt updated event. + * + * @package mod_quiz + * @copyright 2021 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_quiz\event; + +/** + * The mod_quiz attempt updated event class. + * + * @property-read array $other { + * Extra information about event. + * + * - int quizid: the id of the quiz. + * - int page: the page number of attempt. + * } + * + * @package mod_quiz + * @copyright 2021 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class attempt_updated extends \core\event\base { + + /** + * Init method. + */ + protected function init() { + $this->data['objecttable'] = 'quiz_attempts'; + $this->data['crud'] = 'u'; + $this->data['edulevel'] = self::LEVEL_PARTICIPATING; + } + + /** + * Returns localised general event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventattemptupdated', 'mod_quiz'); + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + $pagenumber = $this->other['page'] + 1; + + return "The user with id '$this->userid' has updated responses on page '{$pagenumber}' of the attempt " . + "with id '$this->objectid' belonging to the user " . + "with id '$this->relateduserid' for the quiz with course module id '$this->contextinstanceid'."; + } + + /** + * Returns relevant URL. + * + * @return \moodle_url + */ + public function get_url() { + return new \moodle_url('/mod/quiz/review.php', [ + 'attempt' => $this->objectid, + 'page' => $this->other['page'] + ]); + } + + /** + * Custom validation. + * + * @throws \coding_exception + * @return void + */ + protected function validate_data() { + parent::validate_data(); + + if (!isset($this->relateduserid)) { + throw new \coding_exception('The \'relateduserid\' must be set.'); + } + + if (!isset($this->other['quizid'])) { + throw new \coding_exception('The \'quizid\' value must be set in other.'); + } + + if (!isset($this->other['page'])) { + throw new \coding_exception('The \'page\' value must be set in other.'); + } + } + + /** + * This is used when restoring course logs where it is required that we + * map the objectid to it's new value in the new course. + * + * @return array Mapping of object id. + */ + public static function get_objectid_mapping() { + return ['db' => 'quiz_attempts', 'restore' => 'quiz_attempt']; + } + + /** + * This is used when restoring course logs where it is required that we + * map the information in 'other' to it's new value in the new course. + * + * @return array List of mapping of other ids. + */ + public static function get_other_mapping() { + $othermapped = []; + $othermapped['quizid'] = ['db' => 'quiz', 'restore' => 'quiz']; + + return $othermapped; + } +} diff --git a/mod/quiz/classes/event/attempt_viewed.php b/mod/quiz/classes/event/attempt_viewed.php index f08739936c3..4771e5049b9 100644 --- a/mod/quiz/classes/event/attempt_viewed.php +++ b/mod/quiz/classes/event/attempt_viewed.php @@ -33,6 +33,7 @@ defined('MOODLE_INTERNAL') || die(); * Extra information about event. * * - int quizid: the id of the quiz. + * - int page: the page number of attempt. * } * * @package mod_quiz @@ -66,8 +67,10 @@ class attempt_viewed extends \core\event\base { * @return string */ public function get_description() { - return "The user with id '$this->userid' has viewed the attempt with id '$this->objectid' belonging to the user " . - "with id '$this->relateduserid' for the quiz with course module id '$this->contextinstanceid'."; + $page = isset($this->other['page']) ? $this->other['page'] + 1 : ''; + return "The user with id '$this->userid' has viewed page '$page' of the attempt with id " . + "'$this->objectid' belonging to the user with id '$this->relateduserid' for the quiz " . + "with course module id '$this->contextinstanceid'."; } /** @@ -76,7 +79,10 @@ class attempt_viewed extends \core\event\base { * @return \moodle_url */ public function get_url() { - return new \moodle_url('/mod/quiz/review.php', array('attempt' => $this->objectid)); + return new \moodle_url('/mod/quiz/review.php', [ + 'attempt' => $this->objectid, + 'page' => isset($this->other['page']) ? $this->other['page'] : 0 + ]); } /** @@ -105,6 +111,10 @@ class attempt_viewed extends \core\event\base { if (!isset($this->other['quizid'])) { throw new \coding_exception('The \'quizid\' value must be set in other.'); } + + if (!isset($this->other['page'])) { + throw new \coding_exception('The \'page\' value must be set in other.'); + } } public static function get_objectid_mapping() { diff --git a/mod/quiz/lang/en/quiz.php b/mod/quiz/lang/en/quiz.php index f5353f18f32..9ca776ac3dd 100644 --- a/mod/quiz/lang/en/quiz.php +++ b/mod/quiz/lang/en/quiz.php @@ -338,10 +338,13 @@ $string['errornotnumbers'] = 'Error - answers must be numeric'; $string['errorunexpectedevent'] = 'Unexpected event code {$a->event} found for question {$a->questionid} in attempt {$a->attemptid}.'; $string['essay'] = 'Essay'; $string['essayquestions'] = 'Questions'; +$string['eventattemptautosaved'] = 'Quiz attempt auto-saved'; $string['eventattemptdeleted'] = 'Quiz attempt deleted'; $string['eventattemptpreviewstarted'] = 'Quiz attempt preview started'; +$string['eventattemptquestionrestarted'] = 'Quiz attempt question restarted'; $string['eventattemptreviewed'] = 'Quiz attempt reviewed'; $string['eventattemptsummaryviewed'] = 'Quiz attempt summary viewed'; +$string['eventattemptupdated'] = 'Quiz attempt updated'; $string['eventattemptviewed'] = 'Quiz attempt viewed'; $string['eventeditpageviewed'] = 'Quiz edit page viewed'; $string['eventoverridecreated'] = 'Quiz override created'; diff --git a/mod/quiz/tests/events_test.php b/mod/quiz/tests/events_test.php index 74946552bf0..22a1556e0d0 100644 --- a/mod/quiz/tests/events_test.php +++ b/mod/quiz/tests/events_test.php @@ -255,6 +255,108 @@ class mod_quiz_events_testcase extends advanced_testcase { $this->assertEventContextNotUsed($event); } + /** + * Test the attempt question restarted event. + * + * There is no external API for replacing a question, so the unit test will simply + * create and trigger the event and ensure the event data is returned as expected. + */ + public function test_attempt_question_restarted() { + list($quizobj, $quba, $attempt) = $this->prepare_quiz_data(); + + $params = [ + 'objectid' => 1, + 'relateduserid' => 2, + 'courseid' => $quizobj->get_courseid(), + 'context' => context_module::instance($quizobj->get_cmid()), + 'other' => [ + 'quizid' => $quizobj->get_quizid(), + 'page' => 2, + 'slot' => 3, + 'newquestionid' => 2 + ] + ]; + $event = \mod_quiz\event\attempt_question_restarted::create($params); + + // Trigger and capture the event. + $sink = $this->redirectEvents(); + $event->trigger(); + $events = $sink->get_events(); + $event = reset($events); + + // Check that the event data is valid. + $this->assertInstanceOf('\mod_quiz\event\attempt_question_restarted', $event); + $this->assertEquals(context_module::instance($quizobj->get_cmid()), $event->get_context()); + $this->assertEventContextNotUsed($event); + } + + /** + * Test the attempt updated event. + * + * There is no external API for updating an attempt, so the unit test will simply + * create and trigger the event and ensure the event data is returned as expected. + */ + public function test_attempt_updated() { + list($quizobj, $quba, $attempt) = $this->prepare_quiz_data(); + + $params = [ + 'objectid' => 1, + 'relateduserid' => 2, + 'courseid' => $quizobj->get_courseid(), + 'context' => context_module::instance($quizobj->get_cmid()), + 'other' => [ + 'quizid' => $quizobj->get_quizid(), + 'page' => 0 + ] + ]; + $event = \mod_quiz\event\attempt_updated::create($params); + + // Trigger and capture the event. + $sink = $this->redirectEvents(); + $event->trigger(); + $events = $sink->get_events(); + $event = reset($events); + + // Check that the event data is valid. + $this->assertInstanceOf('\mod_quiz\event\attempt_updated', $event); + $this->assertEquals(context_module::instance($quizobj->get_cmid()), $event->get_context()); + $this->assertEventContextNotUsed($event); + } + + /** + * Test the attempt auto-saved event. + * + * There is no external API for auto-saving an attempt, so the unit test will simply + * create and trigger the event and ensure the event data is returned as expected. + */ + public function test_attempt_autosaved() { + list($quizobj, $quba, $attempt) = $this->prepare_quiz_data(); + + $params = [ + 'objectid' => 1, + 'relateduserid' => 2, + 'courseid' => $quizobj->get_courseid(), + 'context' => context_module::instance($quizobj->get_cmid()), + 'other' => [ + 'quizid' => $quizobj->get_quizid(), + 'page' => 0 + ] + ]; + + $event = \mod_quiz\event\attempt_autosaved::create($params); + + // Trigger and capture the event. + $sink = $this->redirectEvents(); + $event->trigger(); + $events = $sink->get_events(); + $event = reset($events); + + // Check that the event data is valid. + $this->assertInstanceOf('\mod_quiz\event\attempt_autosaved', $event); + $this->assertEquals(context_module::instance($quizobj->get_cmid()), $event->get_context()); + $this->assertEventContextNotUsed($event); + } + /** * Test the edit page viewed event. * @@ -667,7 +769,8 @@ class mod_quiz_events_testcase extends advanced_testcase { 'courseid' => $course->id, 'context' => context_module::instance($quiz->cmid), 'other' => array( - 'quizid' => $quiz->id + 'quizid' => $quiz->id, + 'page' => 0 ) ); $event = \mod_quiz\event\attempt_viewed::create($params);