mirror of
https://github.com/moodle/moodle.git
synced 2025-04-21 00:12:56 +02:00
MDL-80300 mod_quiz: Add override manager class
This commit is contained in:
parent
70f611116d
commit
2c6a746578
5
mod/quiz/classes/cache/overrides.php
vendored
5
mod/quiz/classes/cache/overrides.php
vendored
@ -64,6 +64,11 @@ class overrides implements \cache_data_source {
|
||||
public function load_for_cache($key) {
|
||||
global $DB;
|
||||
|
||||
// Ignore getting data if this is a cache invalidation - {@see \cache_helper::purge_by_event()}.
|
||||
if ($key == 'lastinvalidation') {
|
||||
return null;
|
||||
}
|
||||
|
||||
[$quizid, $ug, $ugid] = explode('_', $key);
|
||||
$quizid = (int) $quizid;
|
||||
|
||||
|
127
mod/quiz/classes/local/override_cache.php
Normal file
127
mod/quiz/classes/local/override_cache.php
Normal file
@ -0,0 +1,127 @@
|
||||
<?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/>.
|
||||
|
||||
namespace mod_quiz\local;
|
||||
|
||||
/**
|
||||
* Cache manager for quiz overrides
|
||||
*
|
||||
* Override cache data is set via its data source, {@see \mod_quiz\cache\overrides}
|
||||
* @package mod_quiz
|
||||
* @copyright 2024 Matthew Hilton <matthewhilton@catalyst-au.net>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class override_cache {
|
||||
|
||||
/** @var string invalidation event used to purge data when reset_userdata is called, {@see \cache_helper::purge_by_event()}**/
|
||||
public const INVALIDATION_USERDATARESET = 'userdatareset';
|
||||
|
||||
/**
|
||||
* Create override_cache object and link to quiz
|
||||
*
|
||||
* @param int $quizid The quiz to link this cache to
|
||||
*/
|
||||
public function __construct(
|
||||
/** @var int $quizid ID of quiz cache is being operated on **/
|
||||
protected readonly int $quizid
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the override cache
|
||||
*
|
||||
* @return cache
|
||||
*/
|
||||
protected function get_cache(): \cache {
|
||||
return \cache::make('mod_quiz', 'overrides');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns group cache key
|
||||
*
|
||||
* @param int $groupid
|
||||
* @return string the group cache key
|
||||
*/
|
||||
protected function get_group_cache_key(int $groupid): string {
|
||||
return "{$this->quizid}_g_{$groupid}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns user cache key
|
||||
*
|
||||
* @param int $userid
|
||||
* @return string the user cache key
|
||||
*/
|
||||
protected function get_user_cache_key(int $userid): string {
|
||||
return "{$this->quizid}_u_{$userid}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the override value in the cache for the given group
|
||||
*
|
||||
* @param int $groupid group to get cached override data for
|
||||
* @return ?\stdClass override value in the cache for the given group, or null if there is none.
|
||||
*/
|
||||
public function get_cached_group_override(int $groupid): ?\stdClass {
|
||||
$raw = $this->get_cache()->get($this->get_group_cache_key($groupid));
|
||||
return empty($raw) || !is_object($raw) ? null : (object) $raw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the override value in the cache for the given user
|
||||
*
|
||||
* @param int $userid user to get cached override data for
|
||||
* @return ?\stdClass the override value in the cache for the given user, or null if there is none.
|
||||
*/
|
||||
public function get_cached_user_override(int $userid): ?\stdClass {
|
||||
$raw = $this->get_cache()->get($this->get_user_cache_key($userid));
|
||||
return empty($raw) || !is_object($raw) ? null : (object) $raw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the cached override data for a given group
|
||||
*
|
||||
* @param int $groupid group to delete data for
|
||||
*/
|
||||
public function clear_for_group(int $groupid): void {
|
||||
$this->get_cache()->delete($this->get_group_cache_key($groupid));
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the cached override data for the given user
|
||||
*
|
||||
* @param int $userid user to delete data for
|
||||
*/
|
||||
public function clear_for_user(int $userid): void {
|
||||
$this->get_cache()->delete($this->get_user_cache_key($userid));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the cache for the given user and/or group.
|
||||
*
|
||||
* @param ?int $userid user to delete data for, or null.
|
||||
* @param ?int $groupid group to delete data for, or null.
|
||||
*/
|
||||
public function clear_for(?int $userid = null, ?int $groupid = null): void {
|
||||
if (!empty($userid)) {
|
||||
$this->clear_for_user($userid);
|
||||
}
|
||||
|
||||
if (!empty($groupid)) {
|
||||
$this->clear_for_group($groupid);
|
||||
}
|
||||
}
|
||||
}
|
604
mod/quiz/classes/local/override_manager.php
Normal file
604
mod/quiz/classes/local/override_manager.php
Normal file
@ -0,0 +1,604 @@
|
||||
<?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/>.
|
||||
|
||||
namespace mod_quiz\local;
|
||||
|
||||
use mod_quiz\event\group_override_created;
|
||||
use mod_quiz\event\group_override_deleted;
|
||||
use mod_quiz\event\group_override_updated;
|
||||
use mod_quiz\event\user_override_created;
|
||||
use mod_quiz\event\user_override_deleted;
|
||||
use mod_quiz\event\user_override_updated;
|
||||
|
||||
/**
|
||||
* Manager class for quiz overrides
|
||||
*
|
||||
* @package mod_quiz
|
||||
* @copyright 2024 Matthew Hilton <matthewhilton@catalyst-au.net>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class override_manager {
|
||||
|
||||
/** @var array quiz setting keys that can be overwritten **/
|
||||
private const OVERRIDEABLE_QUIZ_SETTINGS = ['timeopen', 'timeclose', 'timelimit', 'attempts', 'password'];
|
||||
|
||||
/**
|
||||
* Create override manager
|
||||
*
|
||||
* @param \stdClass $quiz The quiz to link the manager to.
|
||||
* @param \context_module $context Context being operated in
|
||||
*/
|
||||
public function __construct(
|
||||
/** @var \stdClass The quiz linked to this manager instance **/
|
||||
protected readonly \stdClass $quiz,
|
||||
/** @var \context_module The context being operated in **/
|
||||
public readonly \context_module $context
|
||||
) {
|
||||
global $CFG;
|
||||
// Required for quiz_* methods.
|
||||
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
|
||||
|
||||
// Sanity check that the context matches the quiz.
|
||||
if (empty($quiz->cmid) || $quiz->cmid != $context->instanceid) {
|
||||
throw new \coding_exception("Given context does not match the quiz object");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all overrides for the linked quiz.
|
||||
*
|
||||
* @return array of quiz_override records
|
||||
*/
|
||||
public function get_all_overrides(): array {
|
||||
global $DB;
|
||||
return $DB->get_records('quiz_overrides', ['quiz' => $this->quiz->id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the data, usually from a moodleform or a webservice call.
|
||||
* If it contains an 'id' property, additional validation is performed against the existing record.
|
||||
*
|
||||
* @param array $formdata data from moodleform or webservice call.
|
||||
* @return array array where the keys are error elements, and the values are lists of errors for each element.
|
||||
*/
|
||||
public function validate_data(array $formdata): array {
|
||||
global $DB;
|
||||
|
||||
// Because this can be called directly (e.g. via edit_override_form)
|
||||
// and not just through save_override, we must ensure the data
|
||||
// is parsed in the same way.
|
||||
$formdata = $this->parse_formdata($formdata);
|
||||
|
||||
$formdata = (object) $formdata;
|
||||
|
||||
$errors = [];
|
||||
|
||||
// Ensure at least one of the overrideable settings is set.
|
||||
$keysthatareset = array_map(function($key) use ($formdata) {
|
||||
return isset($formdata->$key) && !is_null($formdata->$key);
|
||||
} , self::OVERRIDEABLE_QUIZ_SETTINGS);
|
||||
|
||||
if (!in_array(true, $keysthatareset)) {
|
||||
$errors['general'][] = new \lang_string('nooverridedata', 'quiz');
|
||||
}
|
||||
|
||||
// Ensure quiz is a valid quiz.
|
||||
if (empty($formdata->quiz) || empty(get_coursemodule_from_instance('quiz', $formdata->quiz))) {
|
||||
$errors['quiz'][] = new \lang_string('overrideinvalidquiz', 'quiz');
|
||||
}
|
||||
|
||||
// Ensure either userid or groupid is set.
|
||||
if (empty($formdata->userid) && empty($formdata->groupid)) {
|
||||
$errors['general'][] = new \lang_string('overridemustsetuserorgroup', 'quiz');
|
||||
}
|
||||
|
||||
// Ensure not both userid and groupid are set.
|
||||
if (!empty($formdata->userid) && !empty($formdata->groupid)) {
|
||||
$errors['general'][] = new \lang_string('overridecannotsetbothgroupanduser', 'quiz');
|
||||
}
|
||||
|
||||
// If group is set, ensure it is a real group.
|
||||
if (!empty($formdata->groupid) && empty(groups_get_group($formdata->groupid))) {
|
||||
$errors['groupid'][] = new \lang_string('overrideinvalidgroup', 'quiz');
|
||||
}
|
||||
|
||||
// If user is set, ensure it is a valid user.
|
||||
if (!empty($formdata->userid) && !\core_user::is_real_user($formdata->userid, true)) {
|
||||
$errors['userid'][] = new \lang_string('overrideinvaliduser', 'quiz');
|
||||
}
|
||||
|
||||
// Ensure timeclose is later than timeopen, if both are set.
|
||||
if (!empty($formdata->timeclose) && !empty($formdata->timeopen) && $formdata->timeclose <= $formdata->timeopen) {
|
||||
$errors['timeclose'][] = new \lang_string('closebeforeopen', 'quiz');
|
||||
}
|
||||
|
||||
// Ensure attempts is a integer greater than or equal to 0 (0 is unlimited attempts).
|
||||
if (isset($formdata->attempts) && ((int) $formdata->attempts < 0)) {
|
||||
$errors['attempts'][] = new \lang_string('overrideinvalidattempts', 'quiz');
|
||||
}
|
||||
|
||||
// Ensure timelimit is greather than zero.
|
||||
if (!empty($formdata->timelimit) && $formdata->timelimit <= 0) {
|
||||
$errors['timelimit'][] = new \lang_string('overrideinvalidtimelimit', 'quiz');
|
||||
}
|
||||
|
||||
// Ensure other records do not exist with the same group or user.
|
||||
if (!empty($formdata->quiz) && (!empty($formdata->userid) || !empty($formdata->groupid))) {
|
||||
$existingrecordparams = ['quiz' => $formdata->quiz, 'groupid' => $formdata->groupid ?? null,
|
||||
'userid' => $formdata->userid ?? null, ];
|
||||
$records = $DB->get_records('quiz_overrides', $existingrecordparams, '', 'id');
|
||||
|
||||
// Ignore self if updating.
|
||||
if (!empty($formdata->id)) {
|
||||
unset($records[$formdata->id]);
|
||||
}
|
||||
|
||||
// If count is not zero, it means existing records exist already for this user/group.
|
||||
if (!empty($records)) {
|
||||
$errors['general'][] = new \lang_string('overridemultiplerecordsexist', 'quiz');
|
||||
}
|
||||
}
|
||||
|
||||
// If is existing record, validate it against the existing record.
|
||||
if (!empty($formdata->id)) {
|
||||
$existingrecorderrors = self::validate_against_existing_record($formdata->id, $formdata);
|
||||
$errors = array_merge($errors, $existingrecorderrors);
|
||||
}
|
||||
|
||||
// Implode each value (array of error strings) into a single error string.
|
||||
foreach ($errors as $key => $value) {
|
||||
$errors[$key] = implode(",", $value);
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the existing quiz override record with the given ID or null if does not exist.
|
||||
*
|
||||
* @param int $id existing quiz override id
|
||||
* @return ?\stdClass record, if exists
|
||||
*/
|
||||
private static function get_existing(int $id): ?\stdClass {
|
||||
global $DB;
|
||||
return $DB->get_record('quiz_overrides', ['id' => $id]) ?: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the formdata against an existing record.
|
||||
*
|
||||
* @param int $existingid id of existing quiz override record
|
||||
* @param \stdClass $formdata formdata, usually from moodleform or webservice call.
|
||||
* @return array array where the keys are error elements, and the values are lists of errors for each element.
|
||||
*/
|
||||
private static function validate_against_existing_record(int $existingid, \stdClass $formdata): array {
|
||||
$existingrecord = self::get_existing($existingid);
|
||||
$errors = [];
|
||||
|
||||
// Existing record must exist.
|
||||
if (empty($existingrecord)) {
|
||||
$errors['general'][] = new \lang_string('overrideinvalidexistingid', 'quiz');
|
||||
}
|
||||
|
||||
// Group value must match existing record if it is set in the formdata.
|
||||
if (!empty($existingrecord) && !empty($formdata->groupid) && $existingrecord->groupid != $formdata->groupid) {
|
||||
$errors['groupid'][] = new \lang_string('overridecannotchange', 'quiz');
|
||||
}
|
||||
|
||||
// User value must match existing record if it is set in the formdata.
|
||||
if (!empty($existingrecord) && !empty($formdata->userid) && $existingrecord->userid != $formdata->userid) {
|
||||
$errors['userid'][] = new \lang_string('overridecannotchange', 'quiz');
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the formdata by finding only the OVERRIDEABLE_QUIZ_SETTINGS,
|
||||
* clearing any values that match the existing quiz, and re-adds the user or group id.
|
||||
*
|
||||
* @param array $formdata data usually from moodleform or webservice call.
|
||||
* @return array array containing parsed formdata, with keys as the properties and values as the values.
|
||||
* Any values set the same as the existing quiz are set to null.
|
||||
*/
|
||||
public function parse_formdata(array $formdata): array {
|
||||
// Get the data from the form that we want to update.
|
||||
$settings = array_intersect_key($formdata, array_flip(self::OVERRIDEABLE_QUIZ_SETTINGS));
|
||||
|
||||
// Remove values that are the same as currently in the quiz.
|
||||
$settings = $this->clear_unused_values($settings);
|
||||
|
||||
// Add the user / group back as applicable.
|
||||
$userorgroupdata = array_intersect_key($formdata, array_flip(['userid', 'groupid', 'quiz', 'id']));
|
||||
|
||||
return array_merge($settings, $userorgroupdata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the given override. If an id is given, it updates, otherwise it creates a new one.
|
||||
* Note, capabilities are not checked, {@see require_manage_capability()}
|
||||
*
|
||||
* @param array $formdata data usually from moodleform or webservice call.
|
||||
* @return int updated/inserted record id
|
||||
*/
|
||||
public function save_override(array $formdata): int {
|
||||
global $DB;
|
||||
|
||||
// Extract only the necessary data.
|
||||
$datatoset = $this->parse_formdata($formdata);
|
||||
$datatoset['quiz'] = $this->quiz->id;
|
||||
|
||||
// Validate the data is OK.
|
||||
$errors = $this->validate_data($datatoset);
|
||||
if (!empty($errors)) {
|
||||
$errorstr = implode(',', $errors);
|
||||
throw new \invalid_parameter_exception($errorstr);
|
||||
}
|
||||
|
||||
// Insert or update.
|
||||
$id = $datatoset['id'] ?? 0;
|
||||
if (!empty($id)) {
|
||||
$DB->update_record('quiz_overrides', $datatoset);
|
||||
} else {
|
||||
$id = $DB->insert_record('quiz_overrides', $datatoset);
|
||||
}
|
||||
|
||||
$userid = $datatoset['userid'] ?? null;
|
||||
$groupid = $datatoset['groupid'] ?? null;
|
||||
|
||||
// Clear the cache.
|
||||
$cache = new override_cache($this->quiz->id);
|
||||
$cache->clear_for($userid, $groupid);
|
||||
|
||||
// Trigger moodle events.
|
||||
if (empty($formdata['id'])) {
|
||||
$this->fire_created_event($id, $userid, $groupid);
|
||||
} else {
|
||||
$this->fire_updated_event($id, $userid, $groupid);
|
||||
}
|
||||
|
||||
// Update open events.
|
||||
quiz_update_open_attempts(['quizid' => $this->quiz->id]);
|
||||
|
||||
// Update calendar events.
|
||||
$isgroup = !empty($datatoset['groupid']);
|
||||
if ($isgroup) {
|
||||
// If is group, must update the entire quiz calendar events.
|
||||
quiz_update_events($this->quiz);
|
||||
} else {
|
||||
// If is just a user, can update only their calendar event.
|
||||
quiz_update_events($this->quiz, (object) $datatoset);
|
||||
}
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all the overrides for the linked quiz
|
||||
*
|
||||
* @param bool $shouldlog If true, will log a override_deleted event
|
||||
*/
|
||||
public function delete_all_overrides(bool $shouldlog = true): void {
|
||||
global $DB;
|
||||
$overrides = $DB->get_records('quiz_overrides', ['quiz' => $this->quiz->id], '', 'id,userid,groupid');
|
||||
$this->delete_overrides($overrides, $shouldlog);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes overrides given just their ID.
|
||||
* Note, the given IDs must exist otherwise an exception will be thrown.
|
||||
* Also note, capabilities are not checked, {@see require_manage_capability()}
|
||||
*
|
||||
* @param array $ids IDs of overrides to delete
|
||||
* @param bool $shouldlog If true, will log a override_deleted event
|
||||
*/
|
||||
public function delete_overrides_by_id(array $ids, bool $shouldlog = true): void {
|
||||
global $DB;
|
||||
[$sql, $params] = self::get_override_in_sql($this->quiz->id, $ids);
|
||||
$records = $DB->get_records_select('quiz_overrides', $sql, $params, '', 'id,userid,groupid');
|
||||
|
||||
// Ensure all the given ids exist, so the user is aware if they give a dodgy id.
|
||||
$missingids = array_diff($ids, array_keys($records));
|
||||
|
||||
if (!empty($missingids)) {
|
||||
throw new \invalid_parameter_exception(get_string('overridemissingdelete', 'quiz', implode(',', $missingids)));
|
||||
}
|
||||
|
||||
$this->delete_overrides($records, $shouldlog);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Builds sql and parameters to find overrides in quiz with the given ids
|
||||
*
|
||||
* @param int $quizid id of quiz
|
||||
* @param array $ids array of quiz override ids
|
||||
* @return array sql and params
|
||||
*/
|
||||
private static function get_override_in_sql(int $quizid, array $ids): array {
|
||||
global $DB;
|
||||
|
||||
[$insql, $inparams] = $DB->get_in_or_equal($ids, SQL_PARAMS_NAMED);
|
||||
$params = array_merge($inparams, ['quizid' => $quizid]);
|
||||
$sql = 'id ' . $insql . ' AND quiz = :quizid';
|
||||
return [$sql, $params];
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given overrides in the quiz linked to the override manager.
|
||||
* Note - capabilities are not checked, {@see require_manage_capability()}
|
||||
*
|
||||
* @param array $overrides override to delete. Must specify an id, quizid, and either a userid or groupid.
|
||||
* @param bool $shouldlog If true, will log a override_deleted event
|
||||
*/
|
||||
public function delete_overrides(array $overrides, bool $shouldlog = true): void {
|
||||
global $DB;
|
||||
|
||||
foreach ($overrides as $override) {
|
||||
if (empty($override->id)) {
|
||||
throw new \coding_exception("All overrides must specify an ID");
|
||||
}
|
||||
|
||||
// Sanity check that user xor group is specified.
|
||||
// User or group is required to clear the cache.
|
||||
self::ensure_userid_xor_groupid_set($override->userid ?? null, $override->groupid ?? null);
|
||||
}
|
||||
|
||||
if (empty($overrides)) {
|
||||
// Exit early, since delete select requires at least 1 record.
|
||||
return;
|
||||
}
|
||||
|
||||
// Match id and quiz.
|
||||
[$sql, $params] = self::get_override_in_sql($this->quiz->id, array_column($overrides, 'id'));
|
||||
$DB->delete_records_select('quiz_overrides', $sql, $params);
|
||||
|
||||
$cache = new override_cache($this->quiz->id);
|
||||
|
||||
// Perform other cleanup.
|
||||
foreach ($overrides as $override) {
|
||||
$userid = $override->userid ?? null;
|
||||
$groupid = $override->groupid ?? null;
|
||||
|
||||
$cache->clear_for($userid, $groupid);
|
||||
$this->delete_override_events($userid, $groupid);
|
||||
|
||||
if ($shouldlog) {
|
||||
$this->fire_deleted_event($override->id, $userid, $groupid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures either userid or groupid is set, but not both.
|
||||
* If neither or both are set, a coding exception is thrown.
|
||||
*
|
||||
* @param ?int $userid user for the record, or null
|
||||
* @param ?int $groupid group for the record, or null
|
||||
*/
|
||||
private static function ensure_userid_xor_groupid_set(?int $userid = null, ?int $groupid = null): void {
|
||||
$groupset = !empty($groupid);
|
||||
$userset = !empty($userid);
|
||||
|
||||
// If either set, but not both (xor).
|
||||
$xorset = $groupset ^ $userset;
|
||||
|
||||
if (!$xorset) {
|
||||
throw new \coding_exception("Either userid or groupid must be specified, but not both.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the events associated with the override.
|
||||
*
|
||||
* @param ?int $userid or null if groupid is specified
|
||||
* @param ?int $groupid or null if the userid is specified
|
||||
*/
|
||||
private function delete_override_events(?int $userid = null, ?int $groupid = null): void {
|
||||
global $DB;
|
||||
|
||||
// Sanity check.
|
||||
self::ensure_userid_xor_groupid_set($userid, $groupid);
|
||||
|
||||
$eventssearchparams = ['modulename' => 'quiz', 'instance' => $this->quiz->id];
|
||||
|
||||
if (!empty($userid)) {
|
||||
$eventssearchparams['userid'] = $userid;
|
||||
}
|
||||
|
||||
if (!empty($groupid)) {
|
||||
$eventssearchparams['groupid'] = $groupid;
|
||||
}
|
||||
|
||||
$events = $DB->get_records('event', $eventssearchparams);
|
||||
foreach ($events as $event) {
|
||||
$eventold = \calendar_event::load($event);
|
||||
$eventold->delete();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Requires the user has the override management capability
|
||||
*/
|
||||
public function require_manage_capability(): void {
|
||||
require_capability('mod/quiz:manageoverrides', $this->context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Requires the user has the override viewing capability
|
||||
*/
|
||||
public function require_read_capability(): void {
|
||||
// If user can manage, they can also view.
|
||||
// It would not make sense to be able to create and edit overrides without being able to view them.
|
||||
if (!has_any_capability(['mod/quiz:viewoverrides', 'mod/quiz:manageoverrides'], $this->context)) {
|
||||
throw new \required_capability_exception($this->context, 'mod/quiz:viewoverrides', 'nopermissions', '');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds common event data
|
||||
*
|
||||
* @param int $id override id
|
||||
* @return array of data to add as parameters to an event.
|
||||
*/
|
||||
private function get_base_event_params(int $id): array {
|
||||
return [
|
||||
'context' => $this->context,
|
||||
'other' => [
|
||||
'quizid' => $this->quiz->id,
|
||||
],
|
||||
'objectid' => $id,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Log that a given override was deleted
|
||||
*
|
||||
* @param int $id of quiz override that was just deleted
|
||||
* @param ?int $userid user attached to override record, or null
|
||||
* @param ?int $groupid group attached to override record, or null
|
||||
*/
|
||||
private function fire_deleted_event(int $id, ?int $userid = null, ?int $groupid = null): void {
|
||||
// Sanity check.
|
||||
self::ensure_userid_xor_groupid_set($userid, $groupid);
|
||||
|
||||
$params = $this->get_base_event_params($id);
|
||||
$params['objectid'] = $id;
|
||||
|
||||
if (!empty($userid)) {
|
||||
$params['relateduserid'] = $userid;
|
||||
user_override_deleted::create($params)->trigger();
|
||||
}
|
||||
|
||||
if (!empty($groupid)) {
|
||||
$params['other']['groupid'] = $groupid;
|
||||
group_override_deleted::create($params)->trigger();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Log that a given override was created
|
||||
*
|
||||
* @param int $id of quiz override that was just created
|
||||
* @param ?int $userid user attached to override record, or null
|
||||
* @param ?int $groupid group attached to override record, or null
|
||||
*/
|
||||
private function fire_created_event(int $id, ?int $userid = null, ?int $groupid = null): void {
|
||||
// Sanity check.
|
||||
self::ensure_userid_xor_groupid_set($userid, $groupid);
|
||||
|
||||
$params = $this->get_base_event_params($id);
|
||||
|
||||
if (!empty($userid)) {
|
||||
$params['relateduserid'] = $userid;
|
||||
user_override_created::create($params)->trigger();
|
||||
}
|
||||
|
||||
if (!empty($groupid)) {
|
||||
$params['other']['groupid'] = $groupid;
|
||||
group_override_created::create($params)->trigger();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log that a given override was updated
|
||||
*
|
||||
* @param int $id of quiz override that was just updated
|
||||
* @param ?int $userid user attached to override record, or null
|
||||
* @param ?int $groupid group attached to override record, or null
|
||||
*/
|
||||
private function fire_updated_event(int $id, ?int $userid = null, ?int $groupid = null): void {
|
||||
// Sanity check.
|
||||
self::ensure_userid_xor_groupid_set($userid, $groupid);
|
||||
|
||||
$params = $this->get_base_event_params($id);
|
||||
|
||||
if (!empty($userid)) {
|
||||
$params['relateduserid'] = $userid;
|
||||
user_override_updated::create($params)->trigger();
|
||||
}
|
||||
|
||||
if (!empty($groupid)) {
|
||||
$params['other']['groupid'] = $groupid;
|
||||
group_override_updated::create($params)->trigger();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears any overrideable settings in the formdata, where the value matches what is already in the quiz
|
||||
* If they match, the data is set to null.
|
||||
*
|
||||
* @param array $formdata data usually from moodleform or webservice call.
|
||||
* @return array formdata with same values cleared
|
||||
*/
|
||||
private function clear_unused_values(array $formdata): array {
|
||||
foreach (self::OVERRIDEABLE_QUIZ_SETTINGS as $key) {
|
||||
// If the formdata is the same as the current quiz object data, clear it.
|
||||
if (isset($formdata[$key]) && $formdata[$key] == $this->quiz->$key) {
|
||||
$formdata[$key] = null;
|
||||
}
|
||||
|
||||
// Ensure these keys always are set (even if null).
|
||||
$formdata[$key] = $formdata[$key] ?? null;
|
||||
|
||||
// If the formdata is empty, set it to null.
|
||||
// This avoids putting 0, false, or '' into the DB since the override logic expects null.
|
||||
// Attempts is the exception, it can have a integer value of '0', so we use is_numeric instead.
|
||||
if ($key != 'attempts' && empty($formdata[$key])) {
|
||||
$formdata[$key] = null;
|
||||
}
|
||||
|
||||
if ($key == 'attempts' && !is_numeric($formdata[$key])) {
|
||||
$formdata[$key] = null;
|
||||
}
|
||||
}
|
||||
|
||||
return $formdata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes orphaned group overrides in a given course.
|
||||
* Note - permissions are not checked and events are not logged for performance reasons.
|
||||
*
|
||||
* @param int $courseid ID of course to delete orphaned group overrides in
|
||||
* @return array array of quizzes that had orphaned group overrides.
|
||||
*/
|
||||
public static function delete_orphaned_group_overrides_in_course(int $courseid): array {
|
||||
global $DB;
|
||||
|
||||
// It would be nice if we got the groupid that was deleted.
|
||||
// Instead, we just update all quizzes with orphaned group overrides.
|
||||
$sql = "SELECT o.id, o.quiz, o.groupid
|
||||
FROM {quiz_overrides} o
|
||||
JOIN {quiz} quiz ON quiz.id = o.quiz
|
||||
LEFT JOIN {groups} grp ON grp.id = o.groupid
|
||||
WHERE quiz.course = :courseid
|
||||
AND o.groupid IS NOT NULL
|
||||
AND grp.id IS NULL";
|
||||
$params = ['courseid' => $courseid];
|
||||
$records = $DB->get_records_sql($sql, $params);
|
||||
|
||||
$DB->delete_records_list('quiz_overrides', 'id', array_keys($records));
|
||||
|
||||
// Purge cache for each record.
|
||||
foreach ($records as $record) {
|
||||
$cache = new override_cache($record->quiz);
|
||||
$cache->clear_for_group($record->groupid);
|
||||
}
|
||||
return array_unique(array_column($records, 'quiz'));
|
||||
}
|
||||
}
|
@ -365,8 +365,8 @@ class provider implements
|
||||
[$quizobj]
|
||||
);
|
||||
|
||||
// Delete all overrides - do not log.
|
||||
quiz_delete_all_overrides($quiz, false);
|
||||
// Delete all overrides.
|
||||
$quizobj->get_override_manager()->delete_all_overrides(shouldlog: false);
|
||||
|
||||
// This will delete all question attempts, quiz attempts, and quiz grades for this quiz.
|
||||
quiz_delete_all_attempts($quiz);
|
||||
@ -411,9 +411,11 @@ class provider implements
|
||||
'userid' => $user->id,
|
||||
]);
|
||||
|
||||
foreach ($overrides as $override) {
|
||||
quiz_delete_override($quiz, $override->id, false);
|
||||
}
|
||||
$manager = $quizobj->get_override_manager();
|
||||
$manager->delete_overrides(
|
||||
overrides: $overrides,
|
||||
shouldlog: false,
|
||||
);
|
||||
|
||||
// This will delete all question attempts, quiz attempts, and quiz grades for this quiz.
|
||||
quiz_delete_user_attempts($quizobj, $user);
|
||||
@ -461,9 +463,11 @@ class provider implements
|
||||
'userid' => $userid,
|
||||
]);
|
||||
|
||||
foreach ($overrides as $override) {
|
||||
quiz_delete_override($quiz, $override->id, false);
|
||||
}
|
||||
$manager = $quizobj->get_override_manager();
|
||||
$manager->delete_overrides(
|
||||
overrides: $overrides,
|
||||
shouldlog: false,
|
||||
);
|
||||
|
||||
// This will delete all question attempts, quiz attempts, and quiz grades for this user in the given quiz.
|
||||
quiz_delete_user_attempts($quizobj, (object)['id' => $userid]);
|
||||
|
@ -622,4 +622,16 @@ class quiz_settings {
|
||||
|
||||
return $questiontypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an override manager instance with context and quiz loaded.
|
||||
*
|
||||
* @return \mod_quiz\local\override_manager
|
||||
*/
|
||||
public function get_override_manager(): \mod_quiz\local\override_manager {
|
||||
return new \mod_quiz\local\override_manager(
|
||||
quiz: $this->quiz,
|
||||
context: $this->context
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -31,5 +31,8 @@ $definitions = [
|
||||
'mode' => cache_store::MODE_APPLICATION,
|
||||
'simplekeys' => true,
|
||||
'datasource' => '\mod_quiz\cache\overrides',
|
||||
'invalidationevents' => [
|
||||
\mod_quiz\local\override_cache::INVALIDATION_USERDATARESET,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
@ -311,3 +311,37 @@ function quiz_calculate_best_attempt($quiz, $attempts) {
|
||||
return $maxattempt;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a quiz override from the database and clears any corresponding calendar events
|
||||
*
|
||||
* @deprecated since Moodle 4.4
|
||||
* @todo MDL-80944 Final deprecation in Moodle 4.8
|
||||
* @param stdClass $quiz The quiz object.
|
||||
* @param int $overrideid The id of the override being deleted
|
||||
* @param bool $log Whether to trigger logs.
|
||||
* @return bool true on success
|
||||
*/
|
||||
function quiz_delete_override($quiz, $overrideid, $log = true) {
|
||||
debugging('quiz_delete_override is deprecated. Please use override_manager::delete_override_by_id instead.', DEBUG_DEVELOPER);
|
||||
$quizsettings = quiz_settings::create($quiz->id);
|
||||
$quizsettings->get_override_manager()->delete_overrides_by_id(
|
||||
ids: [$overrideid],
|
||||
shouldlog: $log,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all quiz overrides from the database and clears any corresponding calendar events
|
||||
*
|
||||
* @deprecated since Moodle 4.4
|
||||
* @todo MDL-80944 Final deprecation in Moodle 4.8
|
||||
* @param stdClass $quiz The quiz object.
|
||||
* @param bool $log Whether to trigger logs.
|
||||
*/
|
||||
function quiz_delete_all_overrides($quiz, $log = true) {
|
||||
debugging('quiz_delete_all_overrides is deprecated. Please use override_manager::delete_all_overrides instead.',
|
||||
DEBUG_DEVELOPER);
|
||||
$quizsettings = quiz_settings::create($quiz->id);
|
||||
$quizsettings->get_override_manager()->delete_all_overrides(shouldlog: $log);
|
||||
}
|
||||
|
@ -176,7 +176,7 @@ $string['categorynoedit'] = 'You do not have editing privileges in the category
|
||||
$string['categoryupdated'] = 'The category was successfully updated';
|
||||
$string['close'] = 'Close window';
|
||||
$string['closed'] = 'Closed';
|
||||
$string['closebeforeopen'] = 'Could not update the quiz. You have specified a close date before the open date.';
|
||||
$string['closebeforeopen'] = 'The close date cannot be before or equal to the open date.';
|
||||
$string['closepreview'] = 'Close preview';
|
||||
$string['closereview'] = 'Close review';
|
||||
$string['comment'] = 'Comment';
|
||||
@ -650,10 +650,21 @@ $string['overduehandlinggraceperiod'] = 'There is a grace period when open attem
|
||||
$string['overduehandlingautoabandon'] = 'Attempts must be submitted before time expires, or they are not counted';
|
||||
$string['overduemustbesubmittedby'] = 'This attempt is now overdue. It should already have been submitted. If you would like this quiz to be graded, you must submit it by {$a}. If you do not submit it by then, no marks from this attempt will be counted.';
|
||||
$string['override'] = 'Override';
|
||||
$string['overridecannotchange'] = 'The user or group cannot be changed after an override is created';
|
||||
$string['overridecannotsetbothgroupanduser'] = 'Both group and user cannot be set at the same time';
|
||||
$string['overridedeletegroupsure'] = 'Are you sure you want to delete the override for group {$a}?';
|
||||
$string['overridedeleteusersure'] = 'Are you sure you want to delete the override for user {$a}?';
|
||||
$string['overridegroup'] = 'Override group';
|
||||
$string['overridegroupeventname'] = '{$a->quiz} - {$a->group}';
|
||||
$string['overrideinvalidattempts'] = 'Attempts value must be greater than zero';
|
||||
$string['overrideinvalidexistingid'] = 'Existing override does not exist';
|
||||
$string['overrideinvalidgroup'] = 'Group given does not exist';
|
||||
$string['overrideinvalidquiz'] = 'Quiz id set does not exist';
|
||||
$string['overrideinvalidtimelimit'] = 'Time limit must be greater than zero';
|
||||
$string['overrideinvaliduser'] = 'User given does not exist';
|
||||
$string['overridemissingdelete'] = 'Override id(s) {$a} could not be deleted because they do not exist or are not a part of the given quiz';
|
||||
$string['overridemultiplerecordsexist'] = 'Multiple overrides cannot be made for the same user/group';
|
||||
$string['overridemustsetuserorgroup'] = 'A user or group must be set';
|
||||
$string['overrides'] = 'Overrides';
|
||||
$string['overridesforquiz'] = 'Settings overrides: {$a}';
|
||||
$string['overridesnoneforgroups'] = 'No group settings overrides have been created for this quiz.';
|
||||
|
@ -38,6 +38,7 @@ use mod_quiz\question\display_options;
|
||||
use mod_quiz\question\qubaids_for_quiz;
|
||||
use mod_quiz\question\qubaids_for_users_attempts;
|
||||
use core_question\statistics\questions\all_calculated_for_qubaid_condition;
|
||||
use mod_quiz\local\override_cache;
|
||||
use mod_quiz\quiz_attempt;
|
||||
use mod_quiz\quiz_settings;
|
||||
|
||||
@ -190,7 +191,11 @@ function quiz_delete_instance($id) {
|
||||
$quiz = $DB->get_record('quiz', ['id' => $id], '*', MUST_EXIST);
|
||||
|
||||
quiz_delete_all_attempts($quiz);
|
||||
quiz_delete_all_overrides($quiz);
|
||||
|
||||
// Delete all overrides, and for performance do not log or check permissions.
|
||||
$quizobj = quiz_settings::create($quiz->id);
|
||||
$quizobj->get_override_manager()->delete_all_overrides(shouldlog: false);
|
||||
|
||||
quiz_delete_references($quiz->id);
|
||||
|
||||
// We need to do the following deletes before we try and delete randoms, otherwise they would still be 'in use'.
|
||||
@ -214,86 +219,6 @@ function quiz_delete_instance($id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a quiz override from the database and clears any corresponding calendar events
|
||||
*
|
||||
* @param stdClass $quiz The quiz object.
|
||||
* @param int $overrideid The id of the override being deleted
|
||||
* @param bool $log Whether to trigger logs.
|
||||
* @return bool true on success
|
||||
*/
|
||||
function quiz_delete_override($quiz, $overrideid, $log = true) {
|
||||
global $DB;
|
||||
|
||||
if (!isset($quiz->cmid)) {
|
||||
$cm = get_coursemodule_from_instance('quiz', $quiz->id, $quiz->course);
|
||||
$quiz->cmid = $cm->id;
|
||||
}
|
||||
|
||||
$override = $DB->get_record('quiz_overrides', ['id' => $overrideid], '*', MUST_EXIST);
|
||||
|
||||
// Delete the events.
|
||||
if (isset($override->groupid)) {
|
||||
// Create the search array for a group override.
|
||||
$eventsearcharray = ['modulename' => 'quiz',
|
||||
'instance' => $quiz->id, 'groupid' => (int)$override->groupid];
|
||||
$cachekey = "{$quiz->id}_g_{$override->groupid}";
|
||||
} else {
|
||||
// Create the search array for a user override.
|
||||
$eventsearcharray = ['modulename' => 'quiz',
|
||||
'instance' => $quiz->id, 'userid' => (int)$override->userid];
|
||||
$cachekey = "{$quiz->id}_u_{$override->userid}";
|
||||
}
|
||||
$events = $DB->get_records('event', $eventsearcharray);
|
||||
foreach ($events as $event) {
|
||||
$eventold = calendar_event::load($event);
|
||||
$eventold->delete();
|
||||
}
|
||||
|
||||
$DB->delete_records('quiz_overrides', ['id' => $overrideid]);
|
||||
cache::make('mod_quiz', 'overrides')->delete($cachekey);
|
||||
|
||||
if ($log) {
|
||||
// Set the common parameters for one of the events we will be triggering.
|
||||
$params = [
|
||||
'objectid' => $override->id,
|
||||
'context' => context_module::instance($quiz->cmid),
|
||||
'other' => [
|
||||
'quizid' => $override->quiz
|
||||
]
|
||||
];
|
||||
// Determine which override deleted event to fire.
|
||||
if (!empty($override->userid)) {
|
||||
$params['relateduserid'] = $override->userid;
|
||||
$event = \mod_quiz\event\user_override_deleted::create($params);
|
||||
} else {
|
||||
$params['other']['groupid'] = $override->groupid;
|
||||
$event = \mod_quiz\event\group_override_deleted::create($params);
|
||||
}
|
||||
|
||||
// Trigger the override deleted event.
|
||||
$event->add_record_snapshot('quiz_overrides', $override);
|
||||
$event->trigger();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all quiz overrides from the database and clears any corresponding calendar events
|
||||
*
|
||||
* @param stdClass $quiz The quiz object.
|
||||
* @param bool $log Whether to trigger logs.
|
||||
*/
|
||||
function quiz_delete_all_overrides($quiz, $log = true) {
|
||||
global $DB;
|
||||
|
||||
$overrides = $DB->get_records('quiz_overrides', ['quiz' => $quiz->id], 'id');
|
||||
foreach ($overrides as $override) {
|
||||
quiz_delete_override($quiz, $override->id, $log);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a quiz object with override information for a user.
|
||||
*
|
||||
@ -1620,7 +1545,7 @@ function quiz_reset_userdata($data) {
|
||||
}
|
||||
|
||||
if ($purgeoverrides) {
|
||||
cache::make('mod_quiz', 'overrides')->purge();
|
||||
\cache_helper::purge_by_event(\mod_quiz\local\override_cache::INVALIDATION_USERDATARESET);
|
||||
}
|
||||
|
||||
return $status;
|
||||
@ -2158,8 +2083,8 @@ function quiz_get_coursemodule_info($coursemodule) {
|
||||
function mod_quiz_cm_info_dynamic(cm_info $cm) {
|
||||
global $USER;
|
||||
|
||||
$cache = cache::make('mod_quiz', 'overrides');
|
||||
$override = $cache->get("{$cm->instance}_u_{$USER->id}");
|
||||
$cache = new override_cache($cm->instance);
|
||||
$override = $cache->get_cached_user_override($USER->id);
|
||||
|
||||
if (!$override) {
|
||||
$override = (object) [
|
||||
@ -2174,7 +2099,7 @@ function mod_quiz_cm_info_dynamic(cm_info $cm) {
|
||||
$closes = [];
|
||||
$groupings = groups_get_user_groups($cm->course, $USER->id);
|
||||
foreach ($groupings[0] as $groupid) {
|
||||
$groupoverride = $cache->get("{$cm->instance}_g_{$groupid}");
|
||||
$groupoverride = $cache->get_cached_group_override($groupid);
|
||||
if (isset($groupoverride->timeopen)) {
|
||||
$opens[] = $groupoverride->timeopen;
|
||||
}
|
||||
|
@ -39,6 +39,7 @@ use core_question\local\bank\condition;
|
||||
use mod_quiz\access_manager;
|
||||
use mod_quiz\event\attempt_submitted;
|
||||
use mod_quiz\grade_calculator;
|
||||
use mod_quiz\local\override_manager;
|
||||
use mod_quiz\question\bank\qbank_helper;
|
||||
use mod_quiz\question\display_options;
|
||||
use mod_quiz\quiz_attempt;
|
||||
@ -1585,28 +1586,11 @@ function quiz_send_notify_manual_graded_message(quiz_attempt $attemptobj, object
|
||||
* @return void
|
||||
*/
|
||||
function quiz_process_group_deleted_in_course($courseid) {
|
||||
global $DB;
|
||||
$affectedquizzes = override_manager::delete_orphaned_group_overrides_in_course($courseid);
|
||||
|
||||
// It would be nice if we got the groupid that was deleted.
|
||||
// Instead, we just update all quizzes with orphaned group overrides.
|
||||
$sql = "SELECT o.id, o.quiz, o.groupid
|
||||
FROM {quiz_overrides} o
|
||||
JOIN {quiz} quiz ON quiz.id = o.quiz
|
||||
LEFT JOIN {groups} grp ON grp.id = o.groupid
|
||||
WHERE quiz.course = :courseid
|
||||
AND o.groupid IS NOT NULL
|
||||
AND grp.id IS NULL";
|
||||
$params = ['courseid' => $courseid];
|
||||
$records = $DB->get_records_sql($sql, $params);
|
||||
if (!$records) {
|
||||
return; // Nothing to do.
|
||||
if (!empty($affectedquizzes)) {
|
||||
quiz_update_open_attempts(['quizid' => $affectedquizzes]);
|
||||
}
|
||||
$DB->delete_records_list('quiz_overrides', 'id', array_keys($records));
|
||||
$cache = cache::make('mod_quiz', 'overrides');
|
||||
foreach ($records as $record) {
|
||||
$cache->delete("{$record->quiz}_g_{$record->groupid}");
|
||||
}
|
||||
quiz_update_open_attempts(['quizid' => array_unique(array_column($records, 'quiz'))]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -38,11 +38,12 @@ $quiz = $quizobj->get_quiz();
|
||||
$cm = $quizobj->get_cm();
|
||||
$course = $quizobj->get_course();
|
||||
$context = $quizobj->get_context();
|
||||
$manager = $quizobj->get_override_manager();
|
||||
|
||||
require_login($course, false, $cm);
|
||||
|
||||
// Check the user has the required capabilities to modify an override.
|
||||
require_capability('mod/quiz:manageoverrides', $context);
|
||||
$manager->require_manage_capability();
|
||||
|
||||
if ($override->groupid) {
|
||||
if (!groups_group_visible($override->groupid, $course, $cm)) {
|
||||
@ -65,11 +66,7 @@ if (!empty($override->userid)) {
|
||||
// If confirm is set (PARAM_BOOL) then we have confirmation of intention to delete.
|
||||
if ($confirm) {
|
||||
require_sesskey();
|
||||
|
||||
// Set the course module id before calling quiz_delete_override().
|
||||
$quiz->cmid = $cm->id;
|
||||
quiz_delete_override($quiz, $override->id);
|
||||
|
||||
$manager->delete_overrides(overrides: [$override]);
|
||||
redirect($cancelurl);
|
||||
}
|
||||
|
||||
|
@ -46,6 +46,7 @@ $quiz = $quizobj->get_quiz();
|
||||
$cm = $quizobj->get_cm();
|
||||
$course = $quizobj->get_course();
|
||||
$context = $quizobj->get_context();
|
||||
$manager = $quizobj->get_override_manager();
|
||||
|
||||
$url = new moodle_url('/mod/quiz/overrideedit.php');
|
||||
if ($action) {
|
||||
@ -65,7 +66,7 @@ $PAGE->set_secondary_active_tab("mod_quiz_useroverrides");
|
||||
require_login($course, false, $cm);
|
||||
|
||||
// Add or edit an override.
|
||||
require_capability('mod/quiz:manageoverrides', $context);
|
||||
$manager->require_manage_capability();
|
||||
|
||||
if ($overrideid) {
|
||||
// Editing an override.
|
||||
@ -121,98 +122,13 @@ if ($mform->is_cancelled()) {
|
||||
redirect($url);
|
||||
|
||||
} else if ($fromform = $mform->get_data()) {
|
||||
// Only include id when editing (i.e. action is empty).
|
||||
if (empty($action) && !empty($overrideid)) {
|
||||
$fromform->id = $overrideid;
|
||||
}
|
||||
|
||||
// Process the data.
|
||||
$fromform->quiz = $quiz->id;
|
||||
|
||||
// Replace unchanged values with null.
|
||||
foreach ($keys as $key) {
|
||||
if ($fromform->{$key} == $quiz->{$key}) {
|
||||
$fromform->{$key} = null;
|
||||
}
|
||||
}
|
||||
|
||||
// See if we are replacing an existing override.
|
||||
$userorgroupchanged = false;
|
||||
if (empty($override->id)) {
|
||||
$userorgroupchanged = true;
|
||||
} else if (!empty($fromform->userid)) {
|
||||
$userorgroupchanged = $fromform->userid !== $override->userid;
|
||||
} else {
|
||||
$userorgroupchanged = $fromform->groupid !== $override->groupid;
|
||||
}
|
||||
|
||||
if ($userorgroupchanged) {
|
||||
$conditions = [
|
||||
'quiz' => $quiz->id,
|
||||
'userid' => empty($fromform->userid) ? null : $fromform->userid,
|
||||
'groupid' => empty($fromform->groupid) ? null : $fromform->groupid];
|
||||
if ($oldoverride = $DB->get_record('quiz_overrides', $conditions)) {
|
||||
// There is an old override, so we merge any new settings on top of
|
||||
// the older override.
|
||||
foreach ($keys as $key) {
|
||||
if (is_null($fromform->{$key})) {
|
||||
$fromform->{$key} = $oldoverride->{$key};
|
||||
}
|
||||
}
|
||||
// Set the course module id before calling quiz_delete_override().
|
||||
$quiz->cmid = $cm->id;
|
||||
quiz_delete_override($quiz, $oldoverride->id);
|
||||
}
|
||||
}
|
||||
|
||||
// Set the common parameters for one of the events we may be triggering.
|
||||
$params = [
|
||||
'context' => $context,
|
||||
'other' => [
|
||||
'quizid' => $quiz->id
|
||||
]
|
||||
];
|
||||
if (!empty($override->id)) {
|
||||
$fromform->id = $override->id;
|
||||
$DB->update_record('quiz_overrides', $fromform);
|
||||
$cachekey = $groupmode ? "{$fromform->quiz}_g_{$fromform->groupid}" : "{$fromform->quiz}_u_{$fromform->userid}";
|
||||
cache::make('mod_quiz', 'overrides')->delete($cachekey);
|
||||
|
||||
// Determine which override updated event to fire.
|
||||
$params['objectid'] = $override->id;
|
||||
if (!$groupmode) {
|
||||
$params['relateduserid'] = $fromform->userid;
|
||||
$event = \mod_quiz\event\user_override_updated::create($params);
|
||||
} else {
|
||||
$params['other']['groupid'] = $fromform->groupid;
|
||||
$event = \mod_quiz\event\group_override_updated::create($params);
|
||||
}
|
||||
|
||||
// Trigger the override updated event.
|
||||
$event->trigger();
|
||||
} else {
|
||||
unset($fromform->id);
|
||||
$fromform->id = $DB->insert_record('quiz_overrides', $fromform);
|
||||
$cachekey = $groupmode ? "{$fromform->quiz}_g_{$fromform->groupid}" : "{$fromform->quiz}_u_{$fromform->userid}";
|
||||
cache::make('mod_quiz', 'overrides')->delete($cachekey);
|
||||
|
||||
// Determine which override created event to fire.
|
||||
$params['objectid'] = $fromform->id;
|
||||
if (!$groupmode) {
|
||||
$params['relateduserid'] = $fromform->userid;
|
||||
$event = \mod_quiz\event\user_override_created::create($params);
|
||||
} else {
|
||||
$params['other']['groupid'] = $fromform->groupid;
|
||||
$event = \mod_quiz\event\group_override_created::create($params);
|
||||
}
|
||||
|
||||
// Trigger the override created event.
|
||||
$event->trigger();
|
||||
}
|
||||
|
||||
quiz_update_open_attempts(['quizid' => $quiz->id]);
|
||||
if ($groupmode) {
|
||||
// Priorities may have shifted, so we need to update all of the calendar events for group overrides.
|
||||
quiz_update_events($quiz);
|
||||
} else {
|
||||
// User override. We only need to update the calendar event for this user override.
|
||||
quiz_update_events($quiz, $fromform);
|
||||
}
|
||||
$id = $manager->save_override((array) $fromform);
|
||||
|
||||
if (!empty($fromform->submitbutton)) {
|
||||
redirect($overridelisturl);
|
||||
@ -221,7 +137,7 @@ if ($mform->is_cancelled()) {
|
||||
// The user pressed the 'again' button, so redirect back to this page.
|
||||
$url->remove_params('cmid');
|
||||
$url->param('action', 'duplicate');
|
||||
$url->param('id', $fromform->id);
|
||||
$url->param('id', $id);
|
||||
redirect($url);
|
||||
|
||||
}
|
||||
|
@ -630,6 +630,7 @@ class events_test extends \advanced_testcase {
|
||||
$this->setAdminUser();
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
$quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id]);
|
||||
$quizsettings = quiz_settings::create($quiz->id);
|
||||
|
||||
// Create an override.
|
||||
$override = new \stdClass();
|
||||
@ -639,7 +640,7 @@ class events_test extends \advanced_testcase {
|
||||
|
||||
// Trigger and capture the event.
|
||||
$sink = $this->redirectEvents();
|
||||
quiz_delete_override($quiz, $override->id);
|
||||
$quizsettings->get_override_manager()->delete_overrides(overrides: [$override]);
|
||||
$events = $sink->get_events();
|
||||
$event = reset($events);
|
||||
|
||||
@ -660,6 +661,7 @@ class events_test extends \advanced_testcase {
|
||||
$this->setAdminUser();
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
$quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id]);
|
||||
$quizsettings = quiz_settings::create($quiz->id);
|
||||
|
||||
// Create an override.
|
||||
$override = new \stdClass();
|
||||
@ -669,7 +671,7 @@ class events_test extends \advanced_testcase {
|
||||
|
||||
// Trigger and capture the event.
|
||||
$sink = $this->redirectEvents();
|
||||
quiz_delete_override($quiz, $override->id);
|
||||
$quizsettings->get_override_manager()->delete_overrides(overrides: [$override]);
|
||||
$events = $sink->get_events();
|
||||
$event = reset($events);
|
||||
|
||||
|
86
mod/quiz/tests/override_cache_test.php
Normal file
86
mod/quiz/tests/override_cache_test.php
Normal file
@ -0,0 +1,86 @@
|
||||
<?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/>.
|
||||
|
||||
namespace mod_quiz;
|
||||
|
||||
use mod_quiz\local\override_cache;
|
||||
|
||||
/**
|
||||
* Cache manager tests for quiz overrides
|
||||
*
|
||||
* @package mod_quiz
|
||||
* @copyright 2024 Matthew Hilton <matthewhilton@catalyst-au.net>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @covers \mod_quiz\local\override_cache
|
||||
*/
|
||||
final class override_cache_test extends \advanced_testcase {
|
||||
|
||||
/**
|
||||
* Tests CRUD functions of the override_cache
|
||||
*/
|
||||
public function test_crud(): void {
|
||||
// Cache is normally protected, but for testing we reflect it and put test data into it.
|
||||
$overridecache = new override_cache(0);
|
||||
$reflection = new \ReflectionClass($overridecache);
|
||||
|
||||
$getcache = $reflection->getMethod('get_cache');
|
||||
$getcache->setAccessible(true);
|
||||
$cache = $getcache->invoke($overridecache);
|
||||
|
||||
$getuserkey = $reflection->getMethod('get_user_cache_key');
|
||||
$getuserkey->setAccessible(true);
|
||||
|
||||
$getgroupkey = $reflection->getMethod('get_group_cache_key');
|
||||
$getgroupkey->setAccessible(true);
|
||||
|
||||
$dummydata = (object)[
|
||||
'userid' => 1234,
|
||||
];
|
||||
|
||||
// Set some data.
|
||||
$cache->set($getuserkey->invoke($overridecache, 123), $dummydata);
|
||||
$cache->set($getgroupkey->invoke($overridecache, 456), $dummydata);
|
||||
|
||||
// Get the data back.
|
||||
$this->assertEquals($dummydata, $overridecache->get_cached_user_override(123));
|
||||
$this->assertEquals($dummydata, $overridecache->get_cached_group_override(456));
|
||||
|
||||
// Delete.
|
||||
$overridecache->clear_for_user(123);
|
||||
$overridecache->clear_for_group(456);
|
||||
|
||||
$this->assertEmpty($overridecache->get_cached_user_override(123));
|
||||
$this->assertEmpty($overridecache->get_cached_group_override(456));
|
||||
|
||||
// Put some data back.
|
||||
$cache->set($getuserkey->invoke($overridecache, 123), $dummydata);
|
||||
$cache->set($getgroupkey->invoke($overridecache, 456), $dummydata);
|
||||
|
||||
// Clear it.
|
||||
$overridecache->clear_for(123, 456);
|
||||
$this->assertEmpty($overridecache->get_cached_user_override(123));
|
||||
$this->assertEmpty($overridecache->get_cached_group_override(456));
|
||||
|
||||
// Put some data back.
|
||||
$cache->set($getuserkey->invoke($overridecache, 123), 'testuser');
|
||||
$cache->set($getgroupkey->invoke($overridecache, 456), 'testgroup');
|
||||
|
||||
// Purge it.
|
||||
\cache_helper::purge_by_event(\mod_quiz\local\override_cache::INVALIDATION_USERDATARESET);
|
||||
$this->assertEmpty($overridecache->get_cached_user_override(123));
|
||||
$this->assertEmpty($overridecache->get_cached_group_override(456));
|
||||
}
|
||||
}
|
1071
mod/quiz/tests/override_manager_test.php
Normal file
1071
mod/quiz/tests/override_manager_test.php
Normal file
File diff suppressed because it is too large
Load Diff
@ -535,7 +535,7 @@ class provider_test extends \core_privacy\tests\provider_testcase {
|
||||
// Delete the data for user1 and user3 in course1 and check it is removed.
|
||||
$quiz1context = $quiz1obj->get_context();
|
||||
$approveduserlist = new \core_privacy\local\request\approved_userlist($quiz1context, 'mod_quiz',
|
||||
[$user1->id, $user3->id]);
|
||||
[$user1->id, $user3->id]);
|
||||
provider::delete_data_for_users($approveduserlist);
|
||||
|
||||
// Only the attempt of user2 should be remained in quiz1.
|
||||
|
@ -22,6 +22,12 @@ This file describes API changes in the quiz code.
|
||||
* The following previously deprecated methods have been removed and can no longer be used:
|
||||
- `get_slot_tags_for_slot_id`
|
||||
- `quiz_retrieve_tags_for_slot_ids`
|
||||
* Quiz override logic has been refactored and put in a new override_manager class.
|
||||
* Quiz override cache should now be accessed using the new override_cache class.
|
||||
* The quiz_delete_overrides and quiz_delete_all_overrides functions are now deprecated. Please instead use:
|
||||
- override_manager::delete_override_by_id
|
||||
- override_manager::delete_overrides
|
||||
- override_manager::delete_all_overrides
|
||||
|
||||
=== 4.3 ===
|
||||
|
||||
|
@ -24,6 +24,6 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$plugin->version = 2023112300;
|
||||
$plugin->version = 2023112303;
|
||||
$plugin->requires = 2023100400;
|
||||
$plugin->component = 'mod_quiz';
|
||||
|
Loading…
x
Reference in New Issue
Block a user