diff --git a/data/main/components.json b/data/main/components.json
index c11288c46..eff9d4bd3 100644
--- a/data/main/components.json
+++ b/data/main/components.json
@@ -44,7 +44,8 @@
"local": "local",
"h5plib": "h5p\/h5plib",
"paygw": "payment\/gateway",
- "smsgateway": "sms/gateway"
+ "smsgateway": "sms/gateway",
+ "quizaccessrule": "mod\/quiz\/accessrule"
},
"subsystems": {
"ai": "ai",
diff --git a/docs/apis/plugintypes/quizaccess/_examples/rule.md b/docs/apis/plugintypes/quizaccess/_examples/rule.md
new file mode 100644
index 000000000..f8be96b09
--- /dev/null
+++ b/docs/apis/plugintypes/quizaccess/_examples/rule.md
@@ -0,0 +1,4 @@
+
+The rule class defines the behaviour of your rule and how it will restrict access for users attempting a quiz.
+
+Please refer to the inline phpdocs of the [mod_quiz::access_rule_base class](https://github.com/moodle/moodle/blob/main/mod/quiz/classes/local/access_rule_base.php) for detailed descriptions of the functions and meaning.
diff --git a/docs/apis/plugintypes/quizaccess/_examples/rule.php b/docs/apis/plugintypes/quizaccess/_examples/rule.php
new file mode 100644
index 000000000..3732aac8f
--- /dev/null
+++ b/docs/apis/plugintypes/quizaccess/_examples/rule.php
@@ -0,0 +1,120 @@
+use mod_quiz\form\edit_override_form;
+use mod_quiz\form\preflight_check_form;
+use mod_quiz\quiz_settings;
+use mod_quiz\local\access_rule_base;
+use mod_quiz_mod_form;
+use MoodleQuickForm;
+
+class quizaccess_pluginname extends access_rule_base {
+
+ /**
+ * Below are methods inherited from mod_quiz\local\access_rule_base. All of these functions,
+ * are optional to rewrite - although it depends on the behaviour of your rule which will
+ * determine which functions should be reimplemented.
+ */
+
+ public function __construct($quizobj, $timenow) {
+ $this->quizobj = $quizobj;
+ $this->quiz = $quizobj->get_quiz();
+ $this->timenow = $timenow;
+ }
+
+ public static function make(quiz_settings $quizobj, $timenow, $canignoretimelimits) {
+ return null;
+ }
+
+ public function prevent_new_attempt($numprevattempts, $lastattempt) {
+ return false;
+ }
+
+ public function prevent_access() {
+ return false;
+ }
+
+ public function is_preflight_check_required($attemptid) {
+ return false;
+ }
+
+ public function add_preflight_check_form_fields(preflight_check_form $quizform,
+ MoodleQuickForm $mform, $attemptid) {
+ // Do nothing by default.
+ }
+
+ public function validate_preflight_check($data, $files, $errors, $attemptid) {
+ return $errors;
+ }
+
+ public function notify_preflight_check_passed($attemptid) {
+ // Do nothing by default.
+ }
+
+ public function current_attempt_finished() {
+ // Do nothing by default.
+ }
+
+ public function description() {
+ return '';
+ }
+
+ public function is_finished($numprevattempts, $lastattempt) {
+ return false;
+ }
+
+ public function end_time($attempt) {
+ return false;
+ }
+
+ public function time_left_display($attempt, $timenow) {
+ $endtime = $this->end_time($attempt);
+ if ($endtime === false) {
+ return false;
+ }
+ return $endtime - $timenow;
+ }
+
+ public function attempt_must_be_in_popup() {
+ return false;
+ }
+
+ public function get_popup_options() {
+ return [];
+ }
+
+ public function setup_attempt_page($page) {
+ // Do nothing by default.
+ }
+
+ public function get_superceded_rules() {
+ return [];
+ }
+
+ public static function add_settings_form_fields(
+ mod_quiz_mod_form $quizform, MoodleQuickForm $mform) {
+ // By default do nothing.
+ }
+
+ public static function validate_settings_form_fields(array $errors,
+ array $data, $files, mod_quiz_mod_form $quizform) {
+ return $errors;
+ }
+
+ public static function get_browser_security_choices() {
+ return [];
+ }
+
+ public static function save_settings($quiz) {
+ // By default do nothing.
+ }
+
+ public static function delete_settings($quiz) {
+ // By default do nothing.
+ }
+
+ public static function get_settings_sql($quizid) {
+ return ['', '', []];
+ }
+
+ public static function get_extra_settings($quizid) {
+ return [];
+ }
+}
diff --git a/docs/apis/plugintypes/quizaccess/_examples/rule_overridable.md b/docs/apis/plugintypes/quizaccess/_examples/rule_overridable.md
new file mode 100644
index 000000000..51d8c2b7f
--- /dev/null
+++ b/docs/apis/plugintypes/quizaccess/_examples/rule_overridable.md
@@ -0,0 +1,4 @@
+
+Most quiz settings can be overridden on a per user and/or group level and you can extend this ability to your rule as well. To make your rule overridable, you must implement the `rule_overridable` interface in your rule class definition.
+
+Please refer to the inline phpdocs of the [mod_quiz::rule_overridable interface](https://github.com/moodle/moodle/blob/main/mod/quiz/classes/local/rule_overridable.php) for detailed descriptions of the functions and meaning.
diff --git a/docs/apis/plugintypes/quizaccess/_examples/rule_overridable.php b/docs/apis/plugintypes/quizaccess/_examples/rule_overridable.php
new file mode 100644
index 000000000..58c57ed5d
--- /dev/null
+++ b/docs/apis/plugintypes/quizaccess/_examples/rule_overridable.php
@@ -0,0 +1,100 @@
+use mod_quiz\form\edit_override_form;
+use mod_quiz\local\access_rule_base;
+use mod_quiz\local\rule_overridable;
+use MoodleQuickForm;
+
+class quizaccess_pluginname extends access_rule_base implements rule_overridable {
+
+ /**
+ * All of the below rule_overridable interface functions will need to be implemented.
+ */
+
+ public static function add_override_form_fields(edit_override_form $quizform, MoodleQuickForm $mform): void {
+ // Use the $mform to add the rule override fields...
+ $mform->addElement(
+ 'select',
+ 'plgn_setting1',
+ get_string('plgn_setting1', 'quizaccess_pluginname'),
+ ['A', 'B', 'C'],
+ );
+
+ $mform->addElement(
+ 'select',
+ 'plgn_setting2',
+ get_string('plgn_setting2', 'quizaccess_pluginname'),
+ ['1', '2', '3'],
+ );
+ }
+
+ public static function get_override_form_section_header(): array {
+ // Return the label and content of the section header in an array.
+ return ['name' => 'pluginname', 'title' => get_string('pluginname', 'quizaccess_pluginname')];
+ }
+
+ public static function get_override_form_section_expand(edit_override_form $quizform): bool {
+ // Determine if rule section in override form should load expanded.
+ // Should typically return true if the quiz has existing rule settings.
+ global $DB;
+ return $DB->record_exists('quizaccess_pluginname', ['quiz' => $quizform->get_quiz()->id]);
+ }
+
+ public static function validate_override_form_fields(array $errors,
+ array $data, array $files, edit_override_form $quizform): array {
+ // Check and push to $errors array...
+ return $errors;
+ }
+
+ public static function save_override_settings(array $override): void {
+ // Save $override data to plugin settings table...
+ global $DB;
+
+ $plgnoverride = (object)[
+ 'overrideid' => $override['overrideid'],
+ 'setting1' => $override['plgnm_setting1'],
+ 'setting2' => $override['plgnm_setting2'],
+ ];
+
+ if ($plgnoverrideid = $DB->get_field('quizaccess_pluginname_overrides', 'id', ['overrideid' => $override['overrideid']])) {
+ $plgnoverride->id = $plgnoverrideid;
+ $DB->update_record('quizaccess_pluginname_overrides', $plgnoverride);
+ } else {
+ $DB->insert_record('quizaccess_pluginname_overrides', $plgnoverride);
+ }
+ }
+
+ public static function delete_override_settings($quizid, $overrides): void {
+ // Remove $overrides from $quiz.
+ global $DB;
+ $ids = array_column($overrides, 'id');
+ list($insql, $inparams) = $DB->get_in_or_equal($ids);
+ $DB->delete_records_select('quizaccess_pluginname_overrides', "id $insql", $inparams);
+ }
+
+ public static function get_override_setting_keys(): array {
+ // Return string array of all override form setting keys.
+ return ['plgnm_setting1', 'plgnm_setting2'];
+ }
+
+ public static function get_override_required_setting_keys(): array {
+ // Return string array of override form setting keys that are required.
+ return ['plgnm_setting1'];
+ }
+
+ public static function get_override_settings_sql($overridetablename): array {
+ // Return an array of selects, joins and parameters to be used to query relevant rule overrides...
+ return [
+ "plgnm.setting1 plgnm_setting1, plgnm.setting2 plgnm_setting2",
+ "LEFT JOIN {quizaccess_pluginname_overrides} plgnm ON plgnm.overrideid = {$overridetablename}.id",
+ [],
+ ];
+ }
+
+ public static function add_override_table_fields($override, $fields, $values, $context): array {
+ // Extend the override table view by adding fields and values that display the rule's overrides.
+ if (!empty($override->plgnm_setting1)) {
+ $fields[] = get_string('pluginname', 'quizaccess_pluginname');
+ $values[] = "{$override->plgnm_setting1}, {$override->plgnm_setting2}";
+ }
+ return [$fields, $values];
+ }
+}
diff --git a/docs/apis/plugintypes/quizaccess/index.md b/docs/apis/plugintypes/quizaccess/index.md
new file mode 100644
index 000000000..8c9b33f1b
--- /dev/null
+++ b/docs/apis/plugintypes/quizaccess/index.md
@@ -0,0 +1,86 @@
+---
+title: Quiz access rule sub-plugins
+tags:
+ - Quiz
+ - Access
+ - Rule
+ - Subplugin
+ - Plugintype
+ - Override
+---
+
+import { ComponentFileSummary } from '../../../_utils';
+
+Quiz access rule sub-plugins extend the ability to add conditions a user must meet to attempt a given quiz.
+
+The following rules are readily available as part of Moodle core:
+
+- `quizaccess_delaybetweenattempts`
+- `quizaccess_ipaddress`
+- `quizaccess_numattempts`
+- `quizaccess_offlineattempts`
+- `quizaccess_openclosedate`
+- `quizaccess_password`
+- `quizaccess_seb`
+- `quizaccess_securewindow`
+- `quizaccess_timelimit`
+
+## File structure
+
+Quiz access rule sub-plugins are located in the `/mod/quiz/accessrule` directory. A plugin should not include any custom files outside of it's own plugin folder.
+
+Each plugin is in a separate subdirectory and consists of a number of mandatory files and any other files the developer is going to use.
+
+
+ View an example directory layout for the `quizaccess_delaybetweenattempts` plugin.
+
+```console
+mod/quiz/accessrule/delaybetweenattempts
+├── classes
+│ └── privacy
+│ └── provider.php
+├── lang
+│ └── en
+│ └── quizaccess_delaybetweenattempts.php
+├── tests
+│ └── rule_test.php
+├── rule.php
+└── version.php
+```
+
+
+
+Some of the important files for the format plugintype are described below. See the [common plugin files](../commonfiles) documentation for details of other files which may be useful in your plugin.
+
+### rule.php
+
+import RuleFile from '!!raw-loader!./_examples/rule.php';
+import RuleDescription from './_examples/rule.md';
+
+
+
+import RuleOverridableFile from '!!raw-loader!./_examples/rule_overridable.php';
+import RuleOverridableDescription from './_examples/rule_overridable.md';
+
+
+
+:::info
+
+Implementing `rule_overridable` is not required but can enhance the usability of the rule.
+
+:::
diff --git a/project-words.txt b/project-words.txt
index 2516b1690..3732fdf7d 100644
--- a/project-words.txt
+++ b/project-words.txt
@@ -264,6 +264,7 @@ privatefiles
protectusernames
qeupgradehelper
quizaccess
+quizaccessrule
randomsamatch
recordset
recordsets