diff --git a/mod/quiz/amd/build/question_slot.min.js b/mod/quiz/amd/build/question_slot.min.js index 576b5eb93c964..b00672160f193 100644 --- a/mod/quiz/amd/build/question_slot.min.js +++ b/mod/quiz/amd/build/question_slot.min.js @@ -6,6 +6,6 @@ define("mod_quiz/question_slot",["exports","core/ajax","core/notification"],(fun * @copyright 2021 Catalyst IT Australia Pty Ltd * @author Guillermo Gomez Arias * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_notification=(obj=_notification)&&obj.__esModule?obj:{default:obj};const registerEventListeners=()=>{document.addEventListener("change",(e=>{if(!e.target.matches('[data-action="mod_quiz-select_slot"][data-slot-id]'))return;((slotId,newVersion)=>(0,_ajax.call)([{methodname:"mod_quiz_set_question_version",args:{slotid:slotId,newversion:newVersion}}])[0])(e.target.dataset.slotId,parseInt(e.target.value)).then((()=>{location.reload()})).catch(_notification.default.exception)}))};_exports.init=()=>{registerEventListeners()}})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_notification=(obj=_notification)&&obj.__esModule?obj:{default:obj};const registerEventListeners=()=>{document.addEventListener("change",(e=>{if(!e.target.matches('[data-action="mod_quiz-select_slot"][data-slot-id]'))return;const slotId=e.target.dataset.slotId,newVersion=parseInt(e.target.value);((slotId,newVersion)=>(0,_ajax.call)([{methodname:"mod_quiz_set_question_version",args:{slotid:slotId,newversion:newVersion}}])[0])(slotId,0===newVersion?null:newVersion).then((()=>{location.reload()})).catch(_notification.default.exception)}))};_exports.init=()=>{registerEventListeners()}})); //# sourceMappingURL=question_slot.min.js.map \ No newline at end of file diff --git a/mod/quiz/amd/build/question_slot.min.js.map b/mod/quiz/amd/build/question_slot.min.js.map index 094295df5772a..5ef8586bce45c 100644 --- a/mod/quiz/amd/build/question_slot.min.js.map +++ b/mod/quiz/amd/build/question_slot.min.js.map @@ -1 +1 @@ -{"version":3,"file":"question_slot.min.js","sources":["../src/question_slot.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Render the question slot template for each question in the quiz edit view.\n *\n * @module mod_quiz/question_slot\n * @copyright 2021 Catalyst IT Australia Pty Ltd\n * @author Guillermo Gomez Arias \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {call as fetchMany} from 'core/ajax';\nimport Notification from 'core/notification';\n\n/**\n * Set the question version for the slot.\n *\n * @param {Number} slotId\n * @param {Number} newVersion\n * @return {Array} The modified question version\n */\nconst setQuestionVersion = (slotId, newVersion) => fetchMany([{\n methodname: 'mod_quiz_set_question_version',\n args: {\n slotid: slotId,\n newversion: newVersion,\n }\n}])[0];\n\n/**\n * Replace the container with a new version.\n */\nconst registerEventListeners = () => {\n document.addEventListener('change', e => {\n if (!e.target.matches('[data-action=\"mod_quiz-select_slot\"][data-slot-id]')) {\n return;\n }\n\n const slotId = e.target.dataset.slotId;\n const newVersion = parseInt(e.target.value);\n\n setQuestionVersion(slotId, newVersion)\n .then(() => {\n location.reload();\n return;\n })\n .catch(Notification.exception);\n });\n};\n\n/** @property {Boolean} eventsRegistered If the event has been registered or not */\nlet eventsRegistered = false;\n\n/**\n * Entrypoint of the js.\n */\nexport const init = () => {\n if (eventsRegistered) {\n return;\n }\n\n registerEventListeners();\n};\n"],"names":["registerEventListeners","document","addEventListener","e","target","matches","slotId","newVersion","methodname","args","slotid","newversion","setQuestionVersion","dataset","parseInt","value","then","location","reload","catch","Notification","exception"],"mappings":";;;;;;;;4JA6CMA,uBAAyB,KAC3BC,SAASC,iBAAiB,UAAUC,QAC3BA,EAAEC,OAAOC,QAAQ,6DAbH,EAACC,OAAQC,cAAe,cAAU,CAAC,CAC1DC,WAAY,gCACZC,KAAM,CACFC,OAAQJ,OACRK,WAAYJ,eAEhB,GAcIK,CAHeT,EAAEC,OAAOS,QAAQP,OACbQ,SAASX,EAAEC,OAAOW,QAGhCC,MAAK,KACFC,SAASC,YAGZC,MAAMC,sBAAaC,6BAUZ,KAKhBrB"} \ No newline at end of file +{"version":3,"file":"question_slot.min.js","sources":["../src/question_slot.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Render the question slot template for each question in the quiz edit view.\n *\n * @module mod_quiz/question_slot\n * @copyright 2021 Catalyst IT Australia Pty Ltd\n * @author Guillermo Gomez Arias \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {call as fetchMany} from 'core/ajax';\nimport Notification from 'core/notification';\n\n/**\n * Set the question version for the slot.\n *\n * @param {Number} slotId\n * @param {Number} newVersion\n * @return {Array} The modified question version\n */\nconst setQuestionVersion = (slotId, newVersion) => fetchMany([{\n methodname: 'mod_quiz_set_question_version',\n args: {\n slotid: slotId,\n newversion: newVersion,\n }\n}])[0];\n\n/**\n * Replace the container with a new version.\n */\nconst registerEventListeners = () => {\n document.addEventListener('change', e => {\n if (!e.target.matches('[data-action=\"mod_quiz-select_slot\"][data-slot-id]')) {\n return;\n }\n\n const slotId = e.target.dataset.slotId;\n const newVersion = parseInt(e.target.value);\n\n setQuestionVersion(slotId, newVersion === 0 ? null : newVersion)\n .then(() => {\n location.reload();\n return;\n })\n .catch(Notification.exception);\n });\n};\n\n/**\n * Entrypoint of the js.\n */\nexport const init = () => {\n registerEventListeners();\n};\n"],"names":["registerEventListeners","document","addEventListener","e","target","matches","slotId","dataset","newVersion","parseInt","value","methodname","args","slotid","newversion","setQuestionVersion","then","location","reload","catch","Notification","exception"],"mappings":";;;;;;;;4JA6CMA,uBAAyB,KAC3BC,SAASC,iBAAiB,UAAUC,QAC3BA,EAAEC,OAAOC,QAAQ,mEAIhBC,OAASH,EAAEC,OAAOG,QAAQD,OAC1BE,WAAaC,SAASN,EAAEC,OAAOM,OAlBlB,EAACJ,OAAQE,cAAe,cAAU,CAAC,CAC1DG,WAAY,gCACZC,KAAM,CACFC,OAAQP,OACRQ,WAAYN,eAEhB,GAcIO,CAAmBT,OAAuB,IAAfE,WAAmB,KAAOA,YAChDQ,MAAK,KACFC,SAASC,YAGZC,MAAMC,sBAAaC,6BAOZ,KAChBrB"} \ No newline at end of file diff --git a/mod/quiz/amd/src/question_slot.js b/mod/quiz/amd/src/question_slot.js index b82c99456dc70..3b9079492ea0b 100644 --- a/mod/quiz/amd/src/question_slot.js +++ b/mod/quiz/amd/src/question_slot.js @@ -52,7 +52,7 @@ const registerEventListeners = () => { const slotId = e.target.dataset.slotId; const newVersion = parseInt(e.target.value); - setQuestionVersion(slotId, newVersion) + setQuestionVersion(slotId, newVersion === 0 ? null : newVersion) .then(() => { location.reload(); return; @@ -61,16 +61,9 @@ const registerEventListeners = () => { }); }; -/** @property {Boolean} eventsRegistered If the event has been registered or not */ -let eventsRegistered = false; - /** * Entrypoint of the js. */ export const init = () => { - if (eventsRegistered) { - return; - } - registerEventListeners(); }; diff --git a/mod/quiz/classes/event/slot_version_updated.php b/mod/quiz/classes/event/slot_version_updated.php new file mode 100644 index 0000000000000..a14d00a72ba74 --- /dev/null +++ b/mod/quiz/classes/event/slot_version_updated.php @@ -0,0 +1,107 @@ +. + +declare(strict_types=1); + +namespace mod_quiz\event; + +use core\exception\coding_exception; +use core\url; + +/** + * The question version of a slot has changed. + * + * @property-read array $other { + * Extra information about event. + * + * - int quizid: the id of the quiz. + * - int previousversion: the previous question version. + * - int newversion: the new question version. + * } + * + * @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_version_updated extends \core\event\base { + + #[\Override] + protected function init(): void { + $this->data['objecttable'] = 'quiz_slots'; + $this->data['crud'] = 'u'; + $this->data['edulevel'] = self::LEVEL_TEACHING; + } + + #[\Override] + public static function get_name(): string { + return get_string('eventslotversionupdated', 'mod_quiz'); + } + + #[\Override] + public function get_description(): string { + $previousversion = $this->other['previousversion'] ?? 'Always latest'; + $newversion = $this->other['newversion'] ?? 'Always latest'; + return "The user with id '$this->userid' updated the slot with id '{$this->objectid}' " . + "belonging to the quiz with course module id '$this->contextinstanceid'. " . + "Its question version was changed from '$previousversion' to '$newversion'."; + } + + #[\Override] + public function get_url(): url { + return new url('/mod/quiz/edit.php', ['cmid' => $this->contextinstanceid]); + } + + #[\Override] + protected function validate_data(): void { + parent::validate_data(); + + if (!isset($this->objectid)) { + throw new coding_exception('The \'objectid\' value must be set.'); + } + + if (!isset($this->contextinstanceid)) { + throw new coding_exception('The \'contextinstanceid\' value must be set.'); + } + + if (!isset($this->other['quizid'])) { + throw new coding_exception('The \'quizid\' value must be set in other.'); + } + + // The value of previousversion and newversion can be null, so we check if + // the array key exists. + if (!array_key_exists('previousversion', $this->other)) { + throw new coding_exception('The \'previousversion\' value must be set in other.'); + } + + if (!array_key_exists('newversion', $this->other)) { + throw new coding_exception('The \'newversion\' value must be set in other.'); + } + } + + #[\Override] + public static function get_objectid_mapping(): array { + return ['db' => 'quiz_slots', 'restore' => 'quiz_question_instance']; + } + + #[\Override] + public static function get_other_mapping(): array { + $othermapped = []; + $othermapped['quizid'] = ['db' => 'quiz', 'restore' => 'quiz']; + + return $othermapped; + } +} diff --git a/mod/quiz/classes/external/submit_question_version.php b/mod/quiz/classes/external/submit_question_version.php index 5facc34a8e7b1..328e0c1008c7c 100644 --- a/mod/quiz/classes/external/submit_question_version.php +++ b/mod/quiz/classes/external/submit_question_version.php @@ -22,11 +22,12 @@ require_once($CFG->dirroot . '/question/engine/datalib.php'); require_once($CFG->libdir . '/questionlib.php'); +use core\context\module; use core_external\external_api; use core_external\external_function_parameters; use core_external\external_single_structure; use core_external\external_value; -use stdClass; +use mod_quiz\quiz_settings; /** * External api for changing the question version in the quiz. @@ -45,46 +46,35 @@ class submit_question_version extends external_api { */ public static function execute_parameters(): external_function_parameters { return new external_function_parameters([ - 'slotid' => new external_value(PARAM_INT, ''), - 'newversion' => new external_value(PARAM_INT, '') + 'slotid' => new external_value(PARAM_INT), + 'newversion' => new external_value(PARAM_INT), ]); } /** * Set the questions slot parameters to display the question template. * - * @param int $slotid Slot id to display. - * @param int $newversion the version to set. 0 means 'always latest'. + * @param int $slotid Slot ID to display. + * @param int|null $newversion The version to set. Passing null means 'always latest'. + * For historical reasons, 0 also means 'always latest'. * @return array */ - public static function execute(int $slotid, int $newversion): array { + public static function execute(int $slotid, ?int $newversion): array { global $DB; - $params = [ - 'slotid' => $slotid, - 'newversion' => $newversion - ]; - $params = self::validate_parameters(self::execute_parameters(), $params); - $response = []; - // Get the required data. - $referencedata = $DB->get_record('question_references', - ['itemid' => $params['slotid'], 'component' => 'mod_quiz', 'questionarea' => 'slot']); - $slotdata = $DB->get_record('quiz_slots', ['id' => $slotid]); + $params = self::validate_parameters(self::execute_parameters(), ['slotid' => $slotid, 'newversion' => $newversion]); + $slot = $DB->get_record('quiz_slots', ['id' => $slotid], '*', MUST_EXIST); - // Capability check. - [, $cm] = get_course_and_cm_from_instance($slotdata->quizid, 'quiz'); - $context = \context_module::instance($cm->id); + $context = module::instance(get_course_and_cm_from_instance($slot->quizid, 'quiz')[1]->id); self::validate_context($context); require_capability('mod/quiz:manage', $context); - $reference = new stdClass(); - $reference->id = $referencedata->id; - if ($params['newversion'] === 0) { - $reference->version = null; - } else { - $reference->version = $params['newversion']; - } - $response['result'] = $DB->update_record('question_references', $reference); - return $response; + $quizobj = quiz_settings::create($slot->quizid); + + // This WS historically (and wrongly) accepted 0 to mean 'always latest'. The correct behaviour + // is that null implies awlays latest. To preserve backwards compatibility, we continue to accept + // 0, but just turn it in to null before passing to the appropriate API. See: MDL-82587. + $newversionnormalised = $params['newversion'] === 0 ? null : $params['newversion']; + return ['result' => $quizobj->get_structure()->update_slot_version($slot->id, $newversionnormalised)]; } /** diff --git a/mod/quiz/classes/output/edit_renderer.php b/mod/quiz/classes/output/edit_renderer.php index 21a8d6c9c33be..906c57311358b 100644 --- a/mod/quiz/classes/output/edit_renderer.php +++ b/mod/quiz/classes/output/edit_renderer.php @@ -521,6 +521,8 @@ public function questions_in_section(structure $structure, $section, foreach ($structure->get_slots_in_section($section->id) as $slot) { $output .= $this->question_row($structure, $slot, $contexts, $pagevars, $pageurl); } + + $this->page->requires->js_call_amd('mod_quiz/question_slot', 'init'); return html_writer::tag('ul', $output, ['class' => 'section img-text']); } @@ -776,7 +778,6 @@ public function question(structure $structure, int $slot, \moodle_url $pageurl) if ($structure->get_slot_by_number($slot)->qtype !== 'random') { $data['versionselection'] = true; $data['versionoption'] = $structure->get_version_choices_for_slot($slot); - $this->page->requires->js_call_amd('mod_quiz/question_slot', 'init', [$slotid]); } // Render the question slot template. diff --git a/mod/quiz/classes/structure.php b/mod/quiz/classes/structure.php index 15a72d372d145..1e979e786953b 100644 --- a/mod/quiz/classes/structure.php +++ b/mod/quiz/classes/structure.php @@ -24,6 +24,7 @@ use mod_quiz\event\quiz_grade_item_updated; use mod_quiz\event\slot_grade_item_updated; use mod_quiz\event\slot_mark_updated; +use mod_quiz\event\slot_version_updated; use mod_quiz\question\bank\qbank_helper; use mod_quiz\question\qubaids_for_quiz; use stdClass; @@ -1168,6 +1169,56 @@ public function update_slot_maxmark($slot, $maxmark) { return true; } + /** + * Update the question version for a given slot, if necessary. + * + * @param int $id ID of row from the quiz_slots table. + * @param int|null $newversion The new question version for the slot. + * A null value means 'Always latest'. + * @return bool True if the version was updated, false if no update was required. + * @throws coding_exception If the specified version does not exist. + */ + public function update_slot_version(int $id, ?int $newversion): bool { + global $DB; + + $slot = $this->get_slot_by_id($id); + $context = $this->quizobj->get_context(); + $refparams = ['usingcontextid' => $context->id, 'component' => 'mod_quiz', 'questionarea' => 'slot', 'itemid' => $slot->id]; + $reference = $DB->get_record('question_references', $refparams, '*', MUST_EXIST); + $oldversion = is_null($reference->version) ? null : (int) $reference->version; + $reference->version = $newversion === 0 ? null : $newversion; + $existsparams = ['questionbankentryid' => $reference->questionbankentryid, 'version' => $newversion]; + $versionexists = $DB->record_exists('question_versions', $existsparams); + + // We are attempting to switch to an existing version. + // Verify that the version we want to switch to exists. + if (!is_null($newversion) && !$versionexists) { + throw new coding_exception( + 'Version: ' . $newversion . ' ' . + 'does not exist for question bank entry: ' . $reference->questionbankentryid + ); + } + + if ($newversion === $oldversion) { + return false; + } + + $transaction = $DB->start_delegated_transaction(); + $DB->update_record('question_references', $reference); + slot_version_updated::create([ + 'context' => $this->quizobj->get_context(), + 'objectid' => $slot->id, + 'other' => [ + 'quizid' => $this->get_quizid(), + 'previousversion' => $oldversion, + 'newversion' => $reference->version, + ], + ])->trigger(); + $transaction->allow_commit(); + + return true; + } + /** * Change which grade this slot contributes to, for quizzes with multiple grades. * diff --git a/mod/quiz/lang/en/quiz.php b/mod/quiz/lang/en/quiz.php index 25d56edff470e..f7909ff2efa05 100644 --- a/mod/quiz/lang/en/quiz.php +++ b/mod/quiz/lang/en/quiz.php @@ -390,6 +390,7 @@ $string['eventslotdisplayedquestionnumberupdated'] = 'Slot displayed question number updated'; $string['eventslotgradeitemupdated'] = 'Slot grade item updated'; $string['eventslotmarkupdated'] = 'Slot mark updated'; +$string['eventslotversionupdated'] = 'Slot version updated'; $string['eventslotmoved'] = 'Slot moved'; $string['eventslotrequirepreviousupdated'] = 'Slot require previous updated'; $string['everynquestions'] = 'Every {$a} questions'; diff --git a/mod/quiz/tests/event/events_test.php b/mod/quiz/tests/event/events_test.php index a13a48dbc3919..b9eda2a5c00ba 100644 --- a/mod/quiz/tests/event/events_test.php +++ b/mod/quiz/tests/event/events_test.php @@ -28,6 +28,7 @@ use mod_quiz\quiz_attempt; use mod_quiz\quiz_settings; use context_module; +use mod_quiz\external\submit_question_version; /** * Unit tests for quiz events. @@ -36,6 +37,7 @@ * @category phpunit * @copyright 2013 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers \mod_quiz\external\submit_question_version */ class events_test extends \advanced_testcase { @@ -68,6 +70,10 @@ protected function prepare_quiz() { $saq = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]); $numq = $questiongenerator->create_question('numerical', null, ['category' => $cat->id]); + // Update the numq question so it has multiple versions. Needed to test + // the version updated event. + $questiongenerator->update_question($numq, null, ['name' => 'Second version of numq']); + // Add them to the quiz. quiz_add_quiz_question($saq->id, $quiz); quiz_add_quiz_question($numq->id, $quiz); @@ -1202,6 +1208,28 @@ public function test_slot_mark_updated(): void { $this->assertEventContextNotUsed($event); } + public function test_slot_version_updated(): void { + $quizobj = $this->prepare_quiz(); + $this->setAdminUser(); + + $quizobj->preload_questions(); + [, $numqslotid] = array_column($quizobj->get_questions(null, false), 'slotid'); + + $sink = $this->redirectEvents(); + submit_question_version::execute($numqslotid, 2); + $events = $sink->get_events(); + $event = reset($events); + + // Check that the event data is valid. + $expecteddesc = "The user with id '2' updated the slot with id '$numqslotid' " . + "belonging to the quiz with course module id '{$quizobj->get_cmid()}'. " . + "Its question version was changed from 'Always latest' to '2'."; + $this->assertInstanceOf('\mod_quiz\event\slot_version_updated', $event); + $this->assertEquals($quizobj->get_context(), $event->get_context()); + $this->assertEquals($expecteddesc, $event->get_description()); + $this->assertEventContextNotUsed($event); + } + /** * Test quiz_grade_item_created. * diff --git a/mod/quiz/tests/structure_test.php b/mod/quiz/tests/structure_test.php index afa9d33cf90d7..3d8c2acfed18e 100644 --- a/mod/quiz/tests/structure_test.php +++ b/mod/quiz/tests/structure_test.php @@ -16,6 +16,9 @@ namespace mod_quiz; +use core\exception\coding_exception; +use question_bank; + defined('MOODLE_INTERNAL') || die(); global $CFG; @@ -904,6 +907,40 @@ public function test_update_question_dependency(): void { $this->assertEquals(0, $structure->is_question_dependent_on_previous_slot(2)); } + public function test_update_slot_version(): void { + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); + $quiz = $quizgenerator->create_instance(['course' => $course->id, 'questionsperpage' => 0, + 'grade' => 100.0, 'sumgrades' => 2]); + + get_coursemodule_from_instance('quiz', $quiz->id, $course->id); + + $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); + $cat = $questiongenerator->create_question_category(); + $numq = $questiongenerator->create_question('numerical', null, ['category' => $cat->id]); + $questiongenerator->update_question($numq, null, ['name' => 'Second version of numq']); + quiz_add_quiz_question($numq->id, $quiz); + + $quizobj = quiz_settings::create($quiz->id); + $quizobj->preload_questions(); + [$question] = array_values($quizobj->get_questions(null, false)); + $structure = $quizobj->get_structure(); + + // Updating to a version which exists, should succeed. + $this->assertTrue($structure->update_slot_version($question->slotid, 2)); + + // Updating to the same version as the current version should return false. + $this->assertFalse($structure->update_slot_version($question->slotid, 2)); + + // Updating to a version which does not exists, should throw exception. + $this->expectException(coding_exception::class); + $this->expectExceptionMessage('Version: 3 does not exist for question bank entry: ' . $question->questionbankentryid); + $structure->update_slot_version($question->slotid, 3); + + } + public function test_update_slot_grade_item(): void { $quizobj = $this->create_test_quiz([ ['TF1', 1, 'truefalse'], diff --git a/mod/quiz/version.php b/mod/quiz/version.php index fa22a93ba4087..c0b9aa1a833ed 100644 --- a/mod/quiz/version.php +++ b/mod/quiz/version.php @@ -24,6 +24,6 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2024051700; +$plugin->version = 2024072500; $plugin->requires = 2024041600; $plugin->component = 'mod_quiz';