Skip to content

Commit

Permalink
MDL-82587 mod_quiz: Add slot version updated event
Browse files Browse the repository at this point in the history
  • Loading branch information
cameron1729 committed Aug 1, 2024
1 parent 1a33da6 commit 34785bc
Show file tree
Hide file tree
Showing 11 changed files with 248 additions and 40 deletions.
2 changes: 1 addition & 1 deletion mod/quiz/amd/build/question_slot.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion mod/quiz/amd/build/question_slot.min.js.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 1 addition & 8 deletions mod/quiz/amd/src/question_slot.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
};
107 changes: 107 additions & 0 deletions mod/quiz/classes/event/slot_version_updated.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.

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 <[email protected]>
* @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;
}
}
46 changes: 18 additions & 28 deletions mod/quiz/classes/external/submit_question_version.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)];
}

/**
Expand Down
3 changes: 2 additions & 1 deletion mod/quiz/classes/output/edit_renderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
}

Expand Down Expand Up @@ -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.
Expand Down
51 changes: 51 additions & 0 deletions mod/quiz/classes/structure.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
*
Expand Down
1 change: 1 addition & 0 deletions mod/quiz/lang/en/quiz.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Loading

0 comments on commit 34785bc

Please sign in to comment.