diff --git a/public/mod/quiz/classes/event/slot_created.php b/public/mod/quiz/classes/event/slot_created.php index 5fb52b0f9cf..3b5731c32cb 100644 --- a/public/mod/quiz/classes/event/slot_created.php +++ b/public/mod/quiz/classes/event/slot_created.php @@ -24,6 +24,9 @@ namespace mod_quiz\event; +use core\exception\coding_exception; +use core\url; + /** * The mod_quiz slot created event class. * @@ -51,14 +54,30 @@ class slot_created extends \core\event\base { } public function get_description() { - return "The user with id '$this->userid' created a new slot with id '{$this->objectid}' " . - "and slot number '{$this->other['slotnumber']}' " . - "on page '{$this->other['page']}' " . - "of the quiz with course module id '$this->contextinstanceid'."; + + if (isset($this->other['questionbankentryid'])) { + $version = $this->other['version'] ?? 'Always latest'; + return "The user with id '$this->userid' created a new slot with " . + "id '{$this->objectid}', " . + "slot number '{$this->other['slotnumber']}', and " . + "question bank entry id '{$this->other['questionbankentryid']}' (version '$version') " . + "on page '{$this->other['page']}' " . + "of the quiz with course module id '$this->contextinstanceid'."; + } + + if (isset($this->other['questionscontextid'])) { + return "The user with id '$this->userid' created a new slot using question references with " . + "id '{$this->objectid}', " . + "slot number '{$this->other['slotnumber']}', " . + "question context '{$this->other['questionscontextid']}', and " . + "filter condition '{$this->other['filtercondition']}' " . + "on page '{$this->other['page']}' " . + "of the quiz with course module id '$this->contextinstanceid'."; + } } public function get_url() { - return new \moodle_url('/mod/quiz/edit.php', [ + return new url('/mod/quiz/edit.php', [ 'cmid' => $this->contextinstanceid ]); } @@ -66,24 +85,63 @@ class slot_created extends \core\event\base { protected function validate_data() { parent::validate_data(); + $errors = []; + if (!isset($this->objectid)) { - throw new \coding_exception('The \'objectid\' value must be set.'); + $errors[] = "The 'objectid' value must be set."; } if (!isset($this->contextinstanceid)) { - throw new \coding_exception('The \'contextinstanceid\' value must be set.'); + $errors[] = "The 'contextinstanceid' value must be set."; } if (!isset($this->other['quizid'])) { - throw new \coding_exception('The \'quizid\' value must be set in other.'); + $errors[] = "The 'quizid' value must be set in other."; } if (!isset($this->other['slotnumber'])) { - throw new \coding_exception('The \'slotnumber\' value must be set in other.'); + $errors[] = "The 'slotnumber' value must be set in other."; } if (!isset($this->other['page'])) { - throw new \coding_exception('The \'page\' value must be set in other.'); + $errors[] = "The 'page' value must be set in other."; + } + + $questionbankfields = ['questionbankentryid', 'version']; + $referencefields = ['questionscontextid', 'filtercondition']; + $questionbankset = isset($this->other['questionbankentryid']) || array_key_exists('version', $this->other); + $referenceset = isset($this->other['questionscontextid']) || isset($this->other['filtercondition']); + + if ($questionbankset && $referenceset) { + $errors[] = "Values for exactly one of these field sets must be set in 'other': " . + '(' . implode(', ', $questionbankfields) . '), ' . + '(' . implode(', ', $referencefields) . ')'; + } else if (!$questionbankset && !$referenceset) { + $errors[] = "Values for exactly one of these field sets must be set in 'other': " . + '(' . implode(', ', $questionbankfields) . '), ' . + '(' . implode(', ', $referencefields) . ')'; + } + + if ($questionbankset) { + if (!isset($this->other['questionbankentryid'])) { + $errors[] = "The 'questionbankentryid' value must be set in other."; + } + if (!array_key_exists('version', $this->other)) { + $errors[] = "The 'version' value must be set in other."; + } + } + + if ($referenceset) { + if (!isset($this->other['questionscontextid'])) { + $errors[] = "The 'questionscontextid' value must be set in other."; + } + if (!isset($this->other['filtercondition'])) { + $errors[] = "The 'filtercondition' value must be set in other."; + } + } + + if ($errors) { + throw new coding_exception("Errors in event data:\n\n" . implode("\n", $errors)); } } diff --git a/public/mod/quiz/classes/event/slot_filtercondition_updated.php b/public/mod/quiz/classes/event/slot_filtercondition_updated.php new file mode 100644 index 00000000000..6ba96d3d990 --- /dev/null +++ b/public/mod/quiz/classes/event/slot_filtercondition_updated.php @@ -0,0 +1,92 @@ +. + +declare(strict_types=1); + +namespace mod_quiz\event; + +use core\event\base; +use core\exception\coding_exception; +use core\url; + +/** + * This event is fired when the filter condition of a slot + * using the question set references table is updated. + * + * @package mod_quiz + * @copyright 2024 Catalyst IT Australia Pty Ltd + * @author Cameron Ball + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class slot_filtercondition_updated extends base { + #[\Override] + protected function init() { + $this->data['objecttable'] = 'quiz_slots'; + $this->data['crud'] = 'c'; + $this->data['edulevel'] = self::LEVEL_TEACHING; + } + + #[\Override] + public static function get_name() { + return get_string('eventslotfilterconditionupdated', 'mod_quiz'); + } + + #[\Override] + public function get_description() { + return "The user with id '$this->userid' updated a slot using question references with " . + "id '{$this->objectid}' and " . + "slot number '{$this->other['slotnumber']}' " . + "on page '{$this->other['page']}' " . + "of the quiz with course module id '$this->contextinstanceid' " . + "to use question context '{$this->other['questionscontextid']}' and " . + "filter condition '{$this->other['filtercondition']}'."; + } + + #[\Override] + public function get_url() { + return new url('/mod/quiz/edit.php', ['cmid' => $this->contextinstanceid]); + } + + #[\Override] + protected function validate_data() { + $missing = fn(string $field) => !isset($this->other[$field]); + $missingmembers = array_filter(['objectid'], fn(string $member): bool => !isset($this->$member)); + $missingfields = array_filter(['quizid', 'slotnumber', 'page', 'questionscontextid', 'filtercondition'], $missing); + parent::validate_data(); + + $errors = [ + ...array_map(fn(string $member): string => "The '$member' value must be set.", $missingmembers), + ...array_map(fn(string $field): string => "The '$field' value must be set in other.", $missingfields), + ]; + + if ($errors) { + throw new coding_exception("Errors in event data:\n\n" . implode("\n", $errors)); + } + } + + #[\Override] + public static function get_objectid_mapping() { + return ['db' => 'quiz_slots', 'restore' => 'quiz_question_instance']; + } + + #[\Override] + public static function get_other_mapping() { + $othermapped = []; + $othermapped['quizid'] = ['db' => 'quiz', 'restore' => 'quiz']; + + return $othermapped; + } +} diff --git a/public/mod/quiz/classes/local/structure/slot_random.php b/public/mod/quiz/classes/local/structure/slot_random.php index ff2f3e178fb..899264d2393 100644 --- a/public/mod/quiz/classes/local/structure/slot_random.php +++ b/public/mod/quiz/classes/local/structure/slot_random.php @@ -16,7 +16,10 @@ namespace mod_quiz\local\structure; -use context_module; +use core\context\module; +use core\exception\coding_exception; +use mod_quiz\event\slot_created; +use mod_quiz\event\slot_filtercondition_updated; /** * Class slot_random, represents a random question slot type. @@ -196,20 +199,21 @@ class slot_random { $this->referencerecord->filtercondition = $this->filtercondition; $DB->insert_record('question_set_references', $this->referencerecord); - $trans->allow_commit(); - // Log slot created event. $cm = get_coursemodule_from_instance('quiz', $quiz->id); - $event = \mod_quiz\event\slot_created::create([ - 'context' => context_module::instance($cm->id), + slot_created::create([ + 'context' => module::instance($cm->id), 'objectid' => $this->record->id, 'other' => [ 'quizid' => $quiz->id, 'slotnumber' => $this->record->slot, - 'page' => $this->record->page - ] - ]); - $event->trigger(); + 'page' => $this->record->page, + 'questionscontextid' => $this->referencerecord->questionscontextid, + 'filtercondition' => $this->referencerecord->filtercondition, + ], + ])->trigger(); + + $trans->allow_commit(); } /** @@ -221,20 +225,35 @@ class slot_random { global $DB; if (!isset($this->record->id)) { - throw new \coding_exception('Cannot update filtercondition without slot record ID.'); + throw new coding_exception('Cannot update filtercondition without slot record ID.'); } - $setreferenceid = $DB->get_field('question_set_references', 'id', [ + $cm = get_coursemodule_from_instance('quiz', $this->get_quiz()->id); + $transaction = $DB->start_delegated_transaction(); + $filterconditionjson = json_encode($filtercondition); + $params = [ 'component' => 'mod_quiz', 'questionarea' => 'slot', 'itemid' => $this->record->id, - ], MUST_EXIST); - - $update = (object)[ - 'id' => $setreferenceid, - 'filtercondition' => json_encode($filtercondition), - 'questionscontextid' => $this->referencerecord->questionscontextid, + 'usingcontextid' => $this->referencerecord->usingcontextid ?? module::instance($cm->id)->id, ]; - $DB->update_record('question_set_references', $update); + $setreferenceid = $DB->get_field('question_set_references', 'id', $params, MUST_EXIST); + $DB->update_record('question_set_references', (object)[ + 'id' => $setreferenceid, + 'filtercondition' => $filterconditionjson, + 'questionscontextid' => $this->referencerecord->questionscontextid, + ]); + slot_filtercondition_updated::create([ + 'context' => module::instance($cm->id), + 'objectid' => $this->record->id, + 'other' => [ + 'quizid' => $this->get_quiz()->id, + 'slotnumber' => $this->record->slot, + 'page' => $this->record->page, + 'questionscontextid' => $this->referencerecord->questionscontextid, + 'filtercondition' => $filterconditionjson, + ], + ])->trigger(); + $transaction->allow_commit(); } } diff --git a/public/mod/quiz/lang/en/quiz.php b/public/mod/quiz/lang/en/quiz.php index 10acfd7bff7..8517dd7a12b 100644 --- a/public/mod/quiz/lang/en/quiz.php +++ b/public/mod/quiz/lang/en/quiz.php @@ -395,6 +395,7 @@ $string['eventsectionshuffleupdated'] = 'Section shuffle updated'; $string['eventslotcreated'] = 'Slot created'; $string['eventslotdeleted'] = 'Slot deleted'; $string['eventslotdisplayedquestionnumberupdated'] = 'Slot displayed question number updated'; +$string['eventslotfilterconditionupdated'] = 'Slot filter condition updated'; $string['eventslotgradeitemupdated'] = 'Slot grade item updated'; $string['eventslotmarkupdated'] = 'Slot mark updated'; $string['eventslotversionupdated'] = 'Slot version updated'; diff --git a/public/mod/quiz/locallib.php b/public/mod/quiz/locallib.php index 68617d100de..35131f24aaa 100644 --- a/public/mod/quiz/locallib.php +++ b/public/mod/quiz/locallib.php @@ -1885,8 +1885,6 @@ function quiz_add_quiz_question($questionid, $quiz, $page = 0, $maxmark = null) $DB->insert_record('question_references', $questionreferences); } - $trans->allow_commit(); - // Log slot created event. $cm = get_coursemodule_from_instance('quiz', $quiz->id); $event = \mod_quiz\event\slot_created::create([ @@ -1895,10 +1893,14 @@ function quiz_add_quiz_question($questionid, $quiz, $page = 0, $maxmark = null) 'other' => [ 'quizid' => $quiz->id, 'slotnumber' => $slot->slot, - 'page' => $slot->page + 'page' => $slot->page, + 'questionbankentryid' => $questionreferences->questionbankentryid, + 'version' => $questionreferences->version, ] ]); $event->trigger(); + + $trans->allow_commit(); } /** diff --git a/public/mod/quiz/tests/event/events_test.php b/public/mod/quiz/tests/event/events_test.php index adbc95cdd00..66b5b0ccc58 100644 --- a/public/mod/quiz/tests/event/events_test.php +++ b/public/mod/quiz/tests/event/events_test.php @@ -28,6 +28,7 @@ namespace mod_quiz\event; use mod_quiz\quiz_attempt; use mod_quiz\quiz_settings; use context_module; +use core_question\local\bank\condition; use mod_quiz\external\submit_question_version; /** @@ -1158,8 +1159,10 @@ final class events_test extends \advanced_testcase { 'other' => [ 'quizid' => $quizobj->get_quizid(), 'slotnumber' => 1, - 'page' => 1 - ] + 'page' => 1, + 'questionbankentryid' => 1, + 'version' => null, + ], ]; $event = \mod_quiz\event\slot_created::create($params); @@ -1175,6 +1178,61 @@ final class events_test extends \advanced_testcase { $this->assertEventContextNotUsed($event); } + /** + * Verify slot_created carries question reference information when a specific question is added. + */ + public function test_slot_created_for_question_reference(): void { + $quizobj = $this->prepare_quiz(); + $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); + $category = $questiongenerator->create_question_category(); + $question = $questiongenerator->create_question('shortanswer', null, ['category' => $category->id]); + + $sink = $this->redirectEvents(); + quiz_add_quiz_question($question->id, $quizobj->get_quiz()); + $events = array_filter($sink->get_events(), fn(\core\event\base $event): bool => $event instanceof slot_created); + + $this->assertNotEmpty($events); + $event = reset($events); + $this->assertArrayHasKey('questionbankentryid', $event->other); + $this->assertArrayHasKey('version', $event->other); + $this->assertArrayNotHasKey('questionscontextid', $event->other); + $this->assertArrayNotHasKey('filtercondition', $event->other); + } + + /** + * Verify slot_created carries question set reference information for random slots. + */ + public function test_slot_created_for_random_question(): void { + $quizobj = $this->prepare_quiz(); + $this->setAdminUser(); + $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); + $category = $questiongenerator->create_question_category(); + $questiongenerator->create_question('shortanswer', null, ['category' => $category->id]); + $questiongenerator->create_question('shortanswer', null, ['category' => $category->id]); + + $filtercondition = [ + 'filter' => [ + 'category' => [ + 'jointype' => condition::JOINTYPE_DEFAULT, + 'values' => [$category->id], + 'filteroptions' => ['includesubcategories' => false], + ], + ], + ]; + + $sink = $this->redirectEvents(); + $quizobj->get_structure()->add_random_questions(1, 1, $filtercondition); + $events = array_filter($sink->get_events(), fn(\core\event\base $event): bool => $event instanceof slot_created); + + $this->assertNotEmpty($events); + $event = reset($events); + $this->assertArrayHasKey('questionscontextid', $event->other); + $this->assertArrayHasKey('filtercondition', $event->other); + $this->assertArrayNotHasKey('questionbankentryid', $event->other); + $this->assertArrayNotHasKey('version', $event->other); + $this->assertEquals(json_encode($filtercondition), $event->other['filtercondition']); + } + /** * Test the slot deleted event. * diff --git a/public/mod/quiz/tests/external/update_filter_condition_test.php b/public/mod/quiz/tests/external/update_filter_condition_test.php index 3e83ae73517..604de264b99 100644 --- a/public/mod/quiz/tests/external/update_filter_condition_test.php +++ b/public/mod/quiz/tests/external/update_filter_condition_test.php @@ -18,6 +18,7 @@ namespace mod_quiz\external; use advanced_testcase; use core_question\local\bank\condition; +use mod_quiz\event\slot_filtercondition_updated; use mod_quiz\quiz_settings; /** @@ -115,7 +116,24 @@ final class update_filter_condition_test extends advanced_testcase { // Call the webservice execute method. $filtercondition['filter']['category']['values'] = [$questioncategory2->id]; - update_filter_condition::execute($cm->id, $qsetref->itemid, json_encode($filtercondition)); + $slot = $DB->get_record('quiz_slots', ['id' => $qsetref->itemid], '*', MUST_EXIST); + $filterjson = json_encode($filtercondition); + $sink = $this->redirectEvents(); + + update_filter_condition::execute($cm->id, $qsetref->itemid, $filterjson); + $events = array_filter( + $sink->get_events(), + fn(\core\event\base $event): bool => $event instanceof slot_filtercondition_updated + ); + $this->assertCount(1, $events); + + $event = reset($events); + $this->assertEquals($slot->id, $event->objectid); + $this->assertEquals($quizcontext, $event->get_context()); + $this->assertEquals($slot->slot, $event->other['slotnumber']); + $this->assertEquals($slot->page, $event->other['page']); + $this->assertEquals($questioncategory2->contextid, $event->other['questionscontextid']); + $this->assertEquals($filterjson, $event->other['filtercondition']); // Check that the questionscontextid value has been updated. $qsetref = $DB->get_record('question_set_references', [ diff --git a/public/mod/quiz/version.php b/public/mod/quiz/version.php index 80899f4010f..75c51b6a3d8 100644 --- a/public/mod/quiz/version.php +++ b/public/mod/quiz/version.php @@ -24,6 +24,6 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2025100600; +$plugin->version = 2025121300; $plugin->requires = 2025092600; $plugin->component = 'mod_quiz';