MDL-86493 mod_quiz: Change overrides cache from O(n*m) to O(n)
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
issueNumber: MDL-86493
|
||||
notes:
|
||||
mod_quiz:
|
||||
- message: >-
|
||||
The quiz overrides cache implementation has been replaced with a faster alternative with a different API.
|
||||
This should be a transparent change but any direct references will still need to be updated.
|
||||
type: deprecated
|
||||
- message: >
|
||||
`mod_quiz_cm_info_dynamic()` now uses the new `quiz_overrides` cache via `override_manager`, performing a single cache fetch per quiz/user.
|
||||
This significantly reduces cache calls on course pages with many quizzes and groups.
|
||||
|
||||
The new `mod_quiz:quiz_overrides` cache is keyed by `quizid_userid` using datasource `\mod_quiz\cache\quiz_overrides_cache`.
|
||||
This cache returns all applicable overrides for a user in a quiz (the user override, if any, plus all group overrides for groups they belong to in the quiz's course).
|
||||
|
||||
New class `\mod_quiz\local\quiz_overrides_cache_manager` to interact with the cache:
|
||||
- `get_overrides(int $quizid, int $userid): array`
|
||||
- `purge_for_user(int $quizid, int $userid): void`
|
||||
- `purge_for_users(int $quizid, array $userids): void`
|
||||
- `purge_for_group(int $quizid, int $groupid): void`
|
||||
- `purge_for_group_members(int $groupid, array $userids): void`
|
||||
|
||||
Hook callbacks in `db/hooks.php` to keep the cache in sync with group
|
||||
membership changes:
|
||||
- `\core_group\hook\after_group_membership_added`
|
||||
- `\core_group\hook\after_group_membership_removed`
|
||||
type: improved
|
||||
-115
@@ -1,115 +0,0 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Cache data source for the quiz overrides.
|
||||
*
|
||||
* @package mod_quiz
|
||||
* @copyright 2021 Shamim Rezaie <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace mod_quiz\cache;
|
||||
|
||||
use core_cache\data_source_interface;
|
||||
use core_cache\definition;
|
||||
|
||||
/**
|
||||
* Class quiz_overrides
|
||||
*
|
||||
* @package mod_quiz
|
||||
* @copyright 2021 Shamim Rezaie <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class overrides implements data_source_interface {
|
||||
/** @var overrides the singleton instance of this class. */
|
||||
protected static $instance = null;
|
||||
|
||||
/**
|
||||
* Returns an instance of the data source class that the cache can use for loading data using the other methods
|
||||
* specified by this interface.
|
||||
*
|
||||
* @param definition $definition
|
||||
* @return overrides
|
||||
*/
|
||||
public static function get_instance_for_cache(definition $definition): overrides {
|
||||
if (is_null(self::$instance)) {
|
||||
self::$instance = new overrides();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the data for the key provided ready formatted for caching.
|
||||
*
|
||||
* @param string|int $key The key to load.
|
||||
* @return mixed What ever data should be returned, or false if it can't be loaded.
|
||||
* @throws \coding_exception
|
||||
*/
|
||||
public function load_for_cache($key) {
|
||||
global $DB;
|
||||
|
||||
// Ignore getting data if this is a cache invalidation - {@see \core_cache\helper::purge_by_event()}.
|
||||
if ($key == 'lastinvalidation') {
|
||||
return null;
|
||||
}
|
||||
|
||||
[$quizid, $ug, $ugid] = explode('_', $key);
|
||||
$quizid = (int) $quizid;
|
||||
|
||||
switch ($ug) {
|
||||
case 'u':
|
||||
$userid = (int) $ugid;
|
||||
$override = $DB->get_record(
|
||||
'quiz_overrides',
|
||||
['quiz' => $quizid, 'userid' => $userid],
|
||||
'timeopen, timeclose, timelimit, attempts, password'
|
||||
);
|
||||
break;
|
||||
case 'g':
|
||||
$groupid = (int) $ugid;
|
||||
$override = $DB->get_record(
|
||||
'quiz_overrides',
|
||||
['quiz' => $quizid, 'groupid' => $groupid],
|
||||
'timeopen, timeclose, timelimit, attempts, password'
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new \coding_exception('Invalid cache key');
|
||||
}
|
||||
|
||||
// Return null instead of false, because false will not be cached.
|
||||
return $override ?: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads several keys for the cache.
|
||||
*
|
||||
* @param array $keys An array of keys each of which will be string|int.
|
||||
* @return array An array of matching data items.
|
||||
*/
|
||||
public function load_many_for_cache(array $keys) {
|
||||
$results = [];
|
||||
|
||||
foreach ($keys as $key) {
|
||||
$results[] = $this->load_for_cache($key);
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
// This file is part of Moodle - https://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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
namespace mod_quiz\cache;
|
||||
|
||||
use core_cache\data_source_interface;
|
||||
use core_cache\definition;
|
||||
|
||||
/**
|
||||
* Data source implementation for the new quiz_overrides cache.
|
||||
*
|
||||
* This loads all applicable overrides for a (quizid, userid) pair:
|
||||
* - The user override (if present)
|
||||
* - All group overrides for groups the user belongs to in the quiz's course
|
||||
*
|
||||
* @package mod_quiz
|
||||
* @copyright 2025 Catalyst IT Australia Pty Ltd
|
||||
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class quiz_overrides_cache implements data_source_interface {
|
||||
/** @var ?quiz_overrides_cache Singleton instance. */
|
||||
private static $instance = null;
|
||||
|
||||
#[\Override]
|
||||
public static function get_instance_for_cache(definition $definition): quiz_overrides_cache {
|
||||
return self::$instance ??= new quiz_overrides_cache();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function load_for_cache($key) {
|
||||
global $DB;
|
||||
|
||||
// Core cache invalidation asks datasources for this internal key.
|
||||
if ($key === 'lastinvalidation') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// All regular keys use the "{quizid}_{userid}" format.
|
||||
[$quizid, $userid] = self::split_cache_key((string) $key);
|
||||
|
||||
$subquery = "SELECT g.id
|
||||
FROM {groups} g
|
||||
JOIN {groups_members} gm ON gm.groupid = g.id
|
||||
JOIN {quiz} q ON q.course = g.courseid
|
||||
WHERE q.id = :subqueryquizid AND gm.userid = :subqueryuserid";
|
||||
|
||||
$sql = "SELECT *
|
||||
FROM {quiz_overrides}
|
||||
WHERE quiz = :quizid AND (userid = :userid OR groupid IN ($subquery))";
|
||||
|
||||
return $DB->get_records_sql($sql, [
|
||||
'quizid' => $quizid,
|
||||
'userid' => $userid,
|
||||
'subqueryquizid' => $quizid,
|
||||
'subqueryuserid' => $userid,
|
||||
]);
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function load_many_for_cache(array $keys) {
|
||||
$results = [];
|
||||
foreach ($keys as $key) {
|
||||
$results[$key] = $this->load_for_cache($key);
|
||||
}
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Split a cache key into its quizid and userid components.
|
||||
*
|
||||
* @param string $key The cache key.
|
||||
* @return array{0:int,1:int} An array with quizid and userid as integers.
|
||||
*/
|
||||
private static function split_cache_key(string $key): array {
|
||||
return array_map('intval', explode('_', $key));
|
||||
}
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
<?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 <[email protected]>
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,9 @@
|
||||
|
||||
namespace mod_quiz\local;
|
||||
|
||||
use context_course;
|
||||
use core_group\hook\after_group_membership_added;
|
||||
use core_group\hook\after_group_membership_removed;
|
||||
use mod_quiz\event\group_override_created;
|
||||
use mod_quiz\event\group_override_deleted;
|
||||
use mod_quiz\event\group_override_updated;
|
||||
@@ -260,8 +263,12 @@ class override_manager {
|
||||
$groupid = $datatoset['groupid'] ?? null;
|
||||
|
||||
// Clear the cache.
|
||||
$cache = new override_cache($this->quiz->id);
|
||||
$cache->clear_for($userid, $groupid);
|
||||
if (!empty($userid)) {
|
||||
quiz_overrides_cache_manager::purge_for_user($this->quiz->id, $userid);
|
||||
}
|
||||
if (!empty($groupid)) {
|
||||
quiz_overrides_cache_manager::purge_for_group($this->quiz->id, $groupid);
|
||||
}
|
||||
|
||||
// Trigger moodle events.
|
||||
if (empty($formdata['id'])) {
|
||||
@@ -293,7 +300,7 @@ class override_manager {
|
||||
*/
|
||||
public function delete_all_overrides(bool $shouldlog = true): void {
|
||||
global $DB;
|
||||
$overrides = $DB->get_records('quiz_overrides', ['quiz' => $this->quiz->id], '', 'id,userid,groupid');
|
||||
$overrides = $DB->get_records('quiz_overrides', ['quiz' => $this->quiz->id], '', 'id,quiz,userid,groupid');
|
||||
$this->delete_overrides($overrides, $shouldlog);
|
||||
}
|
||||
|
||||
@@ -313,7 +320,7 @@ class override_manager {
|
||||
// Filter for those overrides user can access.
|
||||
[$sql, $params] = self::get_override_in_sql($this->quiz->id, $ids);
|
||||
$records = array_filter(
|
||||
$DB->get_records_select('quiz_overrides', $sql, $params, '', 'id,userid,groupid'),
|
||||
$DB->get_records_select('quiz_overrides', $sql, $params, '', 'id,quiz,userid,groupid'),
|
||||
fn(\stdClass $override) => $this->can_view_override(
|
||||
$override,
|
||||
$quizsettings->get_course(),
|
||||
@@ -362,6 +369,14 @@ class override_manager {
|
||||
throw new \coding_exception("All overrides must specify an ID");
|
||||
}
|
||||
|
||||
if (empty($override->quiz)) {
|
||||
throw new \coding_exception("All overrides must specify a quiz ID");
|
||||
}
|
||||
|
||||
if ($override->quiz != $this->quiz->id) {
|
||||
throw new \coding_exception("All overrides must belong to the quiz linked to this manager");
|
||||
}
|
||||
|
||||
// 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);
|
||||
@@ -376,14 +391,12 @@ class override_manager {
|
||||
[$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.
|
||||
quiz_overrides_cache_manager::purge_for_overrides($overrides);
|
||||
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) {
|
||||
@@ -596,6 +609,57 @@ class override_manager {
|
||||
return $formdata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the effective overridden open/close times for a user for a given quiz.
|
||||
*
|
||||
* @param int $quizid The quiz ID.
|
||||
* @param int $userid The user ID.
|
||||
* @return array Array with optional keys 'timeopen' and 'timeclose'.
|
||||
*/
|
||||
public static function get_effective_open_close_times(int $quizid, int $userid): array {
|
||||
$overrides = quiz_overrides_cache_manager::get_overrides($quizid, $userid);
|
||||
|
||||
if (empty($overrides)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get user override (there should be at most one per user per quiz).
|
||||
$useroverride = array_filter($overrides, fn($o): bool => !empty($o->userid));
|
||||
$useroverride = reset($useroverride);
|
||||
|
||||
$timeopen = empty($useroverride) ? null : $useroverride->timeopen;
|
||||
$timeclose = empty($useroverride) ? null : $useroverride->timeclose;
|
||||
|
||||
// If either value is still null, check group overrides.
|
||||
if ($timeopen === null || $timeclose === null) {
|
||||
$groupoverrides = array_filter($overrides, fn($o): bool => !empty($o->groupid));
|
||||
if (!empty($groupoverrides)) {
|
||||
$opens = array_filter(array_column($groupoverrides, 'timeopen'), fn($t): bool => $t !== null);
|
||||
$closes = array_filter(array_column($groupoverrides, 'timeclose'), fn($t): bool => $t !== null);
|
||||
|
||||
// Get the earliest open time.
|
||||
if ($timeopen === null && count($opens)) {
|
||||
$timeopen = min($opens);
|
||||
}
|
||||
|
||||
// Get the latest close time, unless any are 0 which takes precedence.
|
||||
if ($timeclose === null && count($closes)) {
|
||||
$timeclose = in_array(0, $closes) ? 0 : max($closes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$result = [];
|
||||
if ($timeopen !== null) {
|
||||
$result['timeopen'] = $timeopen;
|
||||
}
|
||||
if ($timeclose !== null) {
|
||||
$result['timeclose'] = $timeclose;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes orphaned group overrides in a given course.
|
||||
* Note - permissions are not checked and events are not logged for performance reasons.
|
||||
@@ -620,11 +684,31 @@ class override_manager {
|
||||
|
||||
$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);
|
||||
// Clear the cache for all users in the course for each quiz that had an orphaned group override.
|
||||
$quizids = array_unique(array_column($records, 'quiz'));
|
||||
$userids = array_keys(get_enrolled_users(context_course::instance($courseid), '', 0, 'u.id'));
|
||||
foreach ($quizids as $quizid) {
|
||||
quiz_overrides_cache_manager::purge_for_users($quizid, $userids);
|
||||
}
|
||||
return array_unique(array_column($records, 'quiz'));
|
||||
|
||||
return $quizids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook callback to clear relevant cache entries when a user is added to a group.
|
||||
*
|
||||
* @param after_group_membership_added $hook
|
||||
*/
|
||||
public static function after_group_membership_added(after_group_membership_added $hook): void {
|
||||
quiz_overrides_cache_manager::purge_for_group_members($hook->groupinstance->id, $hook->userids);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook callback to clear relevant cache entries when a user is removed from a group.
|
||||
*
|
||||
* @param after_group_membership_removed $hook
|
||||
*/
|
||||
public static function after_group_membership_removed(after_group_membership_removed $hook): void {
|
||||
quiz_overrides_cache_manager::purge_for_group_members($hook->groupinstance->id, $hook->userids);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
<?php
|
||||
// This file is part of Moodle - https://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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
namespace mod_quiz\local;
|
||||
|
||||
use cache;
|
||||
|
||||
/**
|
||||
* Manages cache operations for quiz overrides.
|
||||
*
|
||||
* Please do not use this class directly. Instead, use methods from \mod_quiz\local\override_manager.
|
||||
*
|
||||
* @package mod_quiz
|
||||
* @copyright 2025 Catalyst IT Australia Pty Ltd
|
||||
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class quiz_overrides_cache_manager {
|
||||
/**
|
||||
* Get all overrides for a given quiz and user from cache (or underlying data source).
|
||||
*
|
||||
* @param int $quizid The quiz id.
|
||||
* @param int $userid The user id.
|
||||
* @return array Array of overrides (empty when none).
|
||||
*/
|
||||
public static function get_overrides(int $quizid, int $userid): array {
|
||||
$cache = self::get_cache();
|
||||
$key = self::make_key($quizid, $userid);
|
||||
$value = $cache->get($key);
|
||||
return is_array($value) ? $value : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Purge all overrides from the overrides cache.
|
||||
*/
|
||||
public static function purge_all(): void {
|
||||
self::get_cache()->purge();
|
||||
}
|
||||
|
||||
/**
|
||||
* Purge overrides for a specific user in a specific quiz.
|
||||
*
|
||||
* @param int $quizid The quiz id.
|
||||
* @param int $userid The user id.
|
||||
*/
|
||||
public static function purge_for_user(int $quizid, int $userid): void {
|
||||
self::purge_for_users($quizid, [$userid]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Purge overrides for specific users in a specific quiz.
|
||||
*
|
||||
* @param int $quizid The quiz id.
|
||||
* @param int[] $userids The user ids.
|
||||
*/
|
||||
public static function purge_for_users(int $quizid, array $userids): void {
|
||||
if (empty($userids)) {
|
||||
return;
|
||||
}
|
||||
$keys = array_map(static fn(int $userid): string => self::make_key($quizid, $userid), $userids);
|
||||
self::get_cache()->delete_many($keys);
|
||||
}
|
||||
|
||||
/**
|
||||
* Purge overrides for all members of a given group in a specific quiz.
|
||||
*
|
||||
* @param int $quizid The quiz id.
|
||||
* @param int $groupid The group id.
|
||||
*/
|
||||
public static function purge_for_group(int $quizid, int $groupid): void {
|
||||
self::purge_for_groups($quizid, [$groupid]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Purge overrides for all members of the given groups in a specific quiz.
|
||||
*
|
||||
* @param int $quizid The quiz id.
|
||||
* @param array $groupids The group ids.
|
||||
*/
|
||||
public static function purge_for_groups(int $quizid, array $groupids): void {
|
||||
global $DB;
|
||||
[$insql, $params] = $DB->get_in_or_equal($groupids);
|
||||
$sql = "SELECT DISTINCT userid
|
||||
FROM {groups_members}
|
||||
WHERE groupid {$insql}";
|
||||
$userids = $DB->get_fieldset_sql($sql, $params);
|
||||
if (!empty($userids)) {
|
||||
self::purge_for_users($quizid, $userids);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Purge overrides for all members of a given group across all quizzes.
|
||||
*
|
||||
* @param int $groupid The group id.
|
||||
* @param int[] $userids The user ids.
|
||||
*/
|
||||
public static function purge_for_group_members(int $groupid, array $userids): void {
|
||||
global $DB;
|
||||
|
||||
if (empty($userids)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$sql = "SELECT DISTINCT quiz
|
||||
FROM {quiz_overrides}
|
||||
WHERE groupid = :groupid";
|
||||
$quizids = $DB->get_fieldset_sql($sql, ['groupid' => $groupid]);
|
||||
foreach ($quizids as $quizid) {
|
||||
self::purge_for_users((int) $quizid, $userids);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Purge overrides for all users and groups found in the given override records.
|
||||
*
|
||||
* @param array $overrides Records containing at least quiz id (quiz) and either a user id (userid) or group id (groupid).
|
||||
*/
|
||||
public static function purge_for_overrides(array $overrides): void {
|
||||
$useridsbyquiz = [];
|
||||
$groupidsbyquiz = [];
|
||||
|
||||
foreach ($overrides as $override) {
|
||||
if (!empty($override->userid)) {
|
||||
$useridsbyquiz[$override->quiz][] = (int) $override->userid;
|
||||
}
|
||||
|
||||
if (!empty($override->groupid)) {
|
||||
$groupidsbyquiz[$override->quiz][] = (int) $override->groupid;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($useridsbyquiz as $quizid => $userids) {
|
||||
self::purge_for_users($quizid, array_unique($userids));
|
||||
}
|
||||
|
||||
foreach ($groupidsbyquiz as $quizid => $groupids) {
|
||||
self::purge_for_groups($quizid, array_unique($groupids));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the cache key.
|
||||
*
|
||||
* @param int $quizid The quiz id.
|
||||
* @param int $userid The user id.
|
||||
* @return string The cache key.
|
||||
*/
|
||||
private static function make_key(int $quizid, int $userid): string {
|
||||
return "{$quizid}_{$userid}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the overrides cache instance.
|
||||
*/
|
||||
private static function get_cache(): cache {
|
||||
return cache::make('mod_quiz', 'quiz_overrides');
|
||||
}
|
||||
}
|
||||
@@ -27,12 +27,10 @@ declare(strict_types=1);
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$definitions = [
|
||||
'overrides' => [
|
||||
'mode' => cache_store::MODE_APPLICATION,
|
||||
// Overrides cache keyed by quizid_userid returning all applicable overrides.
|
||||
'quiz_overrides' => [
|
||||
'mode' => core_cache\store::MODE_APPLICATION,
|
||||
'simplekeys' => true,
|
||||
'datasource' => '\mod_quiz\cache\overrides',
|
||||
'invalidationevents' => [
|
||||
\mod_quiz\local\override_cache::INVALIDATION_USERDATARESET,
|
||||
],
|
||||
'datasource' => \mod_quiz\cache\quiz_overrides_cache::class,
|
||||
],
|
||||
];
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
// This file is part of Moodle - https://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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Hook callbacks for quiz module.
|
||||
*
|
||||
* @package mod_quiz
|
||||
* @copyright 2025 Catalyst IT Australia Pty Ltd
|
||||
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$callbacks = [
|
||||
[
|
||||
'hook' => \core_group\hook\after_group_membership_added::class,
|
||||
'callback' => [\mod_quiz\local\override_manager::class, 'after_group_membership_added'],
|
||||
],
|
||||
[
|
||||
'hook' => \core_group\hook\after_group_membership_removed::class,
|
||||
'callback' => [\mod_quiz\local\override_manager::class, 'after_group_membership_removed'],
|
||||
],
|
||||
];
|
||||
@@ -7,3 +7,4 @@ randomcoursewithsubcat,mod_quiz
|
||||
randomsystemwithsubcat,mod_quiz
|
||||
selectquestionbank,mod_quiz
|
||||
gobacktoquiz,mod_quiz
|
||||
cachedef_overrides,mod_quiz
|
||||
|
||||
@@ -135,7 +135,7 @@ $string['browsersecurity_help'] = 'If "Full screen pop-up with some JavaScript s
|
||||
* The quiz will only start if the student has a JavaScript-enabled web-browser
|
||||
* The quiz appears in a full screen popup window that covers all the other windows and has no navigation controls
|
||||
* Students are prevented, as far as is possible, from using facilities like copy and paste';
|
||||
$string['cachedef_overrides'] = 'User and group override information';
|
||||
$string['cachedef_quiz_overrides'] = 'User and group override information';
|
||||
$string['calculated'] = 'Calculated';
|
||||
$string['calculatedquestion'] = 'Calculated question not supported at line {$a}. The question will be ignored';
|
||||
$string['cannotcreatepath'] = 'Path cannot be created ({$a})';
|
||||
@@ -1169,3 +1169,4 @@ $string['randomsystemwithsubcat'] = 'Any system-level category';
|
||||
// Deprecated since Moodle 5.2.
|
||||
$string['gobacktoquiz'] = 'Go back';
|
||||
$string['selectquestionbank'] = 'Select question bank';
|
||||
$string['cachedef_overrides'] = 'User and group override information';
|
||||
|
||||
+21
-56
@@ -26,6 +26,8 @@
|
||||
*/
|
||||
|
||||
use core_question\local\bank\question_bank_helper;
|
||||
use mod_quiz\local\override_manager;
|
||||
use mod_quiz\local\quiz_overrides_cache_manager;
|
||||
use qbank_managecategories\helper;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
@@ -38,7 +40,6 @@ 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;
|
||||
|
||||
@@ -1499,31 +1500,39 @@ function quiz_reset_userdata($data) {
|
||||
'error' => false];
|
||||
}
|
||||
|
||||
$purgeoverrides = false;
|
||||
$overrides = [];
|
||||
|
||||
// Remove user overrides.
|
||||
if (!empty($data->reset_quiz_user_overrides)) {
|
||||
$DB->delete_records_select('quiz_overrides',
|
||||
'quiz IN (SELECT id FROM {quiz} WHERE course = ?) AND userid IS NOT NULL', [$data->courseid]);
|
||||
$select = 'quiz IN (SELECT id FROM {quiz} WHERE course = ?) AND userid IS NOT NULL';
|
||||
$params = [$data->courseid];
|
||||
|
||||
$overrides = array_merge($overrides, $DB->get_records_select('quiz_overrides', $select, $params));
|
||||
$DB->delete_records_select('quiz_overrides', $select, $params);
|
||||
$status[] = [
|
||||
'component' => $componentstr,
|
||||
'item' => get_string('useroverrides', 'quiz'),
|
||||
'error' => false];
|
||||
$purgeoverrides = true;
|
||||
}
|
||||
// Remove group overrides.
|
||||
if (!empty($data->reset_quiz_group_overrides)) {
|
||||
$DB->delete_records_select('quiz_overrides',
|
||||
'quiz IN (SELECT id FROM {quiz} WHERE course = ?) AND groupid IS NOT NULL', [$data->courseid]);
|
||||
$select = 'quiz IN (SELECT id FROM {quiz} WHERE course = ?) AND groupid IS NOT NULL';
|
||||
$params = [$data->courseid];
|
||||
|
||||
$overrides = array_merge($overrides, $DB->get_records_select('quiz_overrides', $select, $params));
|
||||
$DB->delete_records_select('quiz_overrides', $select, $params);
|
||||
$status[] = [
|
||||
'component' => $componentstr,
|
||||
'item' => get_string('groupoverrides', 'quiz'),
|
||||
'error' => false];
|
||||
$purgeoverrides = true;
|
||||
}
|
||||
|
||||
// Updating dates - shift may be negative too.
|
||||
if ($data->timeshift) {
|
||||
$select = 'quiz IN (SELECT id FROM {quiz} WHERE course = ?)';
|
||||
$params = [$data->courseid];
|
||||
|
||||
$overrides = array_merge($overrides, $DB->get_records_select('quiz_overrides', $select, $params));
|
||||
$DB->execute("UPDATE {quiz_overrides}
|
||||
SET timeopen = timeopen + ?
|
||||
WHERE quiz IN (SELECT id FROM {quiz} WHERE course = ?)
|
||||
@@ -1533,8 +1542,6 @@ function quiz_reset_userdata($data) {
|
||||
WHERE quiz IN (SELECT id FROM {quiz} WHERE course = ?)
|
||||
AND timeclose <> 0", [$data->timeshift, $data->courseid]);
|
||||
|
||||
$purgeoverrides = true;
|
||||
|
||||
// Any changes to the list of dates that needs to be rolled should be same during course restore and course reset.
|
||||
// See MDL-9367.
|
||||
shift_course_mod_dates('quiz', ['timeopen', 'timeclose'],
|
||||
@@ -1546,8 +1553,8 @@ function quiz_reset_userdata($data) {
|
||||
'error' => false];
|
||||
}
|
||||
|
||||
if ($purgeoverrides) {
|
||||
\cache_helper::purge_by_event(\mod_quiz\local\override_cache::INVALIDATION_USERDATARESET);
|
||||
if (!empty($overrides)) {
|
||||
quiz_overrides_cache_manager::purge_for_overrides($overrides);
|
||||
}
|
||||
|
||||
return $status;
|
||||
@@ -2212,50 +2219,8 @@ function quiz_get_coursemodule_info($coursemodule) {
|
||||
*/
|
||||
function mod_quiz_cm_info_dynamic(cm_info $cm) {
|
||||
global $USER;
|
||||
|
||||
$cache = new override_cache($cm->instance);
|
||||
$override = $cache->get_cached_user_override($USER->id);
|
||||
|
||||
if (!$override) {
|
||||
$override = (object) [
|
||||
'timeopen' => null,
|
||||
'timeclose' => null,
|
||||
];
|
||||
}
|
||||
|
||||
// No need to look for group overrides if there are user overrides for both timeopen and timeclose.
|
||||
if (is_null($override->timeopen) || is_null($override->timeclose)) {
|
||||
$opens = [];
|
||||
$closes = [];
|
||||
$groupings = groups_get_user_groups($cm->course, $USER->id);
|
||||
foreach ($groupings[0] as $groupid) {
|
||||
$groupoverride = $cache->get_cached_group_override($groupid);
|
||||
if (isset($groupoverride->timeopen)) {
|
||||
$opens[] = $groupoverride->timeopen;
|
||||
}
|
||||
if (isset($groupoverride->timeclose)) {
|
||||
$closes[] = $groupoverride->timeclose;
|
||||
}
|
||||
}
|
||||
// If there is a user override for a setting, ignore the group override.
|
||||
if (is_null($override->timeopen) && count($opens)) {
|
||||
$override->timeopen = min($opens);
|
||||
}
|
||||
if (is_null($override->timeclose) && count($closes)) {
|
||||
if (in_array(0, $closes)) {
|
||||
$override->timeclose = 0;
|
||||
} else {
|
||||
$override->timeclose = max($closes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Populate some other values that can be used in calendar or on dashboard.
|
||||
if (!is_null($override->timeopen)) {
|
||||
$cm->override_customdata('timeopen', $override->timeopen);
|
||||
}
|
||||
if (!is_null($override->timeclose)) {
|
||||
$cm->override_customdata('timeclose', $override->timeclose);
|
||||
foreach (override_manager::get_effective_open_close_times($cm->instance, $USER->id) as $key => $value) {
|
||||
$cm->override_customdata($key, $value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
<?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 tests for quiz overrides
|
||||
*
|
||||
* @package mod_quiz
|
||||
* @copyright 2024 Matthew Hilton <[email protected]>
|
||||
* @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');
|
||||
$cache = $getcache->invoke($overridecache);
|
||||
|
||||
$getuserkey = $reflection->getMethod('get_user_cache_key');
|
||||
|
||||
$getgroupkey = $reflection->getMethod('get_group_cache_key');
|
||||
|
||||
$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(override_cache::INVALIDATION_USERDATARESET);
|
||||
$this->assertEmpty($overridecache->get_cached_user_override(123));
|
||||
$this->assertEmpty($overridecache->get_cached_group_override(456));
|
||||
}
|
||||
}
|
||||
@@ -955,14 +955,14 @@ final class override_manager_test extends \advanced_testcase {
|
||||
$this->assertCount(1, calendar_get_events(0, 999, [$user->id], false, false));
|
||||
|
||||
// Check that the cache was made.
|
||||
$overridecache = new override_cache($quizobj->get_quizid());
|
||||
$this->assertNotEmpty($overridecache->get_cached_user_override($user->id));
|
||||
$this->assertNotEmpty(quiz_overrides_cache_manager::get_overrides($quizobj->get_quizid(), $user->id));
|
||||
|
||||
// Capture events.
|
||||
$sink = $this->redirectEvents();
|
||||
|
||||
$override = (object) [
|
||||
'id' => $id,
|
||||
'quiz' => $quizobj->get_quizid(),
|
||||
'userid' => $user->id,
|
||||
];
|
||||
|
||||
@@ -973,7 +973,7 @@ final class override_manager_test extends \advanced_testcase {
|
||||
$this->assertCount(0, calendar_get_events(0, 999, [$user->id], false, false));
|
||||
|
||||
// Check that the cache was cleared.
|
||||
$this->assertEmpty($overridecache->get_cached_user_override($user->id));
|
||||
$this->assertEmpty(quiz_overrides_cache_manager::get_overrides($quizobj->get_quizid(), $user->id));
|
||||
|
||||
// Check the event was logged.
|
||||
if ($checkeventslogged) {
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
<?php
|
||||
// This file is part of Moodle - https://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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
namespace mod_quiz\local;
|
||||
|
||||
use advanced_testcase;
|
||||
use context_module;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* Tests for the quiz overrides cache manager.
|
||||
*
|
||||
* @package mod_quiz
|
||||
* @copyright 2025 Catalyst IT Australia Pty Ltd
|
||||
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @covers \mod_quiz\cache\quiz_overrides_cache
|
||||
* @covers \mod_quiz\local\quiz_overrides_cache_manager
|
||||
*/
|
||||
final class quiz_overrides_cache_manager_test extends advanced_testcase {
|
||||
/**
|
||||
* Builds and returns a reusable quiz overrides testing context.
|
||||
*
|
||||
* @return stdClass
|
||||
*/
|
||||
private function create_test_data(): stdClass {
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
|
||||
$generator = $this->getDataGenerator();
|
||||
$course = $generator->create_course();
|
||||
$quiz = $generator->create_module('quiz', ['course' => $course->id]);
|
||||
$user1 = $generator->create_and_enrol($course);
|
||||
$user2 = $generator->create_and_enrol($course);
|
||||
$group = $generator->create_group(['courseid' => $course->id]);
|
||||
groups_add_member($group->id, $user2->id);
|
||||
|
||||
$manager = new override_manager($quiz, context_module::instance($quiz->cmid));
|
||||
|
||||
return (object) [
|
||||
'quiz' => $quiz,
|
||||
'manager' => $manager,
|
||||
'user1' => $user1,
|
||||
'user2' => $user2,
|
||||
'group' => $group,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures getting overrides returns an empty array when none have been created.
|
||||
*/
|
||||
public function test_get_overrides_is_empty_initially(): void {
|
||||
$data = $this->create_test_data();
|
||||
|
||||
$this->assertSame([], quiz_overrides_cache_manager::get_overrides($data->quiz->id, $data->user1->id));
|
||||
$this->assertSame([], quiz_overrides_cache_manager::get_overrides($data->quiz->id, $data->user2->id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures a user override is returned only for the specified user.
|
||||
*/
|
||||
public function test_get_overrides_returns_user_override_for_correct_user(): void {
|
||||
$data = $this->create_test_data();
|
||||
|
||||
$overrideid = $data->manager->save_override([
|
||||
'userid' => $data->user1->id,
|
||||
'timelimit' => HOURSECS,
|
||||
]);
|
||||
|
||||
$overridesforuser1 = quiz_overrides_cache_manager::get_overrides($data->quiz->id, $data->user1->id);
|
||||
$this->assertCount(1, $overridesforuser1);
|
||||
$this->assertEquals($overrideid, reset($overridesforuser1)->id);
|
||||
|
||||
$this->assertSame([], quiz_overrides_cache_manager::get_overrides($data->quiz->id, $data->user2->id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures a group override is returned only for users who are members of the group.
|
||||
*/
|
||||
public function test_get_overrides_returns_group_override_for_group_member(): void {
|
||||
$data = $this->create_test_data();
|
||||
|
||||
$overrideid = $data->manager->save_override([
|
||||
'groupid' => $data->group->id,
|
||||
'timelimit' => HOURSECS * 2,
|
||||
]);
|
||||
|
||||
$this->assertSame([], quiz_overrides_cache_manager::get_overrides($data->quiz->id, $data->user1->id));
|
||||
|
||||
$overridesforuser2 = quiz_overrides_cache_manager::get_overrides($data->quiz->id, $data->user2->id);
|
||||
$this->assertCount(1, $overridesforuser2);
|
||||
$this->assertEquals($overrideid, reset($overridesforuser2)->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that deleting an override by its ID invalidates the cache for the affected user.
|
||||
*/
|
||||
public function test_deleting_override_by_id_invalidates_cache(): void {
|
||||
$data = $this->create_test_data();
|
||||
|
||||
$useroverrideid = $data->manager->save_override([
|
||||
'userid' => $data->user1->id,
|
||||
'timelimit' => HOURSECS,
|
||||
]);
|
||||
$data->manager->save_override([
|
||||
'groupid' => $data->group->id,
|
||||
'timelimit' => HOURSECS * 2,
|
||||
]);
|
||||
|
||||
$this->assertCount(1, quiz_overrides_cache_manager::get_overrides($data->quiz->id, $data->user1->id));
|
||||
$this->assertCount(1, quiz_overrides_cache_manager::get_overrides($data->quiz->id, $data->user2->id));
|
||||
|
||||
$data->manager->delete_overrides_by_id([$useroverrideid], false);
|
||||
|
||||
$this->assertSame([], quiz_overrides_cache_manager::get_overrides($data->quiz->id, $data->user1->id));
|
||||
$this->assertCount(1, quiz_overrides_cache_manager::get_overrides($data->quiz->id, $data->user2->id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that deleting an override by its record invalidates the cache for affected users.
|
||||
*/
|
||||
public function test_deleting_override_record_invalidates_cache(): void {
|
||||
global $DB;
|
||||
|
||||
$data = $this->create_test_data();
|
||||
|
||||
$data->manager->save_override([
|
||||
'userid' => $data->user1->id,
|
||||
'timelimit' => HOURSECS,
|
||||
]);
|
||||
$groupoverrideid = $data->manager->save_override([
|
||||
'groupid' => $data->group->id,
|
||||
'timelimit' => HOURSECS * 2,
|
||||
]);
|
||||
|
||||
$this->assertCount(1, quiz_overrides_cache_manager::get_overrides($data->quiz->id, $data->user1->id));
|
||||
$this->assertCount(1, quiz_overrides_cache_manager::get_overrides($data->quiz->id, $data->user2->id));
|
||||
|
||||
$groupoverride = $DB->get_record('quiz_overrides', ['id' => $groupoverrideid], '*', MUST_EXIST);
|
||||
$data->manager->delete_overrides([$groupoverride], false);
|
||||
|
||||
$this->assertCount(1, quiz_overrides_cache_manager::get_overrides($data->quiz->id, $data->user1->id));
|
||||
$this->assertSame([], quiz_overrides_cache_manager::get_overrides($data->quiz->id, $data->user2->id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that deleting all overrides for a quiz invalidates the cache for all users.
|
||||
*/
|
||||
public function test_deleting_all_overrides_invalidates_cache_for_all_users(): void {
|
||||
$data = $this->create_test_data();
|
||||
|
||||
$data->manager->save_override([
|
||||
'userid' => $data->user1->id,
|
||||
'timelimit' => HOURSECS,
|
||||
]);
|
||||
$data->manager->save_override([
|
||||
'groupid' => $data->group->id,
|
||||
'timelimit' => HOURSECS * 2,
|
||||
]);
|
||||
|
||||
$this->assertCount(1, quiz_overrides_cache_manager::get_overrides($data->quiz->id, $data->user1->id));
|
||||
$this->assertCount(1, quiz_overrides_cache_manager::get_overrides($data->quiz->id, $data->user2->id));
|
||||
|
||||
$data->manager->delete_all_overrides(false);
|
||||
|
||||
$this->assertSame([], quiz_overrides_cache_manager::get_overrides($data->quiz->id, $data->user1->id));
|
||||
$this->assertSame([], quiz_overrides_cache_manager::get_overrides($data->quiz->id, $data->user2->id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that changes to group membership invalidate the relevant user caches.
|
||||
*/
|
||||
public function test_group_membership_changes_invalidate_cache(): void {
|
||||
$data = $this->create_test_data();
|
||||
|
||||
$data->manager->save_override([
|
||||
'userid' => $data->user1->id,
|
||||
'timelimit' => HOURSECS,
|
||||
]);
|
||||
$data->manager->save_override([
|
||||
'groupid' => $data->group->id,
|
||||
'timelimit' => HOURSECS * 2,
|
||||
]);
|
||||
|
||||
$this->assertCount(1, quiz_overrides_cache_manager::get_overrides($data->quiz->id, $data->user1->id));
|
||||
$this->assertCount(1, quiz_overrides_cache_manager::get_overrides($data->quiz->id, $data->user2->id));
|
||||
|
||||
groups_add_member($data->group->id, $data->user1->id);
|
||||
$this->assertCount(2, quiz_overrides_cache_manager::get_overrides($data->quiz->id, $data->user1->id));
|
||||
$this->assertCount(1, quiz_overrides_cache_manager::get_overrides($data->quiz->id, $data->user2->id));
|
||||
|
||||
groups_remove_member($data->group->id, $data->user1->id);
|
||||
$this->assertCount(1, quiz_overrides_cache_manager::get_overrides($data->quiz->id, $data->user1->id));
|
||||
$this->assertCount(1, quiz_overrides_cache_manager::get_overrides($data->quiz->id, $data->user2->id));
|
||||
|
||||
groups_delete_group($data->group->id);
|
||||
$this->assertCount(1, quiz_overrides_cache_manager::get_overrides($data->quiz->id, $data->user1->id));
|
||||
$this->assertSame([], quiz_overrides_cache_manager::get_overrides($data->quiz->id, $data->user2->id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that purging the cache for an override invalidates the cache for all affected users.
|
||||
*/
|
||||
public function test_purge_for_override(): void {
|
||||
global $DB;
|
||||
|
||||
$data = $this->create_test_data();
|
||||
|
||||
$useroverrideid = $data->manager->save_override([
|
||||
'userid' => $data->user1->id,
|
||||
'timelimit' => HOURSECS,
|
||||
]);
|
||||
$data->manager->save_override([
|
||||
'groupid' => $data->group->id,
|
||||
'timelimit' => HOURSECS * 2,
|
||||
]);
|
||||
$records = $DB->get_records('quiz_overrides');
|
||||
|
||||
$this->assertCount(1, quiz_overrides_cache_manager::get_overrides($data->quiz->id, $data->user1->id));
|
||||
$this->assertCount(1, quiz_overrides_cache_manager::get_overrides($data->quiz->id, $data->user2->id));
|
||||
|
||||
$DB->delete_records('quiz_overrides', ['quiz' => $data->quiz->id]);
|
||||
|
||||
// Cached data still exists until the manager purges the relevant entries.
|
||||
$this->assertCount(1, quiz_overrides_cache_manager::get_overrides($data->quiz->id, $data->user1->id));
|
||||
$this->assertCount(1, quiz_overrides_cache_manager::get_overrides($data->quiz->id, $data->user2->id));
|
||||
|
||||
// Purge for the user override only.
|
||||
quiz_overrides_cache_manager::purge_for_overrides([$records[$useroverrideid]]);
|
||||
|
||||
$this->assertSame([], quiz_overrides_cache_manager::get_overrides($data->quiz->id, $data->user1->id));
|
||||
$this->assertCount(1, quiz_overrides_cache_manager::get_overrides($data->quiz->id, $data->user2->id));
|
||||
|
||||
// Purge for all overrides.
|
||||
quiz_overrides_cache_manager::purge_for_overrides($records);
|
||||
|
||||
$this->assertSame([], quiz_overrides_cache_manager::get_overrides($data->quiz->id, $data->user1->id));
|
||||
$this->assertSame([], quiz_overrides_cache_manager::get_overrides($data->quiz->id, $data->user2->id));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user