diff --git a/mod/quiz/classes/access_manager.php b/mod/quiz/classes/access_manager.php index b7f29824eee79..be859144c3fc2 100644 --- a/mod/quiz/classes/access_manager.php +++ b/mod/quiz/classes/access_manager.php @@ -17,6 +17,7 @@ namespace mod_quiz; use core_component; +use mod_quiz\form\edit_override_form; use mod_quiz\form\preflight_check_form; use mod_quiz\local\access_rule_base; use mod_quiz\output\renderer; @@ -103,6 +104,17 @@ protected static function get_rule_classes(): array { return core_component::get_plugin_list_with_class('quizaccess', '', 'rule.php'); } + /** + * Get the names of overridable rule classes. + * + * @return array of class names. + */ + protected static function get_overridable_rule_classes(): array { + return array_filter(self::get_rule_classes(), function($rule) { + return in_array(\mod_quiz\local\rule_overridable::class, class_implements($rule)); + }); + } + /** * Add any form fields that the access rules require to the settings form. * @@ -279,6 +291,138 @@ public static function load_quiz_and_settings(int $quizid): stdClass { return $quiz; } + /** + * Add any form fields that the access rules require to the override form. + * + * Note that the standard plugins do not use this mechanism, becuase all their + * settings are stored in the quiz table. + * + * @param edit_override_form $quizform the quiz override settings form being built. + * @param MoodleQuickForm $mform the wrapped MoodleQuickForm. + * @return boolean return true if fields have been added. + */ + public static function add_override_form_fields( + edit_override_form $quizform, MoodleQuickForm $mform): bool { + $fieldsadded = false; + foreach (self::get_overridable_rule_classes() as $rule) { + $element = $rule::get_override_form_section_header(); + $mform->addElement('header', $element['name'], $element['title']); + $mform->setExpanded($element['name'], $rule::get_override_form_section_expand($quizform)); + $rule::add_override_form_fields($quizform, $mform); + $fieldsadded = true; + } + return $fieldsadded; + } + + /** + * Validate the data from any form fields added using {@see add_override_form_fields()}. + * + * @param array $errors the errors found so far. + * @param array $data the submitted form data. + * @param array $files information about any uploaded files. + * @param edit_override_form $quizform the quiz override form object. + * @return array $errors the updated $errors array. + */ + public static function validate_override_form_fields(array $errors, + array $data, array $files, edit_override_form $quizform): array { + foreach (self::get_overridable_rule_classes() as $rule) { + $errors = $rule::validate_override_form_fields($errors, $data, $files, $quizform); + } + return $errors; + } + + /** + * Save any submitted settings when the quiz override settings form is submitted. + * + * @param array $override data from the override form. + * @return void + */ + public static function save_override_settings(array $override): void { + foreach (self::get_overridable_rule_classes() as $rule) { + $rule::save_override_settings($override); + } + } + + /** + * Delete any rule-specific override settings when the quiz override is deleted. + * + * @param int $quizid all overrides being deleted should belong to the same quiz. + * @param array $overrides an array of override objects to be deleted. + * @return void + */ + public static function delete_override_settings($quizid, $overrides): void { + foreach (self::get_overridable_rule_classes() as $rule) { + $rule::delete_override_settings($quizid, $overrides); + } + } + + /** + * Get components of the SQL query to fetch the access rule components' override + * settings. To be used as part of a quiz_override query to reference. + * + * @param string $overridetablename Name of the table to reference for joins. + * @return array 'selects', 'joins' and 'params'. + */ + public static function get_override_settings_sql($overridetablename = 'quiz_overrides'): array { + $allfields = []; + $alljoins = []; + $allparams = []; + + foreach (self::get_overridable_rule_classes() as $rule) { + [$fields, $joins, $params] = $rule::get_override_settings_sql($overridetablename); + $fields && $allfields[] = $fields; + $joins && $alljoins[] = $joins; + $params && $allparams += $params; + } + + $allfields = implode(', ', $allfields); + $alljoins = implode(' ', $alljoins); + + return [$allfields, $alljoins, $allparams]; + } + + /** + * Retrieve all keys of fields to be used in the override form. + * + * @return array + */ + public static function get_override_setting_keys(): array { + $keys = []; + foreach (self::get_overridable_rule_classes() as $rule) { + $keys += $rule::get_override_setting_keys(); + } + return $keys; + } + + /** + * Retrieve keys of fields that are required to be filled in. + * + * @return array + */ + public static function get_override_required_setting_keys(): array { + $keys = []; + foreach (self::get_overridable_rule_classes() as $rule) { + $keys += $rule::get_override_required_setting_keys(); + } + return $keys; + } + + /** + * Update fields and values of the override table using the override settings. + * + * @param object $override the override data to use to update the $fields and $values. + * @param array $fields the fields to populate. + * @param array $values the fields to populate. + * @param context $context the context of which the override is being applied to. + * @return array + */ + public static function add_override_table_fields($override, $fields, $values, $context): array { + foreach (self::get_overridable_rule_classes() as $rule) { + [$fields, $values] = $rule::add_override_table_fields($override, $fields, $values, $context); + } + return [$fields, $values]; + } + /** * Get an array of the class names of all the active rules. * diff --git a/mod/quiz/classes/form/edit_override_form.php b/mod/quiz/classes/form/edit_override_form.php index 4f5c7c6786074..4bc5c2ccacf7c 100644 --- a/mod/quiz/classes/form/edit_override_form.php +++ b/mod/quiz/classes/form/edit_override_form.php @@ -23,6 +23,7 @@ use moodle_url; use moodleform; use stdClass; +use mod_quiz\access_manager; defined('MOODLE_INTERNAL') || die(); @@ -84,6 +85,30 @@ public function __construct(moodle_url $submiturl, parent::__construct($submiturl); } + /** + * Return the course context for new modules, or the module context for existing modules. + * @return context_module + */ + public function get_context(): context_module { + return $this->context; + } + + /** + * Get the quiz override ID. + * @return int + */ + public function get_overrideid(): int { + return $this->overrideid; + } + + /** + * Get the quiz object. + * @return \stdClass + */ + public function get_quiz(): \stdClass { + return $this->quiz; + } + protected function definition() { global $DB; @@ -224,6 +249,11 @@ protected function definition() { $mform->addHelpButton('attempts', 'attempts', 'quiz'); $mform->setDefault('attempts', $this->quiz->attempts); + // Access-rule fields. + if (access_manager::add_override_form_fields($this, $mform)) { + $mform->closeHeaderBefore('resetbutton'); + } + // Submit buttons. $mform->addElement('submit', 'resetbutton', get_string('reverttodefaults', 'quiz')); @@ -289,6 +319,9 @@ public function validation($data, $files): array { } } + // Apply access-rule validation. + $errors = access_manager::validate_override_form_fields($errors, $data, $files, $this); + return $errors; } } diff --git a/mod/quiz/classes/local/override_manager.php b/mod/quiz/classes/local/override_manager.php index 827cdeac5e11a..6c4330022ae8b 100644 --- a/mod/quiz/classes/local/override_manager.php +++ b/mod/quiz/classes/local/override_manager.php @@ -16,6 +16,7 @@ namespace mod_quiz\local; +use mod_quiz\access_manager; use mod_quiz\event\group_override_created; use mod_quiz\event\group_override_deleted; use mod_quiz\event\group_override_updated; @@ -87,9 +88,11 @@ public function validate_data(array $formdata): array { $errors = []; // Ensure at least one of the overrideable settings is set. + $accessrulerequiredkeys = access_manager::get_override_required_setting_keys(); + $requiredkeys = array_merge(self::OVERRIDEABLE_QUIZ_SETTINGS, $accessrulerequiredkeys); $keysthatareset = array_map(function ($key) use ($formdata) { return isset($formdata->$key) && !is_null($formdata->$key); - }, self::OVERRIDEABLE_QUIZ_SETTINGS); + }, $requiredkeys); if (!in_array(true, $keysthatareset)) { $errors['general'][] = new \lang_string('nooverridedata', 'quiz'); @@ -207,7 +210,7 @@ private static function validate_against_existing_record(int $existingid, \stdCl } /** - * Parses the formdata by finding only the OVERRIDEABLE_QUIZ_SETTINGS, + * Parses the formdata by finding the OVERRIDEABLE_QUIZ_SETTINGS and from overridable access-rule components, * 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. @@ -216,7 +219,9 @@ private static function validate_against_existing_record(int $existingid, \stdCl */ 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)); + $accessrulekeys = access_manager::get_override_setting_keys(); + $keys = array_merge(self::OVERRIDEABLE_QUIZ_SETTINGS, $accessrulekeys); + $settings = array_intersect_key($formdata, array_flip($keys)); // Remove values that are the same as currently in the quiz. $settings = $this->clear_unused_values($settings); @@ -255,6 +260,7 @@ public function save_override(array $formdata): int { } else { $id = $DB->insert_record('quiz_overrides', $datatoset); } + $datatoset['overrideid'] = $id; $userid = $datatoset['userid'] ?? null; $groupid = $datatoset['groupid'] ?? null; @@ -283,6 +289,9 @@ public function save_override(array $formdata): int { quiz_update_events($this->quiz, (object) $datatoset); } + // Update access-rule override data. + access_manager::save_override_settings($datatoset); + return $id; } @@ -330,7 +339,6 @@ public function delete_overrides_by_id(array $ids, bool $shouldlog = true): void $this->delete_overrides($records, $shouldlog); } - /** * Builds sql and parameters to find overrides in quiz with the given ids * @@ -390,6 +398,8 @@ public function delete_overrides(array $overrides, bool $shouldlog = true): void $this->fire_deleted_event($override->id, $userid, $groupid); } } + + access_manager::delete_override_settings($this->quiz->id, $overrides); } /** @@ -627,4 +637,48 @@ public static function delete_orphaned_group_overrides_in_course(int $courseid): } return array_unique(array_column($records, 'quiz')); } + + /** + * Checks if the user has overrides for the quiz whether individually or in a group. + * + * @param int $quizid The quiz object. + * @return stdClass|false + */ + public static function get_quiz_override($quizid) { + global $DB, $USER; + + // No quiz, no override. + if (!($quiz = $DB->get_record('quiz', ['id' => $quizid]))) { + return false; + } + + // SQL components to include quiz_access subplugin override fields. + [$selects, $joins, $params] = access_manager::get_override_settings_sql('o'); + $selects = $selects ? ", {$selects}" : ''; + + // Check for user override. + $sql = "SELECT o.*{$selects} + FROM {quiz_overrides} o {$joins} + WHERE o.quiz = ? AND o.userid = ?"; + $userparams = array_merge($params, [$quiz->id, $USER->id]); + $useroverride = $DB->get_record_sql($sql, $userparams); + if ($useroverride) { + return $useroverride; + } + + // Check for group overrides. + $groupings = groups_get_user_groups($quiz->course, $USER->id); + if (!empty($groupings[0])) { + list($insql, $inparams) = $DB->get_in_or_equal(array_values($groupings[0])); + $sql = "SELECT o.*{$selects} + FROM {quiz_overrides} o + {$joins} + WHERE groupid {$insql} AND quiz = ?"; + $groupparams = array_merge($params, $inparams); + $groupparams[] = $quiz->id; + return $DB->get_record_sql($sql, $groupparams); + } + + return false; + } } diff --git a/mod/quiz/classes/local/rule_overridable.php b/mod/quiz/classes/local/rule_overridable.php new file mode 100644 index 0000000000000..f862f669ed183 --- /dev/null +++ b/mod/quiz/classes/local/rule_overridable.php @@ -0,0 +1,120 @@ +. + +namespace mod_quiz\local; + +use mod_quiz\form\edit_override_form; +use MoodleQuickForm; + +/** + * Class overridable + * + * @package mod_quiz + * @copyright 2024 Michael Kotlyar + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +interface rule_overridable { + + /** + * Add fields to the quiz override form. + * + * @param edit_override_form $quizform the quiz override settings form being built. + * @param MoodleQuickForm $mform the wrapped MoodleQuickForm. + * @return void + */ + public static function add_override_form_fields(edit_override_form $quizform, MoodleQuickForm $mform): void; + + /** + * Override form section header. + * + * Array must have three keys. The 'name' key for the name of the heading element, the 'title' key for the + * text to display in the heading and true/false for the 'expand' key to determine if the section containing + * these fields should be expanded. + * + * @return array [name, title] + */ + public static function get_override_form_section_header(): array; + + /** + * Determine whether the rule section should be expanded or not in the override form. + * + * @param edit_override_form $quizform the quiz override settings form being built. + * @return bool return true if section should be expanded. + */ + public static function get_override_form_section_expand(edit_override_form $quizform): bool; + + /* Validate the data from any form fields added using {@see add_override_form_fields()}. + * + * @param array $errors the errors found so far. + * @param array $data the submitted form data. + * @param array $files information about any uploaded files. + * @param edit_override_form $quizform the quiz override form object. + * @return array the updated $errors array. + */ + public static function validate_override_form_fields(array $errors, + array $data, array $files, edit_override_form $quizform): array; + + /** + * Save any submitted settings when the quiz override settings form is submitted. + * + * @param array $override data from the override form. + * @return void + */ + public static function save_override_settings(array $override): void; + + /** + * Delete any rule-specific override settings when the quiz override is deleted. + * + * @param int $quizid all overrides being deleted should belong to the same quiz. + * @param array $overrides an array of override objects to be deleted. + * @return void + */ + public static function delete_override_settings($quizid, $overrides): void; + + /** + * Provide form field keys in the override form as a string array + * + * @return array e.g. ['rule_enabled', 'rule_password']. + */ + public static function get_override_setting_keys(): array; + + /** + * Provide required form field keys in the override form as a string array + * + * @return array e.g. ['rule_enabled']. + */ + public static function get_override_required_setting_keys(): array; + + /** + * Get components of the SQL query to fetch the access rule components' override + * settings. To be used as part of a quiz_override query to reference. + * + * @param string $overridetablename Name of the table to reference for joins. + * @return array [$selects, $joins, $params'] + */ + public static function get_override_settings_sql($overridetablename): array; + + /** + * Update fields and values of the override table using the override settings. + * + * @param object $override the override data to use to update the $fields and $values. + * @param array $fields the fields to populate. + * @param array $values the fields to populate. + * @param context $context the context of which the override is being applied to. + * @return array [$fields, $values] + */ + public static function add_override_table_fields($override, $fields, $values, $context): array; +} diff --git a/mod/quiz/lib.php b/mod/quiz/lib.php index 375f3cc9e40b4..9b0a1804ad59a 100644 --- a/mod/quiz/lib.php +++ b/mod/quiz/lib.php @@ -236,9 +236,20 @@ function quiz_delete_instance($id) { */ function quiz_update_effective_access($quiz, $userid) { global $DB; + [$selects, $joins, $params] = access_manager::get_override_settings_sql('o'); + $hasaccessruleoverrides = !empty($selects); // Check for user override. - $override = $DB->get_record('quiz_overrides', ['quiz' => $quiz->id, 'userid' => $userid]); + if ($hasaccessruleoverrides) { + $sql = "SELECT o.*, {$selects} + FROM {quiz_overrides} o + {$joins} + WHERE o.quiz = ? + AND o.userid = ?"; + $override = $DB->get_record_sql($sql, array_merge($params, [$quiz->id, $userid])); + } else { + $override = $DB->get_record('quiz_overrides', ['quiz' => $quiz->id, 'userid' => $userid]); + } if (!$override) { $override = new stdClass(); @@ -254,9 +265,13 @@ function quiz_update_effective_access($quiz, $userid) { if (!empty($groupings[0])) { // Select all overrides that apply to the User's groups. - list($extra, $params) = $DB->get_in_or_equal(array_values($groupings[0])); - $sql = "SELECT * FROM {quiz_overrides} - WHERE groupid $extra AND quiz = ?"; + [$extra, $params] = $DB->get_in_or_equal(array_values($groupings[0])); + $accessrulesqlselects = $hasaccessruleoverrides ? ", $selects" : ''; + $sql = "SELECT o.*{$accessrulesqlselects} + FROM {quiz_overrides} o + {$joins} + WHERE groupid {$extra} + AND quiz = ?"; $params[] = $quiz->id; $records = $DB->get_records_sql($sql, $params); @@ -319,7 +334,8 @@ function quiz_update_effective_access($quiz, $userid) { } // Merge with quiz defaults. - $keys = ['timeopen', 'timeclose', 'timelimit', 'attempts', 'password', 'extrapasswords']; + $accessrulekeys = access_manager::get_override_setting_keys(); + $keys = ['timeopen', 'timeclose', 'timelimit', 'attempts', 'password', 'extrapasswords', ...$accessrulekeys]; foreach ($keys as $key) { if (isset($override->{$key})) { $quiz->{$key} = $override->{$key}; diff --git a/mod/quiz/overrideedit.php b/mod/quiz/overrideedit.php index 492fb2901892c..01df0edd0a2cf 100644 --- a/mod/quiz/overrideedit.php +++ b/mod/quiz/overrideedit.php @@ -22,6 +22,7 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +use mod_quiz\access_manager; use mod_quiz\form\edit_override_form; use mod_quiz\quiz_settings; @@ -36,7 +37,15 @@ $override = null; if ($overrideid) { - $override = $DB->get_record('quiz_overrides', ['id' => $overrideid], '*', MUST_EXIST); + [$selects, $joins, $params] = access_manager::get_override_settings_sql('o'); + $hasaccessruleoverrides = !empty($selects); + $accessrulesqlselects = $hasaccessruleoverrides ? ", $selects" : ''; + $sql = "SELECT o.* {$accessrulesqlselects} + FROM {quiz_overrides} o + {$joins} + WHERE o.id = ?"; + $params[] = $overrideid; + $override = $DB->get_record_sql($sql, $params, MUST_EXIST); $quizobj = quiz_settings::create($override->quiz); } else { $quizobj = quiz_settings::create_for_cmid($cmid); diff --git a/mod/quiz/overrides.php b/mod/quiz/overrides.php index be467be62435f..a4234ba95bc2f 100644 --- a/mod/quiz/overrides.php +++ b/mod/quiz/overrides.php @@ -23,6 +23,7 @@ */ use mod_quiz\quiz_settings; +use mod_quiz\access_manager; require_once(__DIR__ . '/../../config.php'); require_once($CFG->dirroot.'/mod/quiz/lib.php'); @@ -92,6 +93,8 @@ $colclasses = []; $headers = []; +[$overrideselects, $overridejoins, $overrideparams] = access_manager::get_override_settings_sql('o'); + // Fetch all overrides. if ($groupmode) { $headers[] = get_string('group'); @@ -99,15 +102,16 @@ if ($groups) { $params = ['quizid' => $quiz->id]; list($insql, $inparams) = $DB->get_in_or_equal(array_keys($groups), SQL_PARAMS_NAMED); - $params += $inparams; - $sql = "SELECT o.*, g.name + $sql = "SELECT o.*, g.name, {$overrideselects}} FROM {quiz_overrides} o JOIN {groups} g ON o.groupid = g.id - WHERE o.quiz = :quizid AND g.id $insql + {$overridejoins} + WHERE o.quiz = :quizid + AND g.id $insql ORDER BY g.name"; - $overrides = $DB->get_records_sql($sql, $params); + $overrides = $DB->get_records_sql($sql, array_merge($overrideparams, $params, $inparams)); } } else { @@ -141,16 +145,17 @@ $groupswhere = ' AND 1 = 2'; } - $overrides = $DB->get_records_sql(" - SELECT o.*, {$userfieldssql->selects} + $sql = "SELECT o.*, {$overrideselects}, {$userfieldssql->selects} FROM {quiz_overrides} o - JOIN {user} u ON o.userid = u.id - {$userfieldssql->joins} - $groupsjoin + JOIN {user} u ON u.id = o.userid + {$userfieldssql->joins} + {$overridejoins} + {$groupsjoin} WHERE o.quiz = :quizid - $groupswhere - ORDER BY $sort - ", array_merge($params, $userfieldssql->params)); + {$groupswhere} + ORDER BY $sort"; + $params = array_merge($params, $userfieldssql->params, $overrideparams); + $overrides = $DB->get_records_sql($sql, $params); } // Initialise table. @@ -229,6 +234,9 @@ get_string('enabled', 'quiz') : get_string('none', 'quiz'); } + // Access rule override table field. + [$fields, $values] = access_manager::add_override_table_fields($override, $fields, $values, $context); + // Prepare the information about who this override applies to. $extranamebit = $active ? '' : '*'; $usercells = [];