From fed4a7fbd4d67a05414304d48b789e69a235e7f7 Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Wed, 14 Dec 2022 18:07:22 +0000 Subject: [PATCH 01/30] MDL-76614 quiz: move quiz_access_manager -> mod_quiz\access_manager --- mod/quiz/accessmanager.php | 546 +---------------- mod/quiz/accessrule/accessrulebase.php | 2 +- .../classes/external/validate_quiz_keys.php | 1 - mod/quiz/accessrule/seb/rule.php | 2 +- mod/quiz/attemptlib.php | 15 +- mod/quiz/classes/access_manager.php | 564 ++++++++++++++++++ .../admin/browser_security_setting.php | 4 +- .../classes/completion/custom_completion.php | 6 +- mod/quiz/classes/external.php | 7 +- .../classes/question/bank/qbank_helper.php | 1 - mod/quiz/db/renamedclasses.php | 1 + mod/quiz/deprecatedlib.php | 4 +- mod/quiz/lib.php | 5 +- mod/quiz/locallib.php | 6 +- mod/quiz/mod_form.php | 9 +- mod/quiz/renderer.php | 7 +- mod/quiz/report/reportlib.php | 1 - mod/quiz/upgrade.txt | 4 +- mod/quiz/view.php | 3 +- 19 files changed, 611 insertions(+), 577 deletions(-) create mode 100644 mod/quiz/classes/access_manager.php diff --git a/mod/quiz/accessmanager.php b/mod/quiz/accessmanager.php index f1db4ba549b..df1c42fe9f1 100644 --- a/mod/quiz/accessmanager.php +++ b/mod/quiz/accessmanager.php @@ -15,549 +15,11 @@ // along with Moodle. If not, see . /** - * Classes to enforce the various access rules that can apply to a quiz. + * File only retained to prevent fatal errors in code that tries to require/include this. * - * @package mod_quiz - * @copyright 2009 Tim Hunt - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @todo MDL-76612 delete this file as part of Moodle 4.6 development. + * @deprecated This file is no longer required in Moodle 4.2+. */ - - defined('MOODLE_INTERNAL') || die(); -use mod_quiz\question\display_options; - -/** - * This class keeps track of the various access rules that apply to a particular - * quiz, with convinient methods for seeing whether access is allowed. - * - * @copyright 2009 Tim Hunt - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @since Moodle 2.2 - */ -class quiz_access_manager { - /** @var quiz the quiz settings object. */ - protected $quizobj; - /** @var int the time to be considered as 'now'. */ - protected $timenow; - /** @var array of quiz_access_rule_base. */ - protected $rules = array(); - - /** - * Create an instance for a particular quiz. - * @param object $quizobj An instance of the class quiz from attemptlib.php. - * The quiz we will be controlling access to. - * @param int $timenow The time to use as 'now'. - * @param bool $canignoretimelimits Whether this user is exempt from time - * limits (has_capability('mod/quiz:ignoretimelimits', ...)). - */ - public function __construct($quizobj, $timenow, $canignoretimelimits) { - $this->quizobj = $quizobj; - $this->timenow = $timenow; - $this->rules = $this->make_rules($quizobj, $timenow, $canignoretimelimits); - } - - /** - * Make all the rules relevant to a particular quiz. - * @param quiz $quizobj information about the quiz in question. - * @param int $timenow the time that should be considered as 'now'. - * @param bool $canignoretimelimits whether the current user is exempt from - * time limits by the mod/quiz:ignoretimelimits capability. - * @return array of {@link quiz_access_rule_base}s. - */ - protected function make_rules($quizobj, $timenow, $canignoretimelimits) { - - $rules = array(); - foreach (self::get_rule_classes() as $ruleclass) { - $rule = $ruleclass::make($quizobj, $timenow, $canignoretimelimits); - if ($rule) { - $rules[$ruleclass] = $rule; - } - } - - $superceededrules = array(); - foreach ($rules as $rule) { - $superceededrules += $rule->get_superceded_rules(); - } - - foreach ($superceededrules as $superceededrule) { - unset($rules['quizaccess_' . $superceededrule]); - } - - return $rules; - } - - /** - * @return array of all the installed rule class names. - */ - protected static function get_rule_classes() { - return core_component::get_plugin_list_with_class('quizaccess', '', 'rule.php'); - } - - /** - * Add any form fields that the access rules require to the settings form. - * - * Note that the standard plugins do not use this mechanism, becuase all their - * settings are stored in the quiz table. - * - * @param mod_quiz_mod_form $quizform the quiz settings form that is being built. - * @param MoodleQuickForm $mform the wrapped MoodleQuickForm. - */ - public static function add_settings_form_fields( - mod_quiz_mod_form $quizform, MoodleQuickForm $mform) { - - foreach (self::get_rule_classes() as $rule) { - $rule::add_settings_form_fields($quizform, $mform); - } - } - - /** - * The the options for the Browser security settings menu. - * - * @return array key => lang string. - */ - public static function get_browser_security_choices() { - $options = array('-' => get_string('none', 'quiz')); - foreach (self::get_rule_classes() as $rule) { - $options += $rule::get_browser_security_choices(); - } - return $options; - } - - /** - * Validate the data from any form fields added using {@link add_settings_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 mod_quiz_mod_form $quizform the quiz form object. - * @return array $errors the updated $errors array. - */ - public static function validate_settings_form_fields(array $errors, - array $data, $files, mod_quiz_mod_form $quizform) { - - foreach (self::get_rule_classes() as $rule) { - $errors = $rule::validate_settings_form_fields($errors, $data, $files, $quizform); - } - - return $errors; - } - - /** - * Save any submitted settings when the quiz settings form is submitted. - * - * Note that the standard plugins do not use this mechanism because their - * settings are stored in the quiz table. - * - * @param object $quiz the data from the quiz form, including $quiz->id - * which is the id of the quiz being saved. - */ - public static function save_settings($quiz) { - - foreach (self::get_rule_classes() as $rule) { - $rule::save_settings($quiz); - } - } - - /** - * Delete any rule-specific settings when the quiz is deleted. - * - * Note that the standard plugins do not use this mechanism because their - * settings are stored in the quiz table. - * - * @param object $quiz the data from the database, including $quiz->id - * which is the id of the quiz being deleted. - * @since Moodle 2.7.1, 2.6.4, 2.5.7 - */ - public static function delete_settings($quiz) { - - foreach (self::get_rule_classes() as $rule) { - $rule::delete_settings($quiz); - } - } - - /** - * Build the SQL for loading all the access settings in one go. - * @param int $quizid the quiz id. - * @param string $basefields initial part of the select list. - * @return array with two elements, the sql and the placeholder values. - * If $basefields is '' then you must allow for the possibility that - * there is no data to load, in which case this method returns $sql = ''. - */ - protected static function get_load_sql($quizid, $rules, $basefields) { - $allfields = $basefields; - $alljoins = '{quiz} quiz'; - $allparams = array('quizid' => $quizid); - - foreach ($rules as $rule) { - list($fields, $joins, $params) = $rule::get_settings_sql($quizid); - if ($fields) { - if ($allfields) { - $allfields .= ', '; - } - $allfields .= $fields; - } - if ($joins) { - $alljoins .= ' ' . $joins; - } - if ($params) { - $allparams += $params; - } - } - - if ($allfields === '') { - return array('', array()); - } - - return array("SELECT $allfields FROM $alljoins WHERE quiz.id = :quizid", $allparams); - } - - /** - * Load any settings required by the access rules. We try to do this with - * a single DB query. - * - * Note that the standard plugins do not use this mechanism, becuase all their - * settings are stored in the quiz table. - * - * @param int $quizid the quiz id. - * @return array setting value name => value. The value names should all - * start with the name of the corresponding plugin to avoid collisions. - */ - public static function load_settings($quizid) { - global $DB; - - $rules = self::get_rule_classes(); - list($sql, $params) = self::get_load_sql($quizid, $rules, ''); - - if ($sql) { - $data = (array) $DB->get_record_sql($sql, $params); - } else { - $data = array(); - } - - foreach ($rules as $rule) { - $data += $rule::get_extra_settings($quizid); - } - - return $data; - } - - /** - * Load the quiz settings and any settings required by the access rules. - * We try to do this with a single DB query. - * - * Note that the standard plugins do not use this mechanism, becuase all their - * settings are stored in the quiz table. - * - * @param int $quizid the quiz id. - * @return object mdl_quiz row with extra fields. - */ - public static function load_quiz_and_settings($quizid) { - global $DB; - - $rules = self::get_rule_classes(); - list($sql, $params) = self::get_load_sql($quizid, $rules, 'quiz.*'); - $quiz = $DB->get_record_sql($sql, $params, MUST_EXIST); - - foreach ($rules as $rule) { - foreach ($rule::get_extra_settings($quizid) as $name => $value) { - $quiz->$name = $value; - } - } - - return $quiz; - } - - /** - * @return array the class names of all the active rules. Mainly useful for - * debugging. - */ - public function get_active_rule_names() { - $classnames = array(); - foreach ($this->rules as $rule) { - $classnames[] = get_class($rule); - } - return $classnames; - } - - /** - * Accumulates an array of messages. - * @param array $messages the current list of messages. - * @param string|array $new the new messages or messages. - * @return array the updated array of messages. - */ - protected function accumulate_messages($messages, $new) { - if (is_array($new)) { - $messages = array_merge($messages, $new); - } else if (is_string($new) && $new) { - $messages[] = $new; - } - return $messages; - } - - /** - * Provide a description of the rules that apply to this quiz, such - * as is shown at the top of the quiz view page. Note that not all - * rules consider themselves important enough to output a description. - * - * @return array an array of description messages which may be empty. It - * would be sensible to output each one surrounded by <p> tags. - */ - public function describe_rules() { - $result = array(); - foreach ($this->rules as $rule) { - $result = $this->accumulate_messages($result, $rule->description()); - } - return $result; - } - - /** - * Whether or not a user should be allowed to start a new attempt at this quiz now. - * If there are any restrictions in force now, return an array of reasons why access - * should be blocked. If access is OK, return false. - * - * @param int $numattempts the number of previous attempts this user has made. - * @param object|false $lastattempt information about the user's last completed attempt. - * if there is not a previous attempt, the false is passed. - * @return mixed An array of reason why access is not allowed, or an empty array - * (== false) if access should be allowed. - */ - public function prevent_new_attempt($numprevattempts, $lastattempt) { - $reasons = array(); - foreach ($this->rules as $rule) { - $reasons = $this->accumulate_messages($reasons, - $rule->prevent_new_attempt($numprevattempts, $lastattempt)); - } - return $reasons; - } - - /** - * Whether the user should be blocked from starting a new attempt or continuing - * an attempt now. If there are any restrictions in force now, return an array - * of reasons why access should be blocked. If access is OK, return false. - * - * @return mixed An array of reason why access is not allowed, or an empty array - * (== false) if access should be allowed. - */ - public function prevent_access() { - $reasons = array(); - foreach ($this->rules as $rule) { - $reasons = $this->accumulate_messages($reasons, $rule->prevent_access()); - } - return $reasons; - } - - /** - * @param int|null $attemptid the id of the current attempt, if there is one, - * otherwise null. - * @return bool whether a check is required before the user starts/continues - * their attempt. - */ - public function is_preflight_check_required($attemptid) { - foreach ($this->rules as $rule) { - if ($rule->is_preflight_check_required($attemptid)) { - return true; - } - } - return false; - } - - /** - * Build the form required to do the pre-flight checks. - * @param moodle_url $url the form action URL. - * @param int|null $attemptid the id of the current attempt, if there is one, - * otherwise null. - * @return mod_quiz_preflight_check_form the form. - */ - public function get_preflight_check_form(moodle_url $url, $attemptid) { - // This form normally wants POST submissins. However, it also needs to - // accept GET submissions. Since formslib is strict, we have to detect - // which case we are in, and set the form property appropriately. - $method = 'post'; - if (!empty($_GET['_qf__mod_quiz_preflight_check_form'])) { - $method = 'get'; - } - return new mod_quiz_preflight_check_form($url->out_omit_querystring(), - array('rules' => $this->rules, 'quizobj' => $this->quizobj, - 'attemptid' => $attemptid, 'hidden' => $url->params()), $method); - } - - /** - * The pre-flight check has passed. This is a chance to record that fact in - * some way. - * @param int|null $attemptid the id of the current attempt, if there is one, - * otherwise null. - */ - public function notify_preflight_check_passed($attemptid) { - foreach ($this->rules as $rule) { - $rule->notify_preflight_check_passed($attemptid); - } - } - - /** - * Inform the rules that the current attempt is finished. This is use, for example - * by the password rule, to clear the flag in the session. - */ - public function current_attempt_finished() { - foreach ($this->rules as $rule) { - $rule->current_attempt_finished(); - } - } - - /** - * Do any of the rules mean that this student will no be allowed any further attempts at this - * quiz. Used, for example, to change the label by the grade displayed on the view page from - * 'your current grade is' to 'your final grade is'. - * - * @param int $numattempts the number of previous attempts this user has made. - * @param object $lastattempt information about the user's last completed attempt. - * @return bool true if there is no way the user will ever be allowed to attempt - * this quiz again. - */ - public function is_finished($numprevattempts, $lastattempt) { - foreach ($this->rules as $rule) { - if ($rule->is_finished($numprevattempts, $lastattempt)) { - return true; - } - } - return false; - } - - /** - * Sets up the attempt (review or summary) page with any properties required - * by the access rules. - * - * @param moodle_page $page the page object to initialise. - */ - public function setup_attempt_page($page) { - foreach ($this->rules as $rule) { - $rule->setup_attempt_page($page); - } - } - - /** - * Compute when the attempt must be submitted. - * - * @param object $attempt the data from the relevant quiz_attempts row. - * @return int|false the attempt close time. - * False if there is no limit. - */ - public function get_end_time($attempt) { - $timeclose = false; - foreach ($this->rules as $rule) { - $ruletimeclose = $rule->end_time($attempt); - if ($ruletimeclose !== false && ($timeclose === false || $ruletimeclose < $timeclose)) { - $timeclose = $ruletimeclose; - } - } - return $timeclose; - } - - /** - * Compute what should be displayed to the user for time remaining in this attempt. - * - * @param object $attempt the data from the relevant quiz_attempts row. - * @param int $timenow the time to consider as 'now'. - * @return int|false the number of seconds remaining for this attempt. - * False if no limit should be displayed. - */ - public function get_time_left_display($attempt, $timenow) { - $timeleft = false; - foreach ($this->rules as $rule) { - $ruletimeleft = $rule->time_left_display($attempt, $timenow); - if ($ruletimeleft !== false && ($timeleft === false || $ruletimeleft < $timeleft)) { - $timeleft = $ruletimeleft; - } - } - return $timeleft; - } - - /** - * @return bolean if this quiz should only be shown to students in a popup window. - */ - public function attempt_must_be_in_popup() { - foreach ($this->rules as $rule) { - if ($rule->attempt_must_be_in_popup()) { - return true; - } - } - return false; - } - - /** - * @return array any options that are required for showing the attempt page - * in a popup window. - */ - public function get_popup_options() { - $options = array(); - foreach ($this->rules as $rule) { - $options += $rule->get_popup_options(); - } - return $options; - } - - /** - * Send the user back to the quiz view page. Normally this is just a redirect, but - * If we were in a secure window, we close this window, and reload the view window we came from. - * - * This method does not return; - * - * @param mod_quiz_renderer $output the quiz renderer. - * @param string $message optional message to output while redirecting. - */ - public function back_to_view_page($output, $message = '') { - if ($this->attempt_must_be_in_popup()) { - echo $output->close_attempt_popup($this->quizobj->view_url(), $message); - die(); - } else { - redirect($this->quizobj->view_url(), $message); - } - } - - /** - * Make some text into a link to review the quiz, if that is appropriate. - * - * @param string $linktext some text. - * @param object $attempt the attempt object - * @return string some HTML, the $linktext either unmodified or wrapped in a - * link to the review page. - */ - public function make_review_link($attempt, $reviewoptions, $output) { - - // If the attempt is still open, don't link. - if (in_array($attempt->state, array(quiz_attempt::IN_PROGRESS, quiz_attempt::OVERDUE))) { - return $output->no_review_message(''); - } - - $when = quiz_attempt_state($this->quizobj->get_quiz(), $attempt); - $reviewoptions = display_options::make_from_quiz( - $this->quizobj->get_quiz(), $when); - - if (!$reviewoptions->attempt) { - return $output->no_review_message($this->quizobj->cannot_review_message($when, true)); - - } else { - return $output->review_link($this->quizobj->review_url($attempt->id), - $this->attempt_must_be_in_popup(), $this->get_popup_options()); - } - } - - /** - * Run the preflight checks using the given data in all the rules supporting them. - * - * @param array $data passed data for validation - * @param array $files un-used, Moodle seems to not support it anymore - * @param int|null $attemptid the id of the current attempt, if there is one, - * otherwise null. - * @return array of errors, empty array means no erros - * @since Moodle 3.1 - */ - public function validate_preflight_check($data, $files, $attemptid) { - $errors = array(); - foreach ($this->rules as $rule) { - if ($rule->is_preflight_check_required($attemptid)) { - $errors = $rule->validate_preflight_check($data, $files, $errors, $attemptid); - } - } - return $errors; - } -} +debugging('This file is no longer required in Moodle 4.2+. Please do not include/require it.', DEBUG_DEVELOPER); diff --git a/mod/quiz/accessrule/accessrulebase.php b/mod/quiz/accessrule/accessrulebase.php index d8495122076..3bccd0181f8 100644 --- a/mod/quiz/accessrule/accessrulebase.php +++ b/mod/quiz/accessrule/accessrulebase.php @@ -302,7 +302,7 @@ abstract class quiz_access_rule_base { /** * Return the bits of SQL needed to load all the settings from all the access * plugins in one DB query. The easiest way to understand what you need to do - * here is probalby to read the code of {@link quiz_access_manager::load_settings()}. + * here is probably to read the code of {@see access_manager::load_settings()}. * * If you have some settings that cannot be loaded in this way, then you can * use the {@link get_extra_settings()} method instead, but that has diff --git a/mod/quiz/accessrule/seb/classes/external/validate_quiz_keys.php b/mod/quiz/accessrule/seb/classes/external/validate_quiz_keys.php index 2375e18ec63..1ec74bb7405 100644 --- a/mod/quiz/accessrule/seb/classes/external/validate_quiz_keys.php +++ b/mod/quiz/accessrule/seb/classes/external/validate_quiz_keys.php @@ -29,7 +29,6 @@ use quiz; use quizaccess_seb\event\access_prevented; use quizaccess_seb\access_manager; -require_once($CFG->dirroot . '/mod/quiz/accessmanager.php'); require_once($CFG->dirroot . '/mod/quiz/attemptlib.php'); require_once($CFG->libdir . '/externallib.php'); diff --git a/mod/quiz/accessrule/seb/rule.php b/mod/quiz/accessrule/seb/rule.php index 1772a818237..a9f53bf01c9 100644 --- a/mod/quiz/accessrule/seb/rule.php +++ b/mod/quiz/accessrule/seb/rule.php @@ -228,7 +228,7 @@ class quizaccess_seb extends quiz_access_rule_base { /** * Return the bits of SQL needed to load all the settings from all the access * plugins in one DB query. The easiest way to understand what you need to do - * here is probalby to read the code of {@link quiz_access_manager::load_settings()}. + * here is probably to read the code of {@see \mod\quiz\access_manager::load_settings()}. * * If you have some settings that cannot be loaded in this way, then you can * use the {@link get_extra_settings()} method instead, but that has diff --git a/mod/quiz/attemptlib.php b/mod/quiz/attemptlib.php index 54475f19814..999bb9f6d81 100644 --- a/mod/quiz/attemptlib.php +++ b/mod/quiz/attemptlib.php @@ -27,6 +27,7 @@ defined('MOODLE_INTERNAL') || die(); +use mod_quiz\access_manager; use mod_quiz\question\bank\qbank_helper; use mod_quiz\question\display_options; @@ -89,7 +90,7 @@ class quiz { protected $questions = null; /** @var stdClass[] of quiz_section rows. */ protected $sections = null; - /** @var quiz_access_manager the access manager for this quiz. */ + /** @var access_manager the access manager for this quiz. */ protected $accessmanager = null; /** @var bool whether the current user has capability mod/quiz:preview. */ protected $ispreviewuser = null; @@ -123,7 +124,7 @@ class quiz { public static function create($quizid, $userid = null) { global $DB; - $quiz = quiz_access_manager::load_quiz_and_settings($quizid); + $quiz = access_manager::load_quiz_and_settings($quizid); $course = $DB->get_record('course', array('id' => $quiz->course), '*', MUST_EXIST); $cm = get_coursemodule_from_instance('quiz', $quiz->id, $course->id, false, MUST_EXIST); @@ -360,16 +361,16 @@ class quiz { } /** - * Return quiz_access_manager and instance of the quiz_access_manager class + * Return access_manager and instance of the access_manager class * for this quiz at this time. * * @param int $timenow the current time as a unix timestamp. - * @return quiz_access_manager and instance of the quiz_access_manager class + * @return access_manager and instance of the access_manager class * for this quiz at this time. */ public function get_access_manager($timenow) { if (is_null($this->accessmanager)) { - $this->accessmanager = new quiz_access_manager($this, $timenow, + $this->accessmanager = new access_manager($this, $timenow, has_capability('mod/quiz:ignoretimelimits', $this->context, null, false)); } return $this->accessmanager; @@ -665,7 +666,7 @@ class quiz_attempt { global $DB; $attempt = $DB->get_record('quiz_attempts', $conditions, '*', MUST_EXIST); - $quiz = quiz_access_manager::load_quiz_and_settings($attempt->quiz); + $quiz = access_manager::load_quiz_and_settings($attempt->quiz); $course = $DB->get_record('course', array('id' => $quiz->course), '*', MUST_EXIST); $cm = get_coursemodule_from_instance('quiz', $quiz->id, $course->id, false, MUST_EXIST); @@ -894,7 +895,7 @@ class quiz_attempt { /** * @param int $timenow the current time as a unix timestamp. - * @return quiz_access_manager and instance of the quiz_access_manager class + * @return access_manager and instance of the access_manager class * for this quiz at this time. */ public function get_access_manager($timenow) { diff --git a/mod/quiz/classes/access_manager.php b/mod/quiz/classes/access_manager.php new file mode 100644 index 00000000000..4fec0ca6d2b --- /dev/null +++ b/mod/quiz/classes/access_manager.php @@ -0,0 +1,564 @@ +. + +namespace mod_quiz; + +use core_component; +use mod_quiz\question\display_options; +use mod_quiz_mod_form; +use mod_quiz_preflight_check_form; +use mod_quiz_renderer; +use moodle_page; +use moodle_url; +use MoodleQuickForm; +use quiz; +use quiz_attempt; + +/** + * This class keeps track of the various access rules that apply to a particular + * quiz, with convinient methods for seeing whether access is allowed. + * + * @package mod_quiz + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 2.2 + */ +class access_manager { + /** @var quiz the quiz settings object. */ + protected $quizobj; + /** @var int the time to be considered as 'now'. */ + protected $timenow; + /** @var array of quiz_access_rule_base. */ + protected $rules = array(); + + /** + * Create an instance for a particular quiz. + * @param object $quizobj An instance of the class quiz from attemptlib.php. + * The quiz we will be controlling access to. + * @param int $timenow The time to use as 'now'. + * @param bool $canignoretimelimits Whether this user is exempt from time + * limits (has_capability('mod/quiz:ignoretimelimits', ...)). + */ + public function __construct($quizobj, $timenow, $canignoretimelimits) { + $this->quizobj = $quizobj; + $this->timenow = $timenow; + $this->rules = $this->make_rules($quizobj, $timenow, $canignoretimelimits); + } + + /** + * Make all the rules relevant to a particular quiz. + * @param quiz $quizobj information about the quiz in question. + * @param int $timenow the time that should be considered as 'now'. + * @param bool $canignoretimelimits whether the current user is exempt from + * time limits by the mod/quiz:ignoretimelimits capability. + * @return array of {@link quiz_access_rule_base}s. + */ + protected function make_rules($quizobj, $timenow, $canignoretimelimits) { + + $rules = array(); + foreach (self::get_rule_classes() as $ruleclass) { + $rule = $ruleclass::make($quizobj, $timenow, $canignoretimelimits); + if ($rule) { + $rules[$ruleclass] = $rule; + } + } + + $superceededrules = array(); + foreach ($rules as $rule) { + $superceededrules += $rule->get_superceded_rules(); + } + + foreach ($superceededrules as $superceededrule) { + unset($rules['quizaccess_' . $superceededrule]); + } + + return $rules; + } + + /** + * @return array of all the installed rule class names. + */ + protected static function get_rule_classes() { + return core_component::get_plugin_list_with_class('quizaccess', '', 'rule.php'); + } + + /** + * Add any form fields that the access rules require to the settings form. + * + * Note that the standard plugins do not use this mechanism, becuase all their + * settings are stored in the quiz table. + * + * @param mod_quiz_mod_form $quizform the quiz settings form that is being built. + * @param MoodleQuickForm $mform the wrapped MoodleQuickForm. + */ + public static function add_settings_form_fields( + mod_quiz_mod_form $quizform, MoodleQuickForm $mform) { + + foreach (self::get_rule_classes() as $rule) { + $rule::add_settings_form_fields($quizform, $mform); + } + } + + /** + * The the options for the Browser security settings menu. + * + * @return array key => lang string. + */ + public static function get_browser_security_choices() { + $options = array('-' => get_string('none', 'quiz')); + foreach (self::get_rule_classes() as $rule) { + $options += $rule::get_browser_security_choices(); + } + return $options; + } + + /** + * Validate the data from any form fields added using {@link add_settings_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 mod_quiz_mod_form $quizform the quiz form object. + * @return array $errors the updated $errors array. + */ + public static function validate_settings_form_fields(array $errors, + array $data, $files, mod_quiz_mod_form $quizform) { + + foreach (self::get_rule_classes() as $rule) { + $errors = $rule::validate_settings_form_fields($errors, $data, $files, $quizform); + } + + return $errors; + } + + /** + * Save any submitted settings when the quiz settings form is submitted. + * + * Note that the standard plugins do not use this mechanism because their + * settings are stored in the quiz table. + * + * @param object $quiz the data from the quiz form, including $quiz->id + * which is the id of the quiz being saved. + */ + public static function save_settings($quiz) { + + foreach (self::get_rule_classes() as $rule) { + $rule::save_settings($quiz); + } + } + + /** + * Delete any rule-specific settings when the quiz is deleted. + * + * Note that the standard plugins do not use this mechanism because their + * settings are stored in the quiz table. + * + * @param object $quiz the data from the database, including $quiz->id + * which is the id of the quiz being deleted. + * @since Moodle 2.7.1, 2.6.4, 2.5.7 + */ + public static function delete_settings($quiz) { + + foreach (self::get_rule_classes() as $rule) { + $rule::delete_settings($quiz); + } + } + + /** + * Build the SQL for loading all the access settings in one go. + * @param int $quizid the quiz id. + * @param string $basefields initial part of the select list. + * @return array with two elements, the sql and the placeholder values. + * If $basefields is '' then you must allow for the possibility that + * there is no data to load, in which case this method returns $sql = ''. + */ + protected static function get_load_sql($quizid, $rules, $basefields) { + $allfields = $basefields; + $alljoins = '{quiz} quiz'; + $allparams = array('quizid' => $quizid); + + foreach ($rules as $rule) { + list($fields, $joins, $params) = $rule::get_settings_sql($quizid); + if ($fields) { + if ($allfields) { + $allfields .= ', '; + } + $allfields .= $fields; + } + if ($joins) { + $alljoins .= ' ' . $joins; + } + if ($params) { + $allparams += $params; + } + } + + if ($allfields === '') { + return array('', array()); + } + + return array("SELECT $allfields FROM $alljoins WHERE quiz.id = :quizid", $allparams); + } + + /** + * Load any settings required by the access rules. We try to do this with + * a single DB query. + * + * Note that the standard plugins do not use this mechanism, becuase all their + * settings are stored in the quiz table. + * + * @param int $quizid the quiz id. + * @return array setting value name => value. The value names should all + * start with the name of the corresponding plugin to avoid collisions. + */ + public static function load_settings($quizid) { + global $DB; + + $rules = self::get_rule_classes(); + list($sql, $params) = self::get_load_sql($quizid, $rules, ''); + + if ($sql) { + $data = (array) $DB->get_record_sql($sql, $params); + } else { + $data = array(); + } + + foreach ($rules as $rule) { + $data += $rule::get_extra_settings($quizid); + } + + return $data; + } + + /** + * Load the quiz settings and any settings required by the access rules. + * We try to do this with a single DB query. + * + * Note that the standard plugins do not use this mechanism, becuase all their + * settings are stored in the quiz table. + * + * @param int $quizid the quiz id. + * @return object mdl_quiz row with extra fields. + */ + public static function load_quiz_and_settings($quizid) { + global $DB; + + $rules = self::get_rule_classes(); + list($sql, $params) = self::get_load_sql($quizid, $rules, 'quiz.*'); + $quiz = $DB->get_record_sql($sql, $params, MUST_EXIST); + + foreach ($rules as $rule) { + foreach ($rule::get_extra_settings($quizid) as $name => $value) { + $quiz->$name = $value; + } + } + + return $quiz; + } + + /** + * @return array the class names of all the active rules. Mainly useful for + * debugging. + */ + public function get_active_rule_names() { + $classnames = array(); + foreach ($this->rules as $rule) { + $classnames[] = get_class($rule); + } + return $classnames; + } + + /** + * Accumulates an array of messages. + * @param array $messages the current list of messages. + * @param string|array $new the new messages or messages. + * @return array the updated array of messages. + */ + protected function accumulate_messages($messages, $new) { + if (is_array($new)) { + $messages = array_merge($messages, $new); + } else if (is_string($new) && $new) { + $messages[] = $new; + } + return $messages; + } + + /** + * Provide a description of the rules that apply to this quiz, such + * as is shown at the top of the quiz view page. Note that not all + * rules consider themselves important enough to output a description. + * + * @return array an array of description messages which may be empty. It + * would be sensible to output each one surrounded by <p> tags. + */ + public function describe_rules() { + $result = array(); + foreach ($this->rules as $rule) { + $result = $this->accumulate_messages($result, $rule->description()); + } + return $result; + } + + /** + * Whether or not a user should be allowed to start a new attempt at this quiz now. + * If there are any restrictions in force now, return an array of reasons why access + * should be blocked. If access is OK, return false. + * + * @param int $numattempts the number of previous attempts this user has made. + * @param object|false $lastattempt information about the user's last completed attempt. + * if there is not a previous attempt, the false is passed. + * @return mixed An array of reason why access is not allowed, or an empty array + * (== false) if access should be allowed. + */ + public function prevent_new_attempt($numprevattempts, $lastattempt) { + $reasons = array(); + foreach ($this->rules as $rule) { + $reasons = $this->accumulate_messages($reasons, + $rule->prevent_new_attempt($numprevattempts, $lastattempt)); + } + return $reasons; + } + + /** + * Whether the user should be blocked from starting a new attempt or continuing + * an attempt now. If there are any restrictions in force now, return an array + * of reasons why access should be blocked. If access is OK, return false. + * + * @return mixed An array of reason why access is not allowed, or an empty array + * (== false) if access should be allowed. + */ + public function prevent_access() { + $reasons = array(); + foreach ($this->rules as $rule) { + $reasons = $this->accumulate_messages($reasons, $rule->prevent_access()); + } + return $reasons; + } + + /** + * @param int|null $attemptid the id of the current attempt, if there is one, + * otherwise null. + * @return bool whether a check is required before the user starts/continues + * their attempt. + */ + public function is_preflight_check_required($attemptid) { + foreach ($this->rules as $rule) { + if ($rule->is_preflight_check_required($attemptid)) { + return true; + } + } + return false; + } + + /** + * Build the form required to do the pre-flight checks. + * @param moodle_url $url the form action URL. + * @param int|null $attemptid the id of the current attempt, if there is one, + * otherwise null. + * @return mod_quiz_preflight_check_form the form. + */ + public function get_preflight_check_form(moodle_url $url, $attemptid) { + // This form normally wants POST submissins. However, it also needs to + // accept GET submissions. Since formslib is strict, we have to detect + // which case we are in, and set the form property appropriately. + $method = 'post'; + if (!empty($_GET['_qf__mod_quiz_preflight_check_form'])) { + $method = 'get'; + } + return new mod_quiz_preflight_check_form($url->out_omit_querystring(), + array('rules' => $this->rules, 'quizobj' => $this->quizobj, + 'attemptid' => $attemptid, 'hidden' => $url->params()), $method); + } + + /** + * The pre-flight check has passed. This is a chance to record that fact in + * some way. + * @param int|null $attemptid the id of the current attempt, if there is one, + * otherwise null. + */ + public function notify_preflight_check_passed($attemptid) { + foreach ($this->rules as $rule) { + $rule->notify_preflight_check_passed($attemptid); + } + } + + /** + * Inform the rules that the current attempt is finished. This is use, for example + * by the password rule, to clear the flag in the session. + */ + public function current_attempt_finished() { + foreach ($this->rules as $rule) { + $rule->current_attempt_finished(); + } + } + + /** + * Do any of the rules mean that this student will no be allowed any further attempts at this + * quiz. Used, for example, to change the label by the grade displayed on the view page from + * 'your current grade is' to 'your final grade is'. + * + * @param int $numattempts the number of previous attempts this user has made. + * @param object $lastattempt information about the user's last completed attempt. + * @return bool true if there is no way the user will ever be allowed to attempt + * this quiz again. + */ + public function is_finished($numprevattempts, $lastattempt) { + foreach ($this->rules as $rule) { + if ($rule->is_finished($numprevattempts, $lastattempt)) { + return true; + } + } + return false; + } + + /** + * Sets up the attempt (review or summary) page with any properties required + * by the access rules. + * + * @param moodle_page $page the page object to initialise. + */ + public function setup_attempt_page($page) { + foreach ($this->rules as $rule) { + $rule->setup_attempt_page($page); + } + } + + /** + * Compute when the attempt must be submitted. + * + * @param object $attempt the data from the relevant quiz_attempts row. + * @return int|false the attempt close time. + * False if there is no limit. + */ + public function get_end_time($attempt) { + $timeclose = false; + foreach ($this->rules as $rule) { + $ruletimeclose = $rule->end_time($attempt); + if ($ruletimeclose !== false && ($timeclose === false || $ruletimeclose < $timeclose)) { + $timeclose = $ruletimeclose; + } + } + return $timeclose; + } + + /** + * Compute what should be displayed to the user for time remaining in this attempt. + * + * @param object $attempt the data from the relevant quiz_attempts row. + * @param int $timenow the time to consider as 'now'. + * @return int|false the number of seconds remaining for this attempt. + * False if no limit should be displayed. + */ + public function get_time_left_display($attempt, $timenow) { + $timeleft = false; + foreach ($this->rules as $rule) { + $ruletimeleft = $rule->time_left_display($attempt, $timenow); + if ($ruletimeleft !== false && ($timeleft === false || $ruletimeleft < $timeleft)) { + $timeleft = $ruletimeleft; + } + } + return $timeleft; + } + + /** + * @return bolean if this quiz should only be shown to students in a popup window. + */ + public function attempt_must_be_in_popup() { + foreach ($this->rules as $rule) { + if ($rule->attempt_must_be_in_popup()) { + return true; + } + } + return false; + } + + /** + * @return array any options that are required for showing the attempt page + * in a popup window. + */ + public function get_popup_options() { + $options = array(); + foreach ($this->rules as $rule) { + $options += $rule->get_popup_options(); + } + return $options; + } + + /** + * Send the user back to the quiz view page. Normally this is just a redirect, but + * If we were in a secure window, we close this window, and reload the view window we came from. + * + * This method does not return; + * + * @param mod_quiz_renderer $output the quiz renderer. + * @param string $message optional message to output while redirecting. + */ + public function back_to_view_page($output, $message = '') { + if ($this->attempt_must_be_in_popup()) { + echo $output->close_attempt_popup($this->quizobj->view_url(), $message); + die(); + } else { + redirect($this->quizobj->view_url(), $message); + } + } + + /** + * Make some text into a link to review the quiz, if that is appropriate. + * + * @param string $linktext some text. + * @param object $attempt the attempt object + * @return string some HTML, the $linktext either unmodified or wrapped in a + * link to the review page. + */ + public function make_review_link($attempt, $reviewoptions, $output) { + + // If the attempt is still open, don't link. + if (in_array($attempt->state, array(quiz_attempt::IN_PROGRESS, quiz_attempt::OVERDUE))) { + return $output->no_review_message(''); + } + + $when = quiz_attempt_state($this->quizobj->get_quiz(), $attempt); + $reviewoptions = display_options::make_from_quiz( + $this->quizobj->get_quiz(), $when); + + if (!$reviewoptions->attempt) { + return $output->no_review_message($this->quizobj->cannot_review_message($when, true)); + + } else { + return $output->review_link($this->quizobj->review_url($attempt->id), + $this->attempt_must_be_in_popup(), $this->get_popup_options()); + } + } + + /** + * Run the preflight checks using the given data in all the rules supporting them. + * + * @param array $data passed data for validation + * @param array $files un-used, Moodle seems to not support it anymore + * @param int|null $attemptid the id of the current attempt, if there is one, + * otherwise null. + * @return array of errors, empty array means no erros + * @since Moodle 3.1 + */ + public function validate_preflight_check($data, $files, $attemptid) { + $errors = array(); + foreach ($this->rules as $rule) { + if ($rule->is_preflight_check_required($attemptid)) { + $errors = $rule->validate_preflight_check($data, $files, $errors, $attemptid); + } + } + return $errors; + } +} diff --git a/mod/quiz/classes/admin/browser_security_setting.php b/mod/quiz/classes/admin/browser_security_setting.php index e7be3f35c72..bea427eba71 100644 --- a/mod/quiz/classes/admin/browser_security_setting.php +++ b/mod/quiz/classes/admin/browser_security_setting.php @@ -16,6 +16,8 @@ namespace mod_quiz\admin; +use mod_quiz\access_manager; + /** * Admin settings class for the quiz browser security option. * @@ -35,7 +37,7 @@ class browser_security_setting extends \admin_setting_configselect_with_advanced } require_once($CFG->dirroot . '/mod/quiz/locallib.php'); - $this->choices = \quiz_access_manager::get_browser_security_choices(); + $this->choices = access_manager::get_browser_security_choices(); return true; } diff --git a/mod/quiz/classes/completion/custom_completion.php b/mod/quiz/classes/completion/custom_completion.php index 3502a92b378..3921c02e37e 100644 --- a/mod/quiz/classes/completion/custom_completion.php +++ b/mod/quiz/classes/completion/custom_completion.php @@ -20,10 +20,8 @@ namespace mod_quiz\completion; use context_module; use core_completion\activity_custom_completion; -use grade_grade; -use grade_item; use quiz; -use quiz_access_manager; +use mod_quiz\access_manager; /** * Activity custom completion subclass for the quiz activity. @@ -72,7 +70,7 @@ class custom_completion extends activity_custom_completion { $lastfinishedattempt = end($attempts); $context = context_module::instance($this->cm->id); $quizobj = quiz::create($this->cm->instance, $this->userid); - $accessmanager = new quiz_access_manager( + $accessmanager = new access_manager( $quizobj, time(), has_capability('mod/quiz:ignoretimelimits', $context, $this->userid, false) diff --git a/mod/quiz/classes/external.php b/mod/quiz/classes/external.php index b1b5ecd17ae..024a3dfb1b4 100644 --- a/mod/quiz/classes/external.php +++ b/mod/quiz/classes/external.php @@ -25,6 +25,7 @@ */ use core_course\external\helper_for_get_mods_by_courses; +use mod_quiz\access_manager; defined('MOODLE_INTERNAL') || die; @@ -112,7 +113,7 @@ class mod_quiz_external extends external_api { $timenow = time(); $quizobj = quiz::create($quiz->id, $USER->id); - $accessmanager = new quiz_access_manager($quizobj, $timenow, has_capability('mod/quiz:ignoretimelimits', + $accessmanager = new access_manager($quizobj, $timenow, has_capability('mod/quiz:ignoretimelimits', $context, null, false)); // Fields the user could see if have access to the quiz. @@ -1810,7 +1811,7 @@ class mod_quiz_external extends external_api { $quizobj = quiz::create($cm->instance, $USER->id); $ignoretimelimits = has_capability('mod/quiz:ignoretimelimits', $context, null, false); $timenow = time(); - $accessmanager = new quiz_access_manager($quizobj, $timenow, $ignoretimelimits); + $accessmanager = new access_manager($quizobj, $timenow, $ignoretimelimits); $result['accessrules'] = $accessmanager->describe_rules(); $result['activerulenames'] = $accessmanager->get_active_rule_names(); @@ -1896,7 +1897,7 @@ class mod_quiz_external extends external_api { $quizobj = quiz::create($cm->instance, $USER->id); $ignoretimelimits = has_capability('mod/quiz:ignoretimelimits', $context, null, false); $timenow = time(); - $accessmanager = new quiz_access_manager($quizobj, $timenow, $ignoretimelimits); + $accessmanager = new access_manager($quizobj, $timenow, $ignoretimelimits); $attempts = quiz_get_user_attempts($quiz->id, $USER->id, 'finished', true); $lastfinishedattempt = end($attempts); diff --git a/mod/quiz/classes/question/bank/qbank_helper.php b/mod/quiz/classes/question/bank/qbank_helper.php index f28000c274c..b719974b756 100644 --- a/mod/quiz/classes/question/bank/qbank_helper.php +++ b/mod/quiz/classes/question/bank/qbank_helper.php @@ -23,7 +23,6 @@ use qubaid_condition; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/question/engine/bank.php'); -require_once($CFG->dirroot . '/mod/quiz/accessmanager.php'); require_once($CFG->dirroot . '/mod/quiz/attemptlib.php'); /** diff --git a/mod/quiz/db/renamedclasses.php b/mod/quiz/db/renamedclasses.php index 4904067348e..9d953c12f9b 100644 --- a/mod/quiz/db/renamedclasses.php +++ b/mod/quiz/db/renamedclasses.php @@ -51,4 +51,5 @@ $renamedclasses = [ 'mod_quiz_attempts_report_form' => 'mod_quiz\local\reports\attempts_report_options_form', 'mod_quiz_attempts_report_options' => 'mod_quiz\local\reports\attempts_report_options', 'quiz_attempts_report_table' => 'mod_quiz\local\reports\attempts_report_table', + 'quiz_access_manager' => 'mod_quiz\access_manager', ]; diff --git a/mod/quiz/deprecatedlib.php b/mod/quiz/deprecatedlib.php index 07c39ea8cfc..f430d76d502 100644 --- a/mod/quiz/deprecatedlib.php +++ b/mod/quiz/deprecatedlib.php @@ -22,6 +22,8 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +use mod_quiz\access_manager; + /** * Internal function used in quiz_get_completion_state. Check passing grade (or no attempts left) requirement for completion. * @@ -69,7 +71,7 @@ function quiz_completion_check_passing_grade_or_all_attempts($course, $cm, $user $lastfinishedattempt = end($attempts); $context = context_module::instance($cm->id); $quizobj = quiz::create($quiz->id, $userid); - $accessmanager = new quiz_access_manager($quizobj, time(), + $accessmanager = new access_manager($quizobj, time(), has_capability('mod/quiz:ignoretimelimits', $context, $userid, false)); return $accessmanager->is_finished(count($attempts), $lastfinishedattempt); diff --git a/mod/quiz/lib.php b/mod/quiz/lib.php index 8aaa204b151..7f0bf9daa1b 100644 --- a/mod/quiz/lib.php +++ b/mod/quiz/lib.php @@ -28,6 +28,7 @@ defined('MOODLE_INTERNAL') || die(); +use mod_quiz\access_manager; use mod_quiz\question\bank\custom_view; use mod_quiz\question\display_options; use mod_quiz\question\qubaids_for_quiz; @@ -190,7 +191,7 @@ function quiz_delete_instance($id) { $DB->delete_records('quiz_feedback', array('quizid' => $quiz->id)); - quiz_access_manager::delete_settings($quiz); + access_manager::delete_settings($quiz); $events = $DB->get_records('event', array('modulename' => 'quiz', 'instance' => $quiz->id)); foreach ($events as $event) { @@ -1236,7 +1237,7 @@ function quiz_after_add_or_update($quiz) { } // Store any settings belonging to the access rules. - quiz_access_manager::save_settings($quiz); + access_manager::save_settings($quiz); // Update the events relating to this quiz. quiz_update_events($quiz); diff --git a/mod/quiz/locallib.php b/mod/quiz/locallib.php index 00d8b14161a..f7abd0a28e7 100644 --- a/mod/quiz/locallib.php +++ b/mod/quiz/locallib.php @@ -31,7 +31,6 @@ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/mod/quiz/lib.php'); -require_once($CFG->dirroot . '/mod/quiz/accessmanager.php'); require_once($CFG->dirroot . '/mod/quiz/accessmanager_form.php'); require_once($CFG->dirroot . '/mod/quiz/renderer.php'); require_once($CFG->dirroot . '/mod/quiz/attemptlib.php'); @@ -39,6 +38,7 @@ require_once($CFG->libdir . '/completionlib.php'); require_once($CFG->libdir . '/filelib.php'); require_once($CFG->libdir . '/questionlib.php'); +use mod_quiz\access_manager; use mod_quiz\question\bank\qbank_helper; use mod_quiz\question\display_options; @@ -2438,7 +2438,7 @@ function quiz_view($quiz, $course, $cm, $context) { * Validate permissions for creating a new attempt and start a new preview attempt if required. * * @param quiz $quizobj quiz object - * @param quiz_access_manager $accessmanager quiz access manager + * @param access_manager $accessmanager quiz access manager * @param bool $forcenew whether was required to start a new preview attempt * @param int $page page to jump to in the attempt * @param bool $redirect whether to redirect or throw exceptions (for web or ws usage) @@ -2446,7 +2446,7 @@ function quiz_view($quiz, $course, $cm, $context) { * @throws moodle_quiz_exception * @since Moodle 3.1 */ -function quiz_validate_new_attempt(quiz $quizobj, quiz_access_manager $accessmanager, $forcenew, $page, $redirect) { +function quiz_validate_new_attempt(quiz $quizobj, access_manager $accessmanager, $forcenew, $page, $redirect) { global $DB, $USER; $timenow = time(); diff --git a/mod/quiz/mod_form.php b/mod/quiz/mod_form.php index 772b6ddfd7c..a9fec29231b 100644 --- a/mod/quiz/mod_form.php +++ b/mod/quiz/mod_form.php @@ -28,6 +28,7 @@ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/course/moodleform_mod.php'); require_once($CFG->dirroot . '/mod/quiz/locallib.php'); +use mod_quiz\access_manager; use mod_quiz\question\display_options; /** @@ -289,11 +290,11 @@ class mod_quiz_mod_form extends moodleform_mod { // Browser security choices. $mform->addElement('select', 'browsersecurity', get_string('browsersecurity', 'quiz'), - quiz_access_manager::get_browser_security_choices()); + access_manager::get_browser_security_choices()); $mform->addHelpButton('browsersecurity', 'browsersecurity', 'quiz'); // Any other rule plugins. - quiz_access_manager::add_settings_form_fields($this, $mform); + access_manager::add_settings_form_fields($this, $mform); // ------------------------------------------------------------------------------- $mform->addElement('header', 'overallfeedbackhdr', get_string('overallfeedback', 'quiz')); @@ -479,7 +480,7 @@ class mod_quiz_mod_form extends moodleform_mod { // Load any settings belonging to the access rules. if (!empty($toform['instance'])) { - $accesssettings = quiz_access_manager::load_settings($toform['instance']); + $accesssettings = access_manager::load_settings($toform['instance']); foreach ($accesssettings as $name => $value) { $toform[$name] = $value; } @@ -589,7 +590,7 @@ class mod_quiz_mod_form extends moodleform_mod { unset($errors['gradepass']); } // Any other rule plugins. - $errors = quiz_access_manager::validate_settings_form_fields($errors, $data, $files, $this); + $errors = access_manager::validate_settings_form_fields($errors, $data, $files, $this); return $errors; } diff --git a/mod/quiz/renderer.php b/mod/quiz/renderer.php index 0055f5b7343..3c33af87a05 100644 --- a/mod/quiz/renderer.php +++ b/mod/quiz/renderer.php @@ -25,6 +25,7 @@ defined('MOODLE_INTERNAL') || die(); +use mod_quiz\access_manager; use mod_quiz\question\display_options; @@ -444,7 +445,7 @@ class mod_quiz_renderer extends plugin_renderer_base { * * @param quiz_attempt $attemptobj Instance of quiz_attempt * @param int $page Current page number - * @param quiz_access_manager $accessmanager Instance of quiz_access_manager + * @param access_manager $accessmanager Instance of access_manager * @param array $messages An array of messages * @param array $slots Contains an array of integers that relate to questions * @param int $id The ID of an attempt @@ -912,7 +913,7 @@ class mod_quiz_renderer extends plugin_renderer_base { // Calling code was not updated since the API change. debugging('The third argument to start_attempt_button should now be the ' . 'mod_quiz_preflight_check_form from ' . - 'quiz_access_manager::get_preflight_check_form, not a warning message string.'); + 'access_manager::get_preflight_check_form, not a warning message string.'); } $button = new single_button($url, $buttontext, 'post', true); @@ -1430,7 +1431,7 @@ class mod_quiz_view_object { public $attempts; /** @var array $attemptobjs quiz_attempt objects corresponding to $attempts. */ public $attemptobjs; - /** @var quiz_access_manager $accessmanager contains various access rules. */ + /** @var access_manager $accessmanager contains various access rules. */ public $accessmanager; /** @var bool $canreviewmine whether the current user has the capability to * review their own attempts. */ diff --git a/mod/quiz/report/reportlib.php b/mod/quiz/report/reportlib.php index da0e996af74..611428cdc8f 100644 --- a/mod/quiz/report/reportlib.php +++ b/mod/quiz/report/reportlib.php @@ -28,7 +28,6 @@ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/mod/quiz/lib.php'); require_once($CFG->dirroot . '/mod/quiz/attemptlib.php'); require_once($CFG->libdir . '/filelib.php'); -require_once($CFG->dirroot . '/mod/quiz/accessmanager.php'); use mod_quiz\question\display_options; diff --git a/mod/quiz/upgrade.txt b/mod/quiz/upgrade.txt index 9c27c08735b..f9b020dabf0 100644 --- a/mod/quiz/upgrade.txt +++ b/mod/quiz/upgrade.txt @@ -35,14 +35,16 @@ This files describes API changes in the quiz code. - mod_quiz_attempts_report_form => mod_quiz\local\reports\attempts_report_options_form - mod_quiz_attempts_report_options => mod_quiz\local\reports\attempts_report_options - quiz_attempts_report_table => mod_quiz\local\reports\attempts_report_table + - quiz_access_manager => mod_quiz\access_manager - As part of the clean-up, the following files are no longer required, and if you try to +* As part of the clean-up, the following files are no longer required, and if you try to include them, you will get a debugging notices telling you not to: - mod/quiz/report/attemptsreport.php - mod/quiz/report/attemptsreport_form.php - mod/quiz/report/attemptsreport_options.php - mod/quiz/report/attemptsreport_table.php - mod/quiz/report/default.php + - mod/quiz/accessmanager.php === 4.1 === diff --git a/mod/quiz/view.php b/mod/quiz/view.php index f4e4647f2fe..a176e8d839b 100644 --- a/mod/quiz/view.php +++ b/mod/quiz/view.php @@ -23,6 +23,7 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +use mod_quiz\access_manager; require_once(__DIR__ . '/../../config.php'); require_once($CFG->libdir.'/gradelib.php'); @@ -65,7 +66,7 @@ $canpreview = has_capability('mod/quiz:preview', $context); // Create an object to manage all the other (non-roles) access rules. $timenow = time(); $quizobj = quiz::create($cm->instance, $USER->id); -$accessmanager = new quiz_access_manager($quizobj, $timenow, +$accessmanager = new access_manager($quizobj, $timenow, has_capability('mod/quiz:ignoretimelimits', $context, null, false)); $quiz = $quizobj->get_quiz(); From b3d59e5ad84cb7d52301f54c46dabc6548d9293e Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Wed, 14 Dec 2022 22:35:00 +0000 Subject: [PATCH 02/30] MDL-76614 quiz: clean up class access_manager --- mod/quiz/classes/access_manager.php | 191 ++++++++++++++++------------ mod/quiz/classes/external.php | 4 +- 2 files changed, 109 insertions(+), 86 deletions(-) diff --git a/mod/quiz/classes/access_manager.php b/mod/quiz/classes/access_manager.php index 4fec0ca6d2b..2f40e81ed70 100644 --- a/mod/quiz/classes/access_manager.php +++ b/mod/quiz/classes/access_manager.php @@ -26,10 +26,13 @@ use moodle_url; use MoodleQuickForm; use quiz; use quiz_attempt; +use stdClass; /** - * This class keeps track of the various access rules that apply to a particular - * quiz, with convinient methods for seeing whether access is allowed. + * This class aggregates the access rules that apply to a particular quiz. + * + * This provides a convenient API which other parts of the quiz code can use + * to interact with the access rules. * * @package mod_quiz * @copyright 2009 Tim Hunt @@ -39,20 +42,23 @@ use quiz_attempt; class access_manager { /** @var quiz the quiz settings object. */ protected $quizobj; + /** @var int the time to be considered as 'now'. */ protected $timenow; - /** @var array of quiz_access_rule_base. */ - protected $rules = array(); + + /** @var \quiz_access_rule_base instances of the active rules for this quiz. */ + protected $rules = []; /** * Create an instance for a particular quiz. - * @param object $quizobj An instance of the class quiz from attemptlib.php. + * + * @param quiz $quizobj An instance of the class quiz from attemptlib.php. * The quiz we will be controlling access to. * @param int $timenow The time to use as 'now'. * @param bool $canignoretimelimits Whether this user is exempt from time * limits (has_capability('mod/quiz:ignoretimelimits', ...)). */ - public function __construct($quizobj, $timenow, $canignoretimelimits) { + public function __construct(quiz $quizobj, int $timenow, bool $canignoretimelimits) { $this->quizobj = $quizobj; $this->timenow = $timenow; $this->rules = $this->make_rules($quizobj, $timenow, $canignoretimelimits); @@ -60,15 +66,16 @@ class access_manager { /** * Make all the rules relevant to a particular quiz. + * * @param quiz $quizobj information about the quiz in question. * @param int $timenow the time that should be considered as 'now'. * @param bool $canignoretimelimits whether the current user is exempt from * time limits by the mod/quiz:ignoretimelimits capability. - * @return array of {@link quiz_access_rule_base}s. + * @return array of {@see quiz_access_rule_base}s. */ - protected function make_rules($quizobj, $timenow, $canignoretimelimits) { + protected function make_rules(quiz $quizobj, int $timenow, bool $canignoretimelimits): array { - $rules = array(); + $rules = []; foreach (self::get_rule_classes() as $ruleclass) { $rule = $ruleclass::make($quizobj, $timenow, $canignoretimelimits); if ($rule) { @@ -76,7 +83,7 @@ class access_manager { } } - $superceededrules = array(); + $superceededrules = []; foreach ($rules as $rule) { $superceededrules += $rule->get_superceded_rules(); } @@ -89,9 +96,11 @@ class access_manager { } /** - * @return array of all the installed rule class names. + * Get that names of all the installed rule classes. + * + * @return array of class names. */ - protected static function get_rule_classes() { + protected static function get_rule_classes(): array { return core_component::get_plugin_list_with_class('quizaccess', '', 'rule.php'); } @@ -105,7 +114,7 @@ class access_manager { * @param MoodleQuickForm $mform the wrapped MoodleQuickForm. */ public static function add_settings_form_fields( - mod_quiz_mod_form $quizform, MoodleQuickForm $mform) { + mod_quiz_mod_form $quizform, MoodleQuickForm $mform): void { foreach (self::get_rule_classes() as $rule) { $rule::add_settings_form_fields($quizform, $mform); @@ -117,8 +126,8 @@ class access_manager { * * @return array key => lang string. */ - public static function get_browser_security_choices() { - $options = array('-' => get_string('none', 'quiz')); + public static function get_browser_security_choices(): array { + $options = ['-' => get_string('none', 'quiz')]; foreach (self::get_rule_classes() as $rule) { $options += $rule::get_browser_security_choices(); } @@ -126,7 +135,8 @@ class access_manager { } /** - * Validate the data from any form fields added using {@link add_settings_form_fields()}. + * Validate the data from any form fields added using {@see add_settings_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. @@ -134,7 +144,7 @@ class access_manager { * @return array $errors the updated $errors array. */ public static function validate_settings_form_fields(array $errors, - array $data, $files, mod_quiz_mod_form $quizform) { + array $data, array $files, mod_quiz_mod_form $quizform): array { foreach (self::get_rule_classes() as $rule) { $errors = $rule::validate_settings_form_fields($errors, $data, $files, $quizform); @@ -149,10 +159,10 @@ class access_manager { * Note that the standard plugins do not use this mechanism because their * settings are stored in the quiz table. * - * @param object $quiz the data from the quiz form, including $quiz->id + * @param stdClass $quiz the data from the quiz form, including $quiz->id * which is the id of the quiz being saved. */ - public static function save_settings($quiz) { + public static function save_settings(stdClass $quiz): void { foreach (self::get_rule_classes() as $rule) { $rule::save_settings($quiz); @@ -165,11 +175,11 @@ class access_manager { * Note that the standard plugins do not use this mechanism because their * settings are stored in the quiz table. * - * @param object $quiz the data from the database, including $quiz->id + * @param stdClass $quiz the data from the database, including $quiz->id * which is the id of the quiz being deleted. * @since Moodle 2.7.1, 2.6.4, 2.5.7 */ - public static function delete_settings($quiz) { + public static function delete_settings(stdClass $quiz): void { foreach (self::get_rule_classes() as $rule) { $rule::delete_settings($quiz); @@ -178,19 +188,21 @@ class access_manager { /** * Build the SQL for loading all the access settings in one go. + * * @param int $quizid the quiz id. + * @param array $rules list of rule plugins, from {@see get_rule_classes()}. * @param string $basefields initial part of the select list. * @return array with two elements, the sql and the placeholder values. * If $basefields is '' then you must allow for the possibility that * there is no data to load, in which case this method returns $sql = ''. */ - protected static function get_load_sql($quizid, $rules, $basefields) { + protected static function get_load_sql(int $quizid, array $rules, string $basefields): array { $allfields = $basefields; $alljoins = '{quiz} quiz'; - $allparams = array('quizid' => $quizid); + $allparams = ['quizid' => $quizid]; foreach ($rules as $rule) { - list($fields, $joins, $params) = $rule::get_settings_sql($quizid); + [$fields, $joins, $params] = $rule::get_settings_sql($quizid); if ($fields) { if ($allfields) { $allfields .= ', '; @@ -206,10 +218,10 @@ class access_manager { } if ($allfields === '') { - return array('', array()); + return ['', []]; } - return array("SELECT $allfields FROM $alljoins WHERE quiz.id = :quizid", $allparams); + return ["SELECT $allfields FROM $alljoins WHERE quiz.id = :quizid", $allparams]; } /** @@ -223,16 +235,16 @@ class access_manager { * @return array setting value name => value. The value names should all * start with the name of the corresponding plugin to avoid collisions. */ - public static function load_settings($quizid) { + public static function load_settings(int $quizid): array { global $DB; $rules = self::get_rule_classes(); - list($sql, $params) = self::get_load_sql($quizid, $rules, ''); + [$sql, $params] = self::get_load_sql($quizid, $rules, ''); if ($sql) { $data = (array) $DB->get_record_sql($sql, $params); } else { - $data = array(); + $data = []; } foreach ($rules as $rule) { @@ -250,13 +262,13 @@ class access_manager { * settings are stored in the quiz table. * * @param int $quizid the quiz id. - * @return object mdl_quiz row with extra fields. + * @return stdClass mdl_quiz row with extra fields. */ - public static function load_quiz_and_settings($quizid) { + public static function load_quiz_and_settings(int $quizid): stdClass { global $DB; $rules = self::get_rule_classes(); - list($sql, $params) = self::get_load_sql($quizid, $rules, 'quiz.*'); + [$sql, $params] = self::get_load_sql($quizid, $rules, 'quiz.*'); $quiz = $DB->get_record_sql($sql, $params, MUST_EXIST); foreach ($rules as $rule) { @@ -269,11 +281,14 @@ class access_manager { } /** - * @return array the class names of all the active rules. Mainly useful for - * debugging. + * Get an array of the class names of all the active rules. + * + * Mainly useful for debugging. + * + * @return array */ - public function get_active_rule_names() { - $classnames = array(); + public function get_active_rule_names(): array { + $classnames = []; foreach ($this->rules as $rule) { $classnames[] = get_class($rule); } @@ -282,11 +297,12 @@ class access_manager { /** * Accumulates an array of messages. + * * @param array $messages the current list of messages. * @param string|array $new the new messages or messages. * @return array the updated array of messages. */ - protected function accumulate_messages($messages, $new) { + protected function accumulate_messages(array $messages, $new): array { if (is_array($new)) { $messages = array_merge($messages, $new); } else if (is_string($new) && $new) { @@ -303,8 +319,8 @@ class access_manager { * @return array an array of description messages which may be empty. It * would be sensible to output each one surrounded by <p> tags. */ - public function describe_rules() { - $result = array(); + public function describe_rules(): array { + $result = []; foreach ($this->rules as $rule) { $result = $this->accumulate_messages($result, $rule->description()); } @@ -312,18 +328,18 @@ class access_manager { } /** - * Whether or not a user should be allowed to start a new attempt at this quiz now. + * Whether a user should be allowed to start a new attempt at this quiz now. * If there are any restrictions in force now, return an array of reasons why access * should be blocked. If access is OK, return false. * - * @param int $numattempts the number of previous attempts this user has made. - * @param object|false $lastattempt information about the user's last completed attempt. + * @param int $numprevattempts the number of previous attempts this user has made. + * @param stdClass|false $lastattempt information about the user's last completed attempt. * if there is not a previous attempt, the false is passed. - * @return mixed An array of reason why access is not allowed, or an empty array + * @return array an array of reason why access is not allowed. An empty array * (== false) if access should be allowed. */ - public function prevent_new_attempt($numprevattempts, $lastattempt) { - $reasons = array(); + public function prevent_new_attempt(int $numprevattempts, $lastattempt): array { + $reasons = []; foreach ($this->rules as $rule) { $reasons = $this->accumulate_messages($reasons, $rule->prevent_new_attempt($numprevattempts, $lastattempt)); @@ -336,11 +352,11 @@ class access_manager { * an attempt now. If there are any restrictions in force now, return an array * of reasons why access should be blocked. If access is OK, return false. * - * @return mixed An array of reason why access is not allowed, or an empty array + * @return array An array of reason why access is not allowed, or an empty array * (== false) if access should be allowed. */ - public function prevent_access() { - $reasons = array(); + public function prevent_access(): array { + $reasons = []; foreach ($this->rules as $rule) { $reasons = $this->accumulate_messages($reasons, $rule->prevent_access()); } @@ -348,12 +364,13 @@ class access_manager { } /** + * Is a UI check is required before the user starts/continues their attempt. + * * @param int|null $attemptid the id of the current attempt, if there is one, * otherwise null. - * @return bool whether a check is required before the user starts/continues - * their attempt. + * @return bool whether a check is required. */ - public function is_preflight_check_required($attemptid) { + public function is_preflight_check_required(?int $attemptid): bool { foreach ($this->rules as $rule) { if ($rule->is_preflight_check_required($attemptid)) { return true; @@ -369,8 +386,8 @@ class access_manager { * otherwise null. * @return mod_quiz_preflight_check_form the form. */ - public function get_preflight_check_form(moodle_url $url, $attemptid) { - // This form normally wants POST submissins. However, it also needs to + public function get_preflight_check_form(moodle_url $url, ?int $attemptid): mod_quiz_preflight_check_form { + // This form normally wants POST submissions. However, it also needs to // accept GET submissions. Since formslib is strict, we have to detect // which case we are in, and set the form property appropriately. $method = 'post'; @@ -378,27 +395,28 @@ class access_manager { $method = 'get'; } return new mod_quiz_preflight_check_form($url->out_omit_querystring(), - array('rules' => $this->rules, 'quizobj' => $this->quizobj, - 'attemptid' => $attemptid, 'hidden' => $url->params()), $method); + ['rules' => $this->rules, 'quizobj' => $this->quizobj, + 'attemptid' => $attemptid, 'hidden' => $url->params()], $method); } /** - * The pre-flight check has passed. This is a chance to record that fact in - * some way. + * The pre-flight check has passed. This is a chance to record that fact in some way. + * * @param int|null $attemptid the id of the current attempt, if there is one, * otherwise null. */ - public function notify_preflight_check_passed($attemptid) { + public function notify_preflight_check_passed(?int $attemptid): void { foreach ($this->rules as $rule) { $rule->notify_preflight_check_passed($attemptid); } } /** - * Inform the rules that the current attempt is finished. This is use, for example - * by the password rule, to clear the flag in the session. + * Inform the rules that the current attempt is finished. + * + * This is use, for example by the password rule, to clear the flag in the session. */ - public function current_attempt_finished() { + public function current_attempt_finished(): void { foreach ($this->rules as $rule) { $rule->current_attempt_finished(); } @@ -409,12 +427,12 @@ class access_manager { * quiz. Used, for example, to change the label by the grade displayed on the view page from * 'your current grade is' to 'your final grade is'. * - * @param int $numattempts the number of previous attempts this user has made. - * @param object $lastattempt information about the user's last completed attempt. + * @param int $numprevattempts the number of previous attempts this user has made. + * @param stdClass|false $lastattempt information about the user's last completed attempt. * @return bool true if there is no way the user will ever be allowed to attempt * this quiz again. */ - public function is_finished($numprevattempts, $lastattempt) { + public function is_finished(int $numprevattempts, $lastattempt): bool { foreach ($this->rules as $rule) { if ($rule->is_finished($numprevattempts, $lastattempt)) { return true; @@ -429,7 +447,7 @@ class access_manager { * * @param moodle_page $page the page object to initialise. */ - public function setup_attempt_page($page) { + public function setup_attempt_page(moodle_page $page): void { foreach ($this->rules as $rule) { $rule->setup_attempt_page($page); } @@ -438,11 +456,10 @@ class access_manager { /** * Compute when the attempt must be submitted. * - * @param object $attempt the data from the relevant quiz_attempts row. - * @return int|false the attempt close time. - * False if there is no limit. + * @param stdClass $attempt the data from the relevant quiz_attempts row. + * @return int|false the attempt close time. False if there is no limit. */ - public function get_end_time($attempt) { + public function get_end_time(stdClass $attempt) { $timeclose = false; foreach ($this->rules as $rule) { $ruletimeclose = $rule->end_time($attempt); @@ -456,12 +473,12 @@ class access_manager { /** * Compute what should be displayed to the user for time remaining in this attempt. * - * @param object $attempt the data from the relevant quiz_attempts row. + * @param stdClass $attempt the data from the relevant quiz_attempts row. * @param int $timenow the time to consider as 'now'. * @return int|false the number of seconds remaining for this attempt. * False if no limit should be displayed. */ - public function get_time_left_display($attempt, $timenow) { + public function get_time_left_display(stdClass $attempt, int $timenow) { $timeleft = false; foreach ($this->rules as $rule) { $ruletimeleft = $rule->time_left_display($attempt, $timenow); @@ -473,9 +490,11 @@ class access_manager { } /** - * @return bolean if this quiz should only be shown to students in a popup window. + * Is this quiz required to be shown in a popup window? + * + * @return bool true if a popup is required. */ - public function attempt_must_be_in_popup() { + public function attempt_must_be_in_popup(): bool { foreach ($this->rules as $rule) { if ($rule->attempt_must_be_in_popup()) { return true; @@ -485,11 +504,13 @@ class access_manager { } /** + * Get options required for opening the attempt in a popup window. + * * @return array any options that are required for showing the attempt page * in a popup window. */ - public function get_popup_options() { - $options = array(); + public function get_popup_options(): array { + $options = []; foreach ($this->rules as $rule) { $options += $rule->get_popup_options(); } @@ -505,9 +526,10 @@ class access_manager { * @param mod_quiz_renderer $output the quiz renderer. * @param string $message optional message to output while redirecting. */ - public function back_to_view_page($output, $message = '') { + public function back_to_view_page(mod_quiz_renderer $output, string $message = ''): void { + // Actually return type 'never' on the previous line, once 8.1 is our minimum PHP version. if ($this->attempt_must_be_in_popup()) { - echo $output->close_attempt_popup($this->quizobj->view_url(), $message); + echo $output->close_attempt_popup(new moodle_url($this->quizobj->view_url()), $message); die(); } else { redirect($this->quizobj->view_url(), $message); @@ -517,15 +539,16 @@ class access_manager { /** * Make some text into a link to review the quiz, if that is appropriate. * - * @param string $linktext some text. - * @param object $attempt the attempt object + * @param stdClass $attempt the attempt object + * @param mixed $nolongerused not used any more. + * @param mod_quiz_renderer $output quiz renderer instance. * @return string some HTML, the $linktext either unmodified or wrapped in a * link to the review page. */ - public function make_review_link($attempt, $reviewoptions, $output) { + public function make_review_link(stdClass $attempt, $nolongerused, mod_quiz_renderer $output): string { // If the attempt is still open, don't link. - if (in_array($attempt->state, array(quiz_attempt::IN_PROGRESS, quiz_attempt::OVERDUE))) { + if (in_array($attempt->state, [quiz_attempt::IN_PROGRESS, quiz_attempt::OVERDUE])) { return $output->no_review_message(''); } @@ -549,11 +572,11 @@ class access_manager { * @param array $files un-used, Moodle seems to not support it anymore * @param int|null $attemptid the id of the current attempt, if there is one, * otherwise null. - * @return array of errors, empty array means no erros + * @return array of errors, empty array means no errors * @since Moodle 3.1 */ - public function validate_preflight_check($data, $files, $attemptid) { - $errors = array(); + public function validate_preflight_check(array $data, array $files, ?int $attemptid): array { + $errors = []; foreach ($this->rules as $rule) { if ($rule->is_preflight_check_required($attemptid)) { $errors = $rule->validate_preflight_check($data, $files, $errors, $attemptid); diff --git a/mod/quiz/classes/external.php b/mod/quiz/classes/external.php index 024a3dfb1b4..1531260f2da 100644 --- a/mod/quiz/classes/external.php +++ b/mod/quiz/classes/external.php @@ -1884,7 +1884,7 @@ class mod_quiz_external extends external_api { list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']); - $attempttocheck = 0; + $attempttocheck = null; if (!empty($params['attemptid'])) { $attemptobj = quiz_attempt::create($params['attemptid']); if ($attemptobj->get_userid() != $USER->id) { @@ -1914,7 +1914,7 @@ class mod_quiz_external extends external_api { $numattempts = count($attempts); if (!$attempttocheck) { - $attempttocheck = $unfinishedattempt ? $unfinishedattempt : $lastfinishedattempt; + $attempttocheck = $unfinishedattempt ?: $lastfinishedattempt; } $result = array(); From 91c913debcc028830bf3549431fbc65409a8527c Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Wed, 14 Dec 2022 22:57:15 +0000 Subject: [PATCH 03/30] MDL-76614 quiz: move mod_quiz_preflight_check_form to classes & clean up --- mod/quiz/accessmanager_form.php | 55 ++-------------- mod/quiz/accessrule/accessrulebase.php | 5 +- mod/quiz/accessrule/offlineattempts/rule.php | 3 +- mod/quiz/accessrule/password/rule.php | 3 +- mod/quiz/accessrule/timelimit/rule.php | 3 +- mod/quiz/accessrule/upgrade.txt | 6 ++ mod/quiz/classes/access_manager.php | 10 +-- .../classes/form/preflight_check_form.php | 64 +++++++++++++++++++ mod/quiz/db/renamedclasses.php | 1 + mod/quiz/locallib.php | 1 - mod/quiz/renderer.php | 26 ++++---- mod/quiz/upgrade.txt | 2 + 12 files changed, 106 insertions(+), 73 deletions(-) create mode 100644 mod/quiz/classes/form/preflight_check_form.php diff --git a/mod/quiz/accessmanager_form.php b/mod/quiz/accessmanager_form.php index e2e6fc5e241..df1c42fe9f1 100644 --- a/mod/quiz/accessmanager_form.php +++ b/mod/quiz/accessmanager_form.php @@ -15,58 +15,11 @@ // along with Moodle. If not, see . /** - * Defines the form that limits student's access to attempt a quiz. + * File only retained to prevent fatal errors in code that tries to require/include this. * - * @package mod_quiz - * @copyright 2011 The Open University - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @todo MDL-76612 delete this file as part of Moodle 4.6 development. + * @deprecated This file is no longer required in Moodle 4.2+. */ - - defined('MOODLE_INTERNAL') || die(); -require_once($CFG->libdir.'/formslib.php'); - - -/** - * A form that limits student's access to attempt a quiz. - * - * @copyright 2011 The Open University - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class mod_quiz_preflight_check_form extends moodleform { - - protected function definition() { - $mform = $this->_form; - $this->_form->updateAttributes(array('id' => 'mod_quiz_preflight_form')); - - foreach ($this->_customdata['hidden'] as $name => $value) { - if ($name === 'sesskey') { - continue; - } - $mform->addElement('hidden', $name, $value); - $mform->setType($name, PARAM_INT); - } - - foreach ($this->_customdata['rules'] as $rule) { - if ($rule->is_preflight_check_required($this->_customdata['attemptid'])) { - $rule->add_preflight_check_form_fields($this, $mform, - $this->_customdata['attemptid']); - } - } - - $this->add_action_buttons(true, get_string('startattempt', 'quiz')); - $this->set_display_vertical(); - $mform->setDisableShortforms(); - } - - public function validation($data, $files) { - $errors = parent::validation($data, $files); - - $timenow = time(); - $accessmanager = $this->_customdata['quizobj']->get_access_manager($timenow); - $errors = array_merge($errors, $accessmanager->validate_preflight_check($data, $files, $this->_customdata['attemptid'])); - - return $errors; - } -} +debugging('This file is no longer required in Moodle 4.2+. Please do not include/require it.', DEBUG_DEVELOPER); diff --git a/mod/quiz/accessrule/accessrulebase.php b/mod/quiz/accessrule/accessrulebase.php index 3bccd0181f8..c433c9f8092 100644 --- a/mod/quiz/accessrule/accessrulebase.php +++ b/mod/quiz/accessrule/accessrulebase.php @@ -22,6 +22,7 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +use mod_quiz\form\preflight_check_form; defined('MOODLE_INTERNAL') || die(); @@ -108,12 +109,12 @@ abstract class quiz_access_rule_base { * Add any field you want to pre-flight check form. You should only do * something here if {@link is_preflight_check_required()} returned true. * - * @param mod_quiz_preflight_check_form $quizform the form being built. + * @param preflight_check_form $quizform the form being built. * @param MoodleQuickForm $mform The wrapped MoodleQuickForm. * @param int|null $attemptid the id of the current attempt, if there is one, * otherwise null. */ - public function add_preflight_check_form_fields(mod_quiz_preflight_check_form $quizform, + public function add_preflight_check_form_fields(preflight_check_form $quizform, MoodleQuickForm $mform, $attemptid) { // Do nothing by default. } diff --git a/mod/quiz/accessrule/offlineattempts/rule.php b/mod/quiz/accessrule/offlineattempts/rule.php index 511be30f145..f88f3832854 100644 --- a/mod/quiz/accessrule/offlineattempts/rule.php +++ b/mod/quiz/accessrule/offlineattempts/rule.php @@ -22,6 +22,7 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +use mod_quiz\form\preflight_check_form; defined('MOODLE_INTERNAL') || die(); @@ -63,7 +64,7 @@ class quizaccess_offlineattempts extends quiz_access_rule_base { } } - public function add_preflight_check_form_fields(mod_quiz_preflight_check_form $quizform, + public function add_preflight_check_form_fields(preflight_check_form $quizform, MoodleQuickForm $mform, $attemptid) { global $DB; diff --git a/mod/quiz/accessrule/password/rule.php b/mod/quiz/accessrule/password/rule.php index 778e3902891..6e48b904178 100644 --- a/mod/quiz/accessrule/password/rule.php +++ b/mod/quiz/accessrule/password/rule.php @@ -23,6 +23,7 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +use mod_quiz\form\preflight_check_form; defined('MOODLE_INTERNAL') || die(); @@ -54,7 +55,7 @@ class quizaccess_password extends quiz_access_rule_base { return empty($SESSION->passwordcheckedquizzes[$this->quiz->id]); } - public function add_preflight_check_form_fields(mod_quiz_preflight_check_form $quizform, + public function add_preflight_check_form_fields(preflight_check_form $quizform, MoodleQuickForm $mform, $attemptid) { $mform->addElement('header', 'passwordheader', get_string('password')); diff --git a/mod/quiz/accessrule/timelimit/rule.php b/mod/quiz/accessrule/timelimit/rule.php index ea061398801..08dd92d4a6b 100644 --- a/mod/quiz/accessrule/timelimit/rule.php +++ b/mod/quiz/accessrule/timelimit/rule.php @@ -23,6 +23,7 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +use mod_quiz\form\preflight_check_form; defined('MOODLE_INTERNAL') || die(); @@ -74,7 +75,7 @@ class quizaccess_timelimit extends quiz_access_rule_base { return $attemptid === null; } - public function add_preflight_check_form_fields(mod_quiz_preflight_check_form $quizform, + public function add_preflight_check_form_fields(preflight_check_form $quizform, MoodleQuickForm $mform, $attemptid) { $mform->addElement('header', 'honestycheckheader', get_string('confirmstartheader', 'quizaccess_timelimit')); diff --git a/mod/quiz/accessrule/upgrade.txt b/mod/quiz/accessrule/upgrade.txt index f883b6e1143..41e505834bd 100644 --- a/mod/quiz/accessrule/upgrade.txt +++ b/mod/quiz/accessrule/upgrade.txt @@ -2,6 +2,12 @@ This files describes API changes for quiz access rule plugins. Overview of this plugin type at http://docs.moodle.org/dev/Quiz_access_rules +=== 4.2 === + +* Note that class mod_quiz_preflight_check_form has been renamed to + mod_quiz\form\preflight_check_form. + + === 2.8, 2.7.1, 2.6.4 and 2.5.7 === * New static method delete_settings for access rules, which is called when a diff --git a/mod/quiz/classes/access_manager.php b/mod/quiz/classes/access_manager.php index 2f40e81ed70..ea1c6e7fadc 100644 --- a/mod/quiz/classes/access_manager.php +++ b/mod/quiz/classes/access_manager.php @@ -17,9 +17,9 @@ namespace mod_quiz; use core_component; +use mod_quiz\form\preflight_check_form; use mod_quiz\question\display_options; use mod_quiz_mod_form; -use mod_quiz_preflight_check_form; use mod_quiz_renderer; use moodle_page; use moodle_url; @@ -384,17 +384,17 @@ class access_manager { * @param moodle_url $url the form action URL. * @param int|null $attemptid the id of the current attempt, if there is one, * otherwise null. - * @return mod_quiz_preflight_check_form the form. + * @return preflight_check_form the form. */ - public function get_preflight_check_form(moodle_url $url, ?int $attemptid): mod_quiz_preflight_check_form { + public function get_preflight_check_form(moodle_url $url, ?int $attemptid): preflight_check_form { // This form normally wants POST submissions. However, it also needs to // accept GET submissions. Since formslib is strict, we have to detect // which case we are in, and set the form property appropriately. $method = 'post'; - if (!empty($_GET['_qf__mod_quiz_preflight_check_form'])) { + if (!empty($_GET['_qf__preflight_check_form'])) { $method = 'get'; } - return new mod_quiz_preflight_check_form($url->out_omit_querystring(), + return new preflight_check_form($url->out_omit_querystring(), ['rules' => $this->rules, 'quizobj' => $this->quizobj, 'attemptid' => $attemptid, 'hidden' => $url->params()], $method); } diff --git a/mod/quiz/classes/form/preflight_check_form.php b/mod/quiz/classes/form/preflight_check_form.php new file mode 100644 index 00000000000..a734d137fec --- /dev/null +++ b/mod/quiz/classes/form/preflight_check_form.php @@ -0,0 +1,64 @@ +. + +namespace mod_quiz\form; + +use moodleform; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir.'/formslib.php'); + +/** + * A form that limits student's access to attempt a quiz. + * + * @package mod_quiz + * @copyright 2011 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class preflight_check_form extends moodleform { + + protected function definition() { + $mform = $this->_form; + $this->_form->updateAttributes(array('id' => 'mod_quiz_preflight_form')); + + foreach ($this->_customdata['hidden'] as $name => $value) { + if ($name === 'sesskey') { + continue; + } + $mform->addElement('hidden', $name, $value); + $mform->setType($name, PARAM_INT); + } + + foreach ($this->_customdata['rules'] as $rule) { + if ($rule->is_preflight_check_required($this->_customdata['attemptid'])) { + $rule->add_preflight_check_form_fields($this, $mform, + $this->_customdata['attemptid']); + } + } + + $this->add_action_buttons(true, get_string('startattempt', 'quiz')); + $this->set_display_vertical(); + $mform->setDisableShortforms(); + } + + public function validation($data, $files): array { + $errors = parent::validation($data, $files); + $accessmanager = $this->_customdata['quizobj']->get_access_manager(time()); + return array_merge($errors, $accessmanager->validate_preflight_check( + $data, $files, $this->_customdata['attemptid'])); + } +} diff --git a/mod/quiz/db/renamedclasses.php b/mod/quiz/db/renamedclasses.php index 9d953c12f9b..7256a8b8890 100644 --- a/mod/quiz/db/renamedclasses.php +++ b/mod/quiz/db/renamedclasses.php @@ -52,4 +52,5 @@ $renamedclasses = [ 'mod_quiz_attempts_report_options' => 'mod_quiz\local\reports\attempts_report_options', 'quiz_attempts_report_table' => 'mod_quiz\local\reports\attempts_report_table', 'quiz_access_manager' => 'mod_quiz\access_manager', + 'mod_quiz_preflight_check_form' => 'mod_quiz\form\preflight_check_form', ]; diff --git a/mod/quiz/locallib.php b/mod/quiz/locallib.php index f7abd0a28e7..db534cc1a68 100644 --- a/mod/quiz/locallib.php +++ b/mod/quiz/locallib.php @@ -31,7 +31,6 @@ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/mod/quiz/lib.php'); -require_once($CFG->dirroot . '/mod/quiz/accessmanager_form.php'); require_once($CFG->dirroot . '/mod/quiz/renderer.php'); require_once($CFG->dirroot . '/mod/quiz/attemptlib.php'); require_once($CFG->libdir . '/completionlib.php'); diff --git a/mod/quiz/renderer.php b/mod/quiz/renderer.php index 3c33af87a05..be7f147b816 100644 --- a/mod/quiz/renderer.php +++ b/mod/quiz/renderer.php @@ -26,6 +26,7 @@ defined('MOODLE_INTERNAL') || die(); use mod_quiz\access_manager; +use mod_quiz\form\preflight_check_form; use mod_quiz\question\display_options; @@ -428,7 +429,17 @@ class mod_quiz_renderer extends plugin_renderer_base { return implode(', ', $attemptlinks); } - public function start_attempt_page(quiz $quizobj, mod_quiz_preflight_check_form $mform) { + /** + * Render the 'start attempt' page. + * + * The student gets here if their interaction with the preflight check + * from fails in some way (e.g. they typed the wrong password). + * + * @param quiz $quizobj + * @param preflight_check_form $mform + * @return string + */ + public function start_attempt_page(quiz $quizobj, preflight_check_form $mform) { $output = ''; $output .= $this->header(); $output .= $this->during_attempt_tertiary_nav($quizobj->view_url()); @@ -900,22 +911,15 @@ class mod_quiz_renderer extends plugin_renderer_base { * * @param string $buttontext the label to display on the button. * @param moodle_url $url The URL to POST to in order to start the attempt. - * @param mod_quiz_preflight_check_form $preflightcheckform deprecated. + * @param preflight_check_form $preflightcheckform deprecated. * @param bool $popuprequired whether the attempt needs to be opened in a pop-up. * @param array $popupoptions the options to use if we are opening a popup. * @return string HTML fragment. */ public function start_attempt_button($buttontext, moodle_url $url, - mod_quiz_preflight_check_form $preflightcheckform = null, + preflight_check_form $preflightcheckform = null, $popuprequired = false, $popupoptions = null) { - if (is_string($preflightcheckform)) { - // Calling code was not updated since the API change. - debugging('The third argument to start_attempt_button should now be the ' . - 'mod_quiz_preflight_check_form from ' . - 'access_manager::get_preflight_check_form, not a warning message string.'); - } - $button = new single_button($url, $buttontext, 'post', true); $button->class .= ' quizstartbuttondiv'; if ($popuprequired) { @@ -1474,7 +1478,7 @@ class mod_quiz_view_object { public $buttontext; /** @var moodle_url $startattempturl URL to start an attempt. */ public $startattempturl; - /** @var mod_quiz_preflight_check_form|null $preflightcheckform confirmation form that must be + /** @var preflight_check_form|null $preflightcheckform confirmation form that must be * submitted before an attempt is started, if required. */ public $preflightcheckform; /** @var moodle_url $startattempturl URL for any Back to the course button. */ diff --git a/mod/quiz/upgrade.txt b/mod/quiz/upgrade.txt index f9b020dabf0..2eaa0eebfc6 100644 --- a/mod/quiz/upgrade.txt +++ b/mod/quiz/upgrade.txt @@ -36,6 +36,7 @@ This files describes API changes in the quiz code. - mod_quiz_attempts_report_options => mod_quiz\local\reports\attempts_report_options - quiz_attempts_report_table => mod_quiz\local\reports\attempts_report_table - quiz_access_manager => mod_quiz\access_manager + - mod_quiz_preflight_check_form => mod_quiz\form\preflight_check_form * As part of the clean-up, the following files are no longer required, and if you try to include them, you will get a debugging notices telling you not to: @@ -45,6 +46,7 @@ This files describes API changes in the quiz code. - mod/quiz/report/attemptsreport_table.php - mod/quiz/report/default.php - mod/quiz/accessmanager.php + - mod/quiz/accessmanager_form.php === 4.1 === From 4ae4d8d1b6bf43975d8cd150876f4d444663f203 Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Fri, 16 Dec 2022 16:41:43 +0000 Subject: [PATCH 04/30] MDL-76614 quiz: merge mod_quiz_overdue_attempt_updater into task The only place this code was used was in mod_quiz\task\update_overdue_attempts so neater to combine them into one class. --- .../classes/task/update_overdue_attempts.php | 95 +++++++++++++++- mod/quiz/cronlib.php | 103 +----------------- mod/quiz/deprecatedlib.php | 28 +++++ mod/quiz/tests/attempts_test.php | 14 +-- mod/quiz/upgrade.txt | 4 + 5 files changed, 133 insertions(+), 111 deletions(-) diff --git a/mod/quiz/classes/task/update_overdue_attempts.php b/mod/quiz/classes/task/update_overdue_attempts.php index 54fa14e96bb..98b389f0757 100644 --- a/mod/quiz/classes/task/update_overdue_attempts.php +++ b/mod/quiz/classes/task/update_overdue_attempts.php @@ -24,6 +24,10 @@ */ namespace mod_quiz\task; +use moodle_exception; +use moodle_recordset; +use quiz_attempt; + defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/mod/quiz/locallib.php'); @@ -44,22 +48,103 @@ class update_overdue_attempts extends \core\task\scheduled_task { } /** - * * Close off any overdue attempts. */ public function execute() { global $CFG; - require_once($CFG->dirroot . '/mod/quiz/cronlib.php'); $timenow = time(); - $overduehander = new \mod_quiz_overdue_attempt_updater(); - $processto = $timenow - get_config('quiz', 'graceperiodmin'); mtrace(' Looking for quiz overdue quiz attempts...'); - list($count, $quizcount) = $overduehander->update_overdue_attempts($timenow, $processto); + list($count, $quizcount) = $this->update_all_overdue_attempts($timenow, $processto); mtrace(' Considered ' . $count . ' attempts in ' . $quizcount . ' quizzes.'); } + + /** + * Do the processing required. + * @param int $timenow the time to consider as 'now' during the processing. + * @param int $processto only process attempt with timecheckstate longer ago than this. + * @return array with two elements, the number of attempt considered, and how many different quizzes that was. + */ + public function update_all_overdue_attempts($timenow, $processto) { + global $DB; + + $attemptstoprocess = $this->get_list_of_overdue_attempts($processto); + + $course = null; + $quiz = null; + $cm = null; + + $count = 0; + $quizcount = 0; + foreach ($attemptstoprocess as $attempt) { + try { + + // If we have moved on to a different quiz, fetch the new data. + if (!$quiz || $attempt->quiz != $quiz->id) { + $quiz = $DB->get_record('quiz', array('id' => $attempt->quiz), '*', MUST_EXIST); + $cm = get_coursemodule_from_instance('quiz', $attempt->quiz); + $quizcount += 1; + } + + // If we have moved on to a different course, fetch the new data. + if (!$course || $course->id != $quiz->course) { + $course = $DB->get_record('course', array('id' => $quiz->course), '*', MUST_EXIST); + } + + // Make a specialised version of the quiz settings, with the relevant overrides. + $quizforuser = clone($quiz); + $quizforuser->timeclose = $attempt->usertimeclose; + $quizforuser->timelimit = $attempt->usertimelimit; + + // Trigger any transitions that are required. + $attemptobj = new quiz_attempt($attempt, $quizforuser, $cm, $course); + $attemptobj->handle_if_time_expired($timenow, false); + $count += 1; + + } catch (moodle_exception $e) { + // If an error occurs while processing one attempt, don't let that kill cron. + mtrace("Error while processing attempt {$attempt->id} at {$attempt->quiz} quiz:"); + mtrace($e->getMessage()); + mtrace($e->getTraceAsString()); + // Close down any currently open transactions, otherwise one error + // will stop following DB changes from being committed. + $DB->force_transaction_rollback(); + } + } + + $attemptstoprocess->close(); + return array($count, $quizcount); + } + + /** + * @return moodle_recordset of quiz_attempts that need to be processed because time has + * passed. The array is sorted by courseid then quizid. + */ + public function get_list_of_overdue_attempts($processto) { + global $DB; + + // SQL to compute timeclose and timelimit for each attempt. + $quizausersql = quiz_get_attempt_usertime_sql( + "iquiza.state IN ('inprogress', 'overdue') AND iquiza.timecheckstate <= :iprocessto"); + + // This query should have all the quiz_attempts columns. + return $DB->get_recordset_sql(" + SELECT quiza.*, + quizauser.usertimeclose, + quizauser.usertimelimit + + FROM {quiz_attempts} quiza + JOIN {quiz} quiz ON quiz.id = quiza.quiz + JOIN ( $quizausersql ) quizauser ON quizauser.id = quiza.id + + WHERE quiza.state IN ('inprogress', 'overdue') + AND quiza.timecheckstate <= :processto + ORDER BY quiz.course, quiza.quiz", + + array('processto' => $processto, 'iprocessto' => $processto)); + } } diff --git a/mod/quiz/cronlib.php b/mod/quiz/cronlib.php index 51509e7a95c..bf6091bab87 100644 --- a/mod/quiz/cronlib.php +++ b/mod/quiz/cronlib.php @@ -20,106 +20,13 @@ * @package mod_quiz * @copyright 2012 the Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @todo MDL-76612 delete this file as part of Moodle 4.6 development. + * @deprecated This file is no longer required in Moodle 4.2+. */ - defined('MOODLE_INTERNAL') || die(); +debugging('This file is no longer required in Moodle 4.2+. Please do not include/require it.', DEBUG_DEVELOPER); + require_once($CFG->dirroot . '/mod/quiz/locallib.php'); - - -/** - * This class holds all the code for automatically updating all attempts that have - * gone over their time limit. - * - * @copyright 2012 the Open University - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class mod_quiz_overdue_attempt_updater { - - /** - * Do the processing required. - * @param int $timenow the time to consider as 'now' during the processing. - * @param int $processto only process attempt with timecheckstate longer ago than this. - * @return array with two elements, the number of attempt considered, and how many different quizzes that was. - */ - public function update_overdue_attempts($timenow, $processto) { - global $DB; - - $attemptstoprocess = $this->get_list_of_overdue_attempts($processto); - - $course = null; - $quiz = null; - $cm = null; - - $count = 0; - $quizcount = 0; - foreach ($attemptstoprocess as $attempt) { - try { - - // If we have moved on to a different quiz, fetch the new data. - if (!$quiz || $attempt->quiz != $quiz->id) { - $quiz = $DB->get_record('quiz', array('id' => $attempt->quiz), '*', MUST_EXIST); - $cm = get_coursemodule_from_instance('quiz', $attempt->quiz); - $quizcount += 1; - } - - // If we have moved on to a different course, fetch the new data. - if (!$course || $course->id != $quiz->course) { - $course = $DB->get_record('course', array('id' => $quiz->course), '*', MUST_EXIST); - } - - // Make a specialised version of the quiz settings, with the relevant overrides. - $quizforuser = clone($quiz); - $quizforuser->timeclose = $attempt->usertimeclose; - $quizforuser->timelimit = $attempt->usertimelimit; - - // Trigger any transitions that are required. - $attemptobj = new quiz_attempt($attempt, $quizforuser, $cm, $course); - $attemptobj->handle_if_time_expired($timenow, false); - $count += 1; - - } catch (moodle_exception $e) { - // If an error occurs while processing one attempt, don't let that kill cron. - mtrace("Error while processing attempt {$attempt->id} at {$attempt->quiz} quiz:"); - mtrace($e->getMessage()); - mtrace($e->getTraceAsString()); - // Close down any currently open transactions, otherwise one error - // will stop following DB changes from being committed. - $DB->force_transaction_rollback(); - } - } - - $attemptstoprocess->close(); - return array($count, $quizcount); - } - - /** - * @return moodle_recordset of quiz_attempts that need to be processed because time has - * passed. The array is sorted by courseid then quizid. - */ - public function get_list_of_overdue_attempts($processto) { - global $DB; - - - // SQL to compute timeclose and timelimit for each attempt: - $quizausersql = quiz_get_attempt_usertime_sql( - "iquiza.state IN ('inprogress', 'overdue') AND iquiza.timecheckstate <= :iprocessto"); - - // This query should have all the quiz_attempts columns. - return $DB->get_recordset_sql(" - SELECT quiza.*, - quizauser.usertimeclose, - quizauser.usertimelimit - - FROM {quiz_attempts} quiza - JOIN {quiz} quiz ON quiz.id = quiza.quiz - JOIN ( $quizausersql ) quizauser ON quizauser.id = quiza.id - - WHERE quiza.state IN ('inprogress', 'overdue') - AND quiza.timecheckstate <= :processto - ORDER BY quiz.course, quiza.quiz", - - array('processto' => $processto, 'iprocessto' => $processto)); - } -} +require_once($CFG->dirroot . '/mod/quiz/deprecatedlib.php'); diff --git a/mod/quiz/deprecatedlib.php b/mod/quiz/deprecatedlib.php index f430d76d502..42a87b04056 100644 --- a/mod/quiz/deprecatedlib.php +++ b/mod/quiz/deprecatedlib.php @@ -23,6 +23,7 @@ */ use mod_quiz\access_manager; +use mod_quiz\task\update_overdue_attempts; /** * Internal function used in quiz_get_completion_state. Check passing grade (or no attempts left) requirement for completion. @@ -134,3 +135,30 @@ function quiz_get_completion_state($course, $cm, $userid, $type) { return true; } + +/** + * @copyright 2012 the Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @deprecated since Moodle 4.2. Code moved to mod_quiz\task\update_overdue_attempts. + * @todo MDL-71196 Final deprecation in Moodle 4.3 + */ +class mod_quiz_overdue_attempt_updater { + + /** + * @deprecated since Moodle 4.2. Code moved to mod_quiz\task\update_overdue_attempts. that was. + */ + public function update_overdue_attempts($timenow, $processto) { + debugging('mod_quiz_overdue_attempt_updater has been deprecated. The code wsa moved to ' . + 'mod_quiz\task\update_overdue_attempts.'); + return (new update_overdue_attempts())->update_all_overdue_attempts((int) $timenow, (int) $processto); + } + + /** + * @deprecated since Moodle 4.2. Code moved to mod_quiz\task\update_overdue_attempts. + */ + public function get_list_of_overdue_attempts($processto) { + debugging('mod_quiz_overdue_attempt_updater has been deprecated. The code wsa moved to ' . + 'mod_quiz\task\update_overdue_attempts.'); + return (new update_overdue_attempts())->get_list_of_overdue_attempts((int) $processto); + } +} diff --git a/mod/quiz/tests/attempts_test.php b/mod/quiz/tests/attempts_test.php index 52abb662895..da714842221 100644 --- a/mod/quiz/tests/attempts_test.php +++ b/mod/quiz/tests/attempts_test.php @@ -16,7 +16,9 @@ namespace mod_quiz; -use mod_quiz_overdue_attempt_updater; +use core_question_generator; +use mod_quiz\task\update_overdue_attempts; +use mod_quiz_generator; use question_engine; use quiz; @@ -40,12 +42,8 @@ class attempts_test extends \advanced_testcase { * update_overdue_attempts(). */ public function test_bulk_update_functions() { - global $DB,$CFG; - - require_once($CFG->dirroot.'/mod/quiz/cronlib.php'); - + global $DB; $this->resetAfterTest(); - $this->setAdminUser(); // Setup course, user and groups @@ -390,7 +388,7 @@ class attempts_test extends \advanced_testcase { // Test get_list_of_overdue_attempts(). // - $overduehander = new mod_quiz_overdue_attempt_updater(); + $overduehander = new update_overdue_attempts(); $attempts = $overduehander->get_list_of_overdue_attempts(100000); // way in the future $count = 0; @@ -417,7 +415,7 @@ class attempts_test extends \advanced_testcase { // Test update_overdue_attempts(). // - [$count, $quizcount] = $overduehander->update_overdue_attempts(1000, 940); + [$count, $quizcount] = $overduehander->update_all_overdue_attempts(1000, 940); $attempts = $DB->get_records('quiz_attempts', null, 'quiz, userid, attempt', 'id, quiz, userid, attempt, state, timestart, timefinish, timecheckstate'); diff --git a/mod/quiz/upgrade.txt b/mod/quiz/upgrade.txt index 2eaa0eebfc6..0973fdf4b4d 100644 --- a/mod/quiz/upgrade.txt +++ b/mod/quiz/upgrade.txt @@ -38,6 +38,9 @@ This files describes API changes in the quiz code. - quiz_access_manager => mod_quiz\access_manager - mod_quiz_preflight_check_form => mod_quiz\form\preflight_check_form +* The following classes have been deprecated: + - mod_quiz_overdue_attempt_updater - merged into mod_quiz\task\update_overdue_attempts + * As part of the clean-up, the following files are no longer required, and if you try to include them, you will get a debugging notices telling you not to: - mod/quiz/report/attemptsreport.php @@ -47,6 +50,7 @@ This files describes API changes in the quiz code. - mod/quiz/report/default.php - mod/quiz/accessmanager.php - mod/quiz/accessmanager_form.php + - mod/quiz/cronlib.php === 4.1 === From f6ae83f40e7dba572f5c3be145aaac1a32f53c67 Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Mon, 19 Dec 2022 13:25:17 +0000 Subject: [PATCH 05/30] MDL-76614 quiz: clean up update_overdue_attempts class --- .../classes/task/update_overdue_attempts.php | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/mod/quiz/classes/task/update_overdue_attempts.php b/mod/quiz/classes/task/update_overdue_attempts.php index 98b389f0757..8241243466d 100644 --- a/mod/quiz/classes/task/update_overdue_attempts.php +++ b/mod/quiz/classes/task/update_overdue_attempts.php @@ -30,6 +30,7 @@ use quiz_attempt; defined('MOODLE_INTERNAL') || die(); +global $CFG; require_once($CFG->dirroot . '/mod/quiz/locallib.php'); /** @@ -43,7 +44,7 @@ require_once($CFG->dirroot . '/mod/quiz/locallib.php'); */ class update_overdue_attempts extends \core\task\scheduled_task { - public function get_name() { + public function get_name(): string { return get_string('updateoverdueattemptstask', 'mod_quiz'); } @@ -51,8 +52,6 @@ class update_overdue_attempts extends \core\task\scheduled_task { * Close off any overdue attempts. */ public function execute() { - global $CFG; - $timenow = time(); $processto = $timenow - get_config('quiz', 'graceperiodmin'); @@ -65,11 +64,12 @@ class update_overdue_attempts extends \core\task\scheduled_task { /** * Do the processing required. + * * @param int $timenow the time to consider as 'now' during the processing. * @param int $processto only process attempt with timecheckstate longer ago than this. * @return array with two elements, the number of attempt considered, and how many different quizzes that was. */ - public function update_all_overdue_attempts($timenow, $processto) { + public function update_all_overdue_attempts(int $timenow, int $processto): array { global $DB; $attemptstoprocess = $this->get_list_of_overdue_attempts($processto); @@ -107,7 +107,7 @@ class update_overdue_attempts extends \core\task\scheduled_task { } catch (moodle_exception $e) { // If an error occurs while processing one attempt, don't let that kill cron. - mtrace("Error while processing attempt {$attempt->id} at {$attempt->quiz} quiz:"); + mtrace("Error while processing attempt $attempt->id at $attempt->quiz quiz:"); mtrace($e->getMessage()); mtrace($e->getTraceAsString()); // Close down any currently open transactions, otherwise one error @@ -121,10 +121,15 @@ class update_overdue_attempts extends \core\task\scheduled_task { } /** + * Get a recordset of all the attempts that need to be processed now. + * + * (Only public to allow unit testing. Do not use!) + * + * @param int $processto timestamp to process up to. * @return moodle_recordset of quiz_attempts that need to be processed because time has - * passed. The array is sorted by courseid then quizid. + * passed, sorted by courseid then quizid. */ - public function get_list_of_overdue_attempts($processto) { + public function get_list_of_overdue_attempts(int $processto): moodle_recordset { global $DB; // SQL to compute timeclose and timelimit for each attempt. From f83181d397b9f864787272f8e98c3d760c621c2b Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Fri, 16 Dec 2022 17:09:36 +0000 Subject: [PATCH 06/30] MDL-76614 quiz: deprecate class moodle_quiz_exception Code should just directly use moodle_execption. Note, some of the existing uses (in web services) were passing a literal message, not a language string identifier, but I decided now was not the time to try to fix that. --- mod/quiz/attempt.php | 4 +- mod/quiz/attemptlib.php | 31 +------------- mod/quiz/autosave.ajax.php | 5 +-- mod/quiz/classes/external.php | 49 +++++++++-------------- mod/quiz/deprecatedlib.php | 32 ++++++++++++++- mod/quiz/locallib.php | 3 +- mod/quiz/processattempt.php | 5 +-- mod/quiz/review.php | 2 +- mod/quiz/reviewquestion.php | 2 +- mod/quiz/summary.php | 2 +- mod/quiz/tests/external/external_test.php | 37 ++++++++--------- mod/quiz/upgrade.txt | 1 + 12 files changed, 80 insertions(+), 93 deletions(-) diff --git a/mod/quiz/attempt.php b/mod/quiz/attempt.php index ecd85256505..4375498aab3 100644 --- a/mod/quiz/attempt.php +++ b/mod/quiz/attempt.php @@ -57,7 +57,7 @@ if ($attemptobj->get_userid() != $USER->id) { if ($attemptobj->has_capability('mod/quiz:viewreports')) { redirect($attemptobj->review_url(null, $page)); } else { - throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'notyourattempt'); + throw new moodle_exception('notyourattempt', 'quiz', $quizobj->view_url()); } } @@ -107,7 +107,7 @@ $slots = $attemptobj->get_slots($page); // Check. if (empty($slots)) { - throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'noquestionsfound'); + throw new moodle_exception('noquestionsfound', 'quiz', $quizobj->view_url()); } // Update attempt page, redirecting the user if $page is not valid. diff --git a/mod/quiz/attemptlib.php b/mod/quiz/attemptlib.php index 999bb9f6d81..fd55f4c7cea 100644 --- a/mod/quiz/attemptlib.php +++ b/mod/quiz/attemptlib.php @@ -31,35 +31,6 @@ use mod_quiz\access_manager; use mod_quiz\question\bank\qbank_helper; use mod_quiz\question\display_options; - -/** - * Class for quiz exceptions. Just saves a couple of arguments on the - * constructor for a moodle_exception. - * - * @copyright 2008 Tim Hunt - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @since Moodle 2.0 - */ -class moodle_quiz_exception extends moodle_exception { - /** - * Constructor. - * - * @param quiz $quizobj the quiz the error relates to. - * @param string $errorcode The name of the string from error.php to print. - * @param mixed $a Extra words and phrases that might be required in the error string. - * @param string $link The url where the user will be prompted to continue. - * If no url is provided the user will be directed to the site index page. - * @param string|null $debuginfo optional debugging information. - */ - public function __construct($quizobj, $errorcode, $a = null, $link = '', $debuginfo = null) { - if (!$link) { - $link = $quizobj->view_url(); - } - parent::__construct($errorcode, 'quiz', $link, $a, $debuginfo); - } -} - - /** * A class encapsulating a quiz and the questions it contains, and making the * information available to scripts like view.php. @@ -525,7 +496,7 @@ class quiz { */ protected function ensure_question_loaded($id) { if (isset($this->questions[$id]->_partiallyloaded)) { - throw new moodle_quiz_exception($this, 'questionnotloaded', $id); + throw new moodle_exception('questionnotloaded', 'quiz', $this->view_url(), $id); } } diff --git a/mod/quiz/autosave.ajax.php b/mod/quiz/autosave.ajax.php index c2f38179f48..6600650f6b8 100644 --- a/mod/quiz/autosave.ajax.php +++ b/mod/quiz/autosave.ajax.php @@ -45,7 +45,7 @@ require_login($attemptobj->get_course(), false, $attemptobj->get_cm()); // Check that this attempt belongs to this user. if ($attemptobj->get_userid() != $USER->id) { - throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'notyourattempt'); + throw new moodle_exception('notyourattempt', 'quiz', $attemptobj->view_url()); } // Check capabilities. @@ -55,8 +55,7 @@ if (!$attemptobj->is_preview_user()) { // If the attempt is already closed, send them to the review page. if ($attemptobj->is_finished()) { - throw new moodle_quiz_exception($attemptobj->get_quizobj(), - 'attemptalreadyclosed', null, $attemptobj->review_url()); + throw new moodle_exception('attemptalreadyclosed', 'quiz', $attemptobj->review_url()); } $attemptobj->process_auto_save($timenow); diff --git a/mod/quiz/classes/external.php b/mod/quiz/classes/external.php index 1531260f2da..289d88b279e 100644 --- a/mod/quiz/classes/external.php +++ b/mod/quiz/classes/external.php @@ -305,7 +305,6 @@ class mod_quiz_external extends external_api { * @param int $quizid quiz instance id * @return array of warnings and status result * @since Moodle 3.1 - * @throws moodle_exception */ public static function view_quiz($quizid) { global $DB; @@ -366,10 +365,9 @@ class mod_quiz_external extends external_api { * @param bool $includepreviews whether to include previews or not * @return array of warnings and the list of attempts * @since Moodle 3.1 - * @throws invalid_parameter_exception */ public static function get_user_attempts($quizid, $userid = 0, $status = 'finished', $includepreviews = false) { - global $DB, $USER; + global $USER; $warnings = array(); @@ -711,7 +709,6 @@ class mod_quiz_external extends external_api { * @param bool $forcenew Whether to force a new attempt or not. * @return array of warnings and the attempt basic data * @since Moodle 3.1 - * @throws moodle_quiz_exception */ public static function start_attempt($quizid, $preflightdata = array(), $forcenew = false) { global $DB, $USER; @@ -733,7 +730,7 @@ class mod_quiz_external extends external_api { // Check questions. if (!$quizobj->has_questions()) { - throw new moodle_quiz_exception($quizobj, 'noquestionsfound'); + throw new moodle_exception('noquestionsfound', 'quiz', $quizobj->view_url()); } // Create an object to manage all the other (non-roles) access rules. @@ -767,7 +764,7 @@ class mod_quiz_external extends external_api { $errors = $accessmanager->validate_preflight_check($provideddata, [], $currentattemptid); if (!empty($errors)) { - throw new moodle_quiz_exception($quizobj, array_shift($errors)); + throw new moodle_exception(array_shift($errors), 'quiz', $quizobj->view_url()); } // Pre-flight check passed. @@ -776,9 +773,9 @@ class mod_quiz_external extends external_api { if ($currentattemptid) { if ($lastattempt->state == quiz_attempt::OVERDUE) { - throw new moodle_quiz_exception($quizobj, 'stateoverdue'); + throw new moodle_exception('stateoverdue', 'quiz', $quizobj->view_url()); } else { - throw new moodle_quiz_exception($quizobj, 'attemptstillinprogress'); + throw new moodle_exception('attemptstillinprogress', 'quiz', $quizobj->view_url()); } } $offlineattempt = WS_SERVER ? true : false; @@ -813,7 +810,6 @@ class mod_quiz_external extends external_api { * @param bool $checkaccessrules whether to check the quiz access rules or not * @param bool $failifoverdue whether to return error if the attempt is overdue * @return array containing the attempt object and access messages - * @throws moodle_quiz_exception * @since Moodle 3.1 */ protected static function validate_attempt($params, $checkaccessrules = true, $failifoverdue = true) { @@ -826,7 +822,7 @@ class mod_quiz_external extends external_api { // Check that this attempt belongs to this user. if ($attemptobj->get_userid() != $USER->id) { - throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'notyourattempt'); + throw new moodle_exception('notyourattempt', 'quiz', $quizobj->view_url()); } // General capabilities check. @@ -844,15 +840,15 @@ class mod_quiz_external extends external_api { $messages = $accessmanager->prevent_access(); if (!$ispreviewuser && $messages) { - throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'attempterror'); + throw new moodle_exception('attempterror', 'quiz', $quizobj->view_url()); } } // Attempt closed?. if ($attemptobj->is_finished()) { - throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'attemptalreadyclosed'); + throw new moodle_exception('attemptalreadyclosed', 'quiz', $attemptobj->view_url()); } else if ($failifoverdue && $attemptobj->get_state() == quiz_attempt::OVERDUE) { - throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'stateoverdue'); + throw new moodle_exception('stateoverdue', 'quiz', $attemptobj->view_url()); } // User submitted data (like the quiz password). @@ -864,7 +860,7 @@ class mod_quiz_external extends external_api { $errors = $accessmanager->validate_preflight_check($provideddata, [], $params['attemptid']); if (!empty($errors)) { - throw new moodle_quiz_exception($attemptobj->get_quizobj(), array_shift($errors)); + throw new moodle_exception(array_shift($errors), 'quiz', $attemptobj->view_url()); } // Pre-flight check passed. $accessmanager->notify_preflight_check_passed($params['attemptid']); @@ -873,19 +869,19 @@ class mod_quiz_external extends external_api { if (isset($params['page'])) { // Check if the page is out of range. if ($params['page'] != $attemptobj->force_page_number_into_range($params['page'])) { - throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'Invalid page number'); + throw new moodle_exception('Invalid page number', 'quiz', $attemptobj->view_url()); } // Prevent out of sequence access. if (!$attemptobj->check_page_access($params['page'])) { - throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'Out of sequence access'); + throw new moodle_exception('Out of sequence access', 'quiz', $attemptobj->view_url()); } // Check slots. $slots = $attemptobj->get_slots($params['page']); if (empty($slots)) { - throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'noquestionsfound'); + throw new moodle_exception('noquestionsfound', 'quiz', $attemptobj->view_url()); } } @@ -1048,7 +1044,6 @@ class mod_quiz_external extends external_api { * @param array $preflightdata preflight required data (like passwords) * @return array of warnings and the attempt data, next page, message and questions * @since Moodle 3.1 - * @throws moodle_quiz_exceptions */ public static function get_attempt_data($attemptid, $page, $preflightdata = array()) { global $PAGE; @@ -1372,8 +1367,6 @@ class mod_quiz_external extends external_api { * @param array $params Array of parameters including the attemptid * @return array containing the attempt object and display options * @since Moodle 3.1 - * @throws moodle_exception - * @throws moodle_quiz_exception */ protected static function validate_attempt_review($params) { @@ -1383,13 +1376,13 @@ class mod_quiz_external extends external_api { $displayoptions = $attemptobj->get_display_options(true); if ($attemptobj->is_own_attempt()) { if (!$attemptobj->is_finished()) { - throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'attemptclosed'); + throw new moodle_exception('attemptclosed', 'quiz', $attemptobj->view_url()); } else if (!$displayoptions->attempt) { - throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'noreview', null, '', + throw new moodle_exception('noreview', 'quiz', $attemptobj->view_url(), null, $attemptobj->cannot_review_message()); } } else if (!$attemptobj->is_review_allowed()) { - throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'noreviewattempt'); + throw new moodle_exception('noreviewattempt', 'quiz', $attemptobj->view_url()); } return array($attemptobj, $displayoptions); } @@ -1417,8 +1410,6 @@ class mod_quiz_external extends external_api { * @param int $page page number, empty for all the questions in all the pages * @return array of warnings and the attempt data, feedback and questions * @since Moodle 3.1 - * @throws moodle_exception - * @throws moodle_quiz_exception */ public static function get_attempt_review($attemptid, $page = -1) { global $PAGE; @@ -1548,7 +1539,7 @@ class mod_quiz_external extends external_api { // Update attempt page, throwing an exception if $page is not valid. if (!$attemptobj->set_currentpage($params['page'])) { - throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'Out of sequence access'); + throw new moodle_exception('Out of sequence access', 'quiz', $attemptobj->view_url()); } $result = array(); @@ -1714,7 +1705,6 @@ class mod_quiz_external extends external_api { * @param float $grade the grade to check * @return array of warnings and status result * @since Moodle 3.1 - * @throws moodle_exception */ public static function get_quiz_feedback_for_grade($quizid, $grade) { global $DB; @@ -1785,7 +1775,6 @@ class mod_quiz_external extends external_api { * @param int $quizid quiz instance id * @return array of warnings and the access information * @since Moodle 3.1 - * @throws moodle_quiz_exception */ public static function get_quiz_access_information($quizid) { global $DB, $USER; @@ -1869,7 +1858,6 @@ class mod_quiz_external extends external_api { * @param int $attemptid attempt id, 0 for the user last attempt if exists * @return array of warnings and the access information * @since Moodle 3.1 - * @throws moodle_quiz_exception */ public static function get_attempt_access_information($quizid, $attemptid = 0) { global $DB, $USER; @@ -1888,7 +1876,7 @@ class mod_quiz_external extends external_api { if (!empty($params['attemptid'])) { $attemptobj = quiz_attempt::create($params['attemptid']); if ($attemptobj->get_userid() != $USER->id) { - throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'notyourattempt'); + throw new moodle_exception('notyourattempt', 'quiz', $attemptobj->view_url()); } $attempttocheck = $attemptobj->get_attempt(); } @@ -1975,7 +1963,6 @@ class mod_quiz_external extends external_api { * @param int $quizid quiz instance id * @return array of warnings and the access information * @since Moodle 3.1 - * @throws moodle_quiz_exception */ public static function get_quiz_required_qtypes($quizid) { global $DB, $USER; diff --git a/mod/quiz/deprecatedlib.php b/mod/quiz/deprecatedlib.php index 42a87b04056..0195153b86e 100644 --- a/mod/quiz/deprecatedlib.php +++ b/mod/quiz/deprecatedlib.php @@ -140,7 +140,7 @@ function quiz_get_completion_state($course, $cm, $userid, $type) { * @copyright 2012 the Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @deprecated since Moodle 4.2. Code moved to mod_quiz\task\update_overdue_attempts. - * @todo MDL-71196 Final deprecation in Moodle 4.3 + * @todo MDL-76612 Final deprecation in Moodle 4.6 */ class mod_quiz_overdue_attempt_updater { @@ -162,3 +162,33 @@ class mod_quiz_overdue_attempt_updater { return (new update_overdue_attempts())->get_list_of_overdue_attempts((int) $processto); } } + +/** + * Class for quiz exceptions. Just saves a couple of arguments on the + * constructor for a moodle_exception. + * + * @copyright 2008 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 2.0 + * @deprecated since Moodle 4.2. Please just use moodle_exception. + * @todo MDL-76612 Final deprecation in Moodle 4.6 + */ +class moodle_quiz_exception extends moodle_exception { + /** + * Constructor. + * + * @param quiz $quizobj the quiz the error relates to. + * @param string $errorcode The name of the string from error.php to print. + * @param mixed $a Extra words and phrases that might be required in the error string. + * @param string $link The url where the user will be prompted to continue. + * If no url is provided the user will be directed to the site index page. + * @param string|null $debuginfo optional debugging information. + * @deprecated since Moodle 4.2. Please just use moodle_exception. + */ + public function __construct($quizobj, $errorcode, $a = null, $link = '', $debuginfo = null) { + if (!$link) { + $link = $quizobj->view_url(); + } + parent::__construct($errorcode, 'quiz', $link, $a, $debuginfo); + } +} diff --git a/mod/quiz/locallib.php b/mod/quiz/locallib.php index db534cc1a68..17d5adbc0da 100644 --- a/mod/quiz/locallib.php +++ b/mod/quiz/locallib.php @@ -2442,7 +2442,6 @@ function quiz_view($quiz, $course, $cm, $context) { * @param int $page page to jump to in the attempt * @param bool $redirect whether to redirect or throw exceptions (for web or ws usage) * @return array an array containing the attempt information, access error messages and the page to jump to in the attempt - * @throws moodle_quiz_exception * @since Moodle 3.1 */ function quiz_validate_new_attempt(quiz $quizobj, access_manager $accessmanager, $forcenew, $page, $redirect) { @@ -2485,7 +2484,7 @@ function quiz_validate_new_attempt(quiz $quizobj, access_manager $accessmanager, if ($redirect) { redirect($quizobj->review_url($lastattempt->id)); } else { - throw new moodle_quiz_exception($quizobj, 'attemptalreadyclosed'); + throw new moodle_exception('attemptalreadyclosed', 'quiz', $quizobj->view_url()); } } diff --git a/mod/quiz/processattempt.php b/mod/quiz/processattempt.php index 50dba84d6ee..8e0d594550b 100644 --- a/mod/quiz/processattempt.php +++ b/mod/quiz/processattempt.php @@ -71,7 +71,7 @@ require_sesskey(); // Check that this attempt belongs to this user. if ($attemptobj->get_userid() != $USER->id) { - throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'notyourattempt'); + throw new moodle_exception('notyourattempt', 'quiz', $attemptobj->view_url()); } // Check capabilities. @@ -81,8 +81,7 @@ if (!$attemptobj->is_preview_user()) { // If the attempt is already closed, send them to the review page. if ($attemptobj->is_finished()) { - throw new moodle_quiz_exception($attemptobj->get_quizobj(), - 'attemptalreadyclosed', null, $attemptobj->review_url()); + throw new moodle_exception('attemptalreadyclosed', 'quiz', $attemptobj->view_url()); } // Process the attempt, getting the new status for the attempt. diff --git a/mod/quiz/review.php b/mod/quiz/review.php index 5c9e2ef9440..b230b6eb590 100644 --- a/mod/quiz/review.php +++ b/mod/quiz/review.php @@ -76,7 +76,7 @@ if ($attemptobj->is_own_attempt()) { } } else if (!$attemptobj->is_review_allowed()) { - throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'noreviewattempt'); + throw new moodle_exception('noreviewattempt', 'quiz', $attemptobj->view_url()); } // Load the questions and states needed by this page. diff --git a/mod/quiz/reviewquestion.php b/mod/quiz/reviewquestion.php index f49306b9774..9bdb904dc00 100644 --- a/mod/quiz/reviewquestion.php +++ b/mod/quiz/reviewquestion.php @@ -71,7 +71,7 @@ if ($attemptobj->is_own_attempt()) { } } else if (!$attemptobj->is_review_allowed()) { - throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'noreviewattempt'); + throw new moodle_exception('noreviewattempt', 'quiz', $attemptobj->view_url()); } // Prepare summary informat about this question attempt. diff --git a/mod/quiz/summary.php b/mod/quiz/summary.php index 496ed8e960c..dc429195f8d 100644 --- a/mod/quiz/summary.php +++ b/mod/quiz/summary.php @@ -44,7 +44,7 @@ if ($attemptobj->get_userid() != $USER->id) { if ($attemptobj->has_capability('mod/quiz:viewreports')) { redirect($attemptobj->review_url(null)); } else { - throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'notyourattempt'); + throw new moodle_exception('notyourattempt', 'quiz', $attemptobj->view_url()); } } diff --git a/mod/quiz/tests/external/external_test.php b/mod/quiz/tests/external/external_test.php index 41f4b903e3a..58b085df251 100644 --- a/mod/quiz/tests/external/external_test.php +++ b/mod/quiz/tests/external/external_test.php @@ -29,6 +29,7 @@ namespace mod_quiz\external; use externallib_advanced_testcase; use mod_quiz_external; use mod_quiz\question\display_options; +use moodle_exception; use quiz; use quiz_attempt; @@ -340,7 +341,7 @@ class external_test extends externallib_advanced_testcase { try { mod_quiz_external::view_quiz(0); $this->fail('Exception expected due to invalid mod_quiz instance id.'); - } catch (\moodle_exception $e) { + } catch (moodle_exception $e) { $this->assertEquals('invalidrecord', $e->errorcode); } @@ -350,7 +351,7 @@ class external_test extends externallib_advanced_testcase { try { mod_quiz_external::view_quiz($this->quiz->id); $this->fail('Exception expected due to not enrolled user.'); - } catch (\moodle_exception $e) { + } catch (moodle_exception $e) { $this->assertEquals('requireloginerror', $e->errorcode); } @@ -386,7 +387,7 @@ class external_test extends externallib_advanced_testcase { try { mod_quiz_external::view_quiz($this->quiz->id); $this->fail('Exception expected due to missing capability.'); - } catch (\moodle_exception $e) { + } catch (moodle_exception $e) { $this->assertEquals('requireloginerror', $e->errorcode); } @@ -846,7 +847,7 @@ class external_test extends externallib_advanced_testcase { try { mod_quiz_external::start_attempt($quiz->id, array(array("name" => "quizpassword", "value" => 'bad'))); $this->fail('Exception expected due to invalid passwod.'); - } catch (\moodle_exception $e) { + } catch (moodle_exception $e) { $this->assertEquals(get_string('passworderror', 'quizaccess_password'), $e->errorcode); } @@ -865,7 +866,7 @@ class external_test extends externallib_advanced_testcase { try { mod_quiz_external::start_attempt($quiz->id, array(array("name" => "quizpassword", "value" => 'abc'))); $this->fail('Exception expected due to attempt not finished.'); - } catch (\moodle_quiz_exception $e) { + } catch (moodle_exception $e) { $this->assertEquals('attemptstillinprogress', $e->errorcode); } @@ -942,7 +943,7 @@ class external_test extends externallib_advanced_testcase { 'preflightdata' => array(array("name" => "quizpassword", "value" => 'bad'))); testable_mod_quiz_external::validate_attempt($params); $this->fail('Exception expected due to invalid passwod.'); - } catch (\moodle_exception $e) { + } catch (moodle_exception $e) { $this->assertEquals(get_string('passworderror', 'quizaccess_password'), $e->errorcode); } @@ -958,7 +959,7 @@ class external_test extends externallib_advanced_testcase { try { testable_mod_quiz_external::validate_attempt($params); $this->fail('Exception expected due to page out of range.'); - } catch (\moodle_quiz_exception $e) { + } catch (moodle_exception $e) { $this->assertEquals('Invalid page number', $e->errorcode); } @@ -975,7 +976,7 @@ class external_test extends externallib_advanced_testcase { try { testable_mod_quiz_external::validate_attempt($params); $this->fail('Exception expected due to passed dates.'); - } catch (\moodle_quiz_exception $e) { + } catch (moodle_exception $e) { $this->assertEquals('attempterror', $e->errorcode); } @@ -986,7 +987,7 @@ class external_test extends externallib_advanced_testcase { try { testable_mod_quiz_external::validate_attempt($params, false); $this->fail('Exception expected due to attempt finished.'); - } catch (\moodle_quiz_exception $e) { + } catch (moodle_exception $e) { $this->assertEquals('attemptalreadyclosed', $e->errorcode); } @@ -1011,7 +1012,7 @@ class external_test extends externallib_advanced_testcase { try { testable_mod_quiz_external::validate_attempt($params); $this->fail('Exception expected due to not your attempt.'); - } catch (\moodle_quiz_exception $e) { + } catch (moodle_exception $e) { $this->assertEquals('notyourattempt', $e->errorcode); } } @@ -1504,7 +1505,7 @@ class external_test extends externallib_advanced_testcase { $params = array('attemptid' => $attempt->id); testable_mod_quiz_external::validate_attempt_review($params); $this->fail('Exception expected due not closed attempt.'); - } catch (\moodle_quiz_exception $e) { + } catch (moodle_exception $e) { $this->assertEquals('attemptclosed', $e->errorcode); } @@ -1527,7 +1528,7 @@ class external_test extends externallib_advanced_testcase { $params = array('attemptid' => $attempt->id); testable_mod_quiz_external::validate_attempt_review($params); $this->fail('Exception expected due missing permissions.'); - } catch (\moodle_quiz_exception $e) { + } catch (moodle_exception $e) { $this->assertEquals('noreviewattempt', $e->errorcode); } } @@ -1642,7 +1643,7 @@ class external_test extends externallib_advanced_testcase { try { mod_quiz_external::view_attempt($attempt->id, 0); $this->fail('Exception expected due to try to see a previous page.'); - } catch (\moodle_quiz_exception $e) { + } catch (moodle_exception $e) { $this->assertEquals('Out of sequence access', $e->errorcode); } @@ -2026,7 +2027,7 @@ class external_test extends externallib_advanced_testcase { try { mod_quiz_external::view_attempt($attemptobj->get_attemptid(), 3, []); $this->fail('Exception expected due to out of sequence access.'); - } catch (\moodle_exception $e) { + } catch (moodle_exception $e) { $this->assertStringContainsString('quiz/Out of sequence access', $e->getMessage()); } } @@ -2057,7 +2058,7 @@ class external_test extends externallib_advanced_testcase { try { mod_quiz_external::get_attempt_data($attemptobj->get_attemptid(), 2); $this->fail('Exception expected due to out of sequence access.'); - } catch (\moodle_exception $e) { + } catch (moodle_exception $e) { $this->assertStringContainsString('quiz/Out of sequence access', $e->getMessage()); } // Now we moved to page 1, we should see page 2 and 1 but not 0 or 3. @@ -2066,14 +2067,14 @@ class external_test extends externallib_advanced_testcase { try { mod_quiz_external::get_attempt_data($attemptobj->get_attemptid(), 0); $this->fail('Exception expected due to out of sequence access.'); - } catch (\moodle_exception $e) { + } catch (moodle_exception $e) { $this->assertStringContainsString('quiz/Out of sequence access', $e->getMessage()); } try { mod_quiz_external::get_attempt_data($attemptobj->get_attemptid(), 3); $this->fail('Exception expected due to out of sequence access.'); - } catch (\moodle_exception $e) { + } catch (moodle_exception $e) { $this->assertStringContainsString('quiz/Out of sequence access', $e->getMessage()); } @@ -2127,7 +2128,7 @@ class external_test extends externallib_advanced_testcase { * @param int|null $userid * @param bool|null $ispreview * @return quiz_attempt - * @throws \moodle_exception + * @throws moodle_exception */ private function create_quiz_attempt_object(quiz $quizobj, ?int $userid = null, ?bool $ispreview = false): quiz_attempt { global $USER; diff --git a/mod/quiz/upgrade.txt b/mod/quiz/upgrade.txt index 0973fdf4b4d..aaf2da397f7 100644 --- a/mod/quiz/upgrade.txt +++ b/mod/quiz/upgrade.txt @@ -40,6 +40,7 @@ This files describes API changes in the quiz code. * The following classes have been deprecated: - mod_quiz_overdue_attempt_updater - merged into mod_quiz\task\update_overdue_attempts + - moodle_quiz_exception - just use normal moodle_exception * As part of the clean-up, the following files are no longer required, and if you try to include them, you will get a debugging notices telling you not to: From dda9ba07f37d050b5d2085f46b222feb6640bfa6 Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Fri, 16 Dec 2022 17:25:15 +0000 Subject: [PATCH 07/30] MDL-76614 quiz: quiz_override_form => mod_quiz\form\edit_override_form --- mod/quiz/classes/form/edit_override_form.php | 297 +++++++++++++++++++ mod/quiz/db/renamedclasses.php | 1 + mod/quiz/override_form.php | 274 +---------------- mod/quiz/overridedelete.php | 4 +- mod/quiz/overrideedit.php | 5 +- mod/quiz/overrides.php | 2 - mod/quiz/upgrade.txt | 2 + 7 files changed, 307 insertions(+), 278 deletions(-) create mode 100644 mod/quiz/classes/form/edit_override_form.php diff --git a/mod/quiz/classes/form/edit_override_form.php b/mod/quiz/classes/form/edit_override_form.php new file mode 100644 index 00000000000..7cbfcc76293 --- /dev/null +++ b/mod/quiz/classes/form/edit_override_form.php @@ -0,0 +1,297 @@ +. + +namespace mod_quiz\form; + +use cm_info; +use context; +use mod_quiz_mod_form; +use moodle_url; +use moodleform; +use stdClass; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/formslib.php'); +require_once($CFG->dirroot . '/mod/quiz/mod_form.php'); + +/** + * Form for editing quiz settings overrides. + * + * @package mod_quiz + * @copyright 2010 Matt Petro + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class edit_override_form extends moodleform { + + /** @var cm_info course module object. */ + protected $cm; + + /** @var stdClass the quiz settings object. */ + protected $quiz; + + /** @var context the quiz context. */ + protected $context; + + /** @var bool editing group override (true) or user override (false). */ + protected $groupmode; + + /** @var int groupid, if provided. */ + protected $groupid; + + /** @var int userid, if provided. */ + protected $userid; + + /** + * Constructor. + * @param moodle_url $submiturl the form action URL. + * @param object course module object. + * @param object the quiz settings object. + * @param context the quiz context. + * @param bool editing group override (true) or user override (false). + * @param object $override the override being edited, if it already exists. + */ + public function __construct($submiturl, $cm, $quiz, $context, $groupmode, $override) { + + $this->cm = $cm; + $this->quiz = $quiz; + $this->context = $context; + $this->groupmode = $groupmode; + $this->groupid = empty($override->groupid) ? 0 : $override->groupid; + $this->userid = empty($override->userid) ? 0 : $override->userid; + + parent::__construct($submiturl); + } + + protected function definition() { + global $DB; + + $cm = $this->cm; + $mform = $this->_form; + + $mform->addElement('header', 'override', get_string('override', 'quiz')); + + $quizgroupmode = groups_get_activity_groupmode($cm); + $accessallgroups = ($quizgroupmode == NOGROUPS) || has_capability('moodle/site:accessallgroups', $this->context); + + if ($this->groupmode) { + // Group override. + if ($this->groupid) { + // There is already a groupid, so freeze the selector. + $groupchoices = array(); + $groupchoices[$this->groupid] = groups_get_group_name($this->groupid); + $mform->addElement('select', 'groupid', + get_string('overridegroup', 'quiz'), $groupchoices); + $mform->freeze('groupid'); + } else { + // Prepare the list of groups. + // Only include the groups the current can access. + $groups = $accessallgroups ? groups_get_all_groups($cm->course) : groups_get_activity_allowed_groups($cm); + if (empty($groups)) { + // Generate an error. + $link = new moodle_url('/mod/quiz/overrides.php', array('cmid'=>$cm->id)); + throw new \moodle_exception('groupsnone', 'quiz', $link); + } + + $groupchoices = array(); + foreach ($groups as $group) { + $groupchoices[$group->id] = $group->name; + } + unset($groups); + + if (count($groupchoices) == 0) { + $groupchoices[0] = get_string('none'); + } + + $mform->addElement('select', 'groupid', + get_string('overridegroup', 'quiz'), $groupchoices); + $mform->addRule('groupid', get_string('required'), 'required', null, 'client'); + } + } else { + // User override. + $userfieldsapi = \core_user\fields::for_identity($this->context)->with_userpic()->with_name(); + $extrauserfields = $userfieldsapi->get_required_fields([\core_user\fields::PURPOSE_IDENTITY]); + if ($this->userid) { + // There is already a userid, so freeze the selector. + $user = $DB->get_record('user', ['id' => $this->userid]); + profile_load_custom_fields($user); + $userchoices = array(); + $userchoices[$this->userid] = self::display_user_name($user, $extrauserfields); + $mform->addElement('select', 'userid', + get_string('overrideuser', 'quiz'), $userchoices); + $mform->freeze('userid'); + } else { + // Prepare the list of users. + $groupids = 0; + if (!$accessallgroups) { + $groups = groups_get_activity_allowed_groups($cm); + $groupids = array_keys($groups); + } + $enrolledjoin = get_enrolled_with_capabilities_join( + $this->context, '', 'mod/quiz:attempt', $groupids, true); + $userfieldsql = $userfieldsapi->get_sql('u', true, '', '', false); + list($sort, $sortparams) = users_order_by_sql('u', null, + $this->context, $userfieldsql->mappings); + + $users = $DB->get_records_sql(" + SELECT $userfieldsql->selects + FROM {user} u + $enrolledjoin->joins + $userfieldsql->joins + LEFT JOIN {quiz_overrides} existingoverride ON + existingoverride.userid = u.id AND existingoverride.quiz = :quizid + WHERE existingoverride.id IS NULL + AND $enrolledjoin->wheres + ORDER BY $sort + ", array_merge(['quizid' => $this->quiz->id], $userfieldsql->params, $enrolledjoin->params, $sortparams)); + + // Filter users based on any fixed restrictions (groups, profile). + $info = new \core_availability\info_module($cm); + $users = $info->filter_user_list($users); + + if (empty($users)) { + // Generate an error. + $link = new moodle_url('/mod/quiz/overrides.php', array('cmid'=>$cm->id)); + throw new \moodle_exception('usersnone', 'quiz', $link); + } + + $userchoices = []; + foreach ($users as $id => $user) { + $userchoices[$id] = self::display_user_name($user, $extrauserfields); + } + unset($users); + + $mform->addElement('searchableselector', 'userid', + get_string('overrideuser', 'quiz'), $userchoices); + $mform->addRule('userid', get_string('required'), 'required', null, 'client'); + } + } + + // Password. + // This field has to be above the date and timelimit fields, + // otherwise browsers will clear it when those fields are changed. + $mform->addElement('passwordunmask', 'password', get_string('requirepassword', 'quiz')); + $mform->setType('password', PARAM_TEXT); + $mform->addHelpButton('password', 'requirepassword', 'quiz'); + $mform->setDefault('password', $this->quiz->password); + + // Open and close dates. + $mform->addElement('date_time_selector', 'timeopen', + get_string('quizopen', 'quiz'), mod_quiz_mod_form::$datefieldoptions); + $mform->setDefault('timeopen', $this->quiz->timeopen); + + $mform->addElement('date_time_selector', 'timeclose', + get_string('quizclose', 'quiz'), mod_quiz_mod_form::$datefieldoptions); + $mform->setDefault('timeclose', $this->quiz->timeclose); + + // Time limit. + $mform->addElement('duration', 'timelimit', + get_string('timelimit', 'quiz'), array('optional' => true)); + $mform->addHelpButton('timelimit', 'timelimit', 'quiz'); + $mform->setDefault('timelimit', $this->quiz->timelimit); + + // Number of attempts. + $attemptoptions = array('0' => get_string('unlimited')); + for ($i = 1; $i <= QUIZ_MAX_ATTEMPT_OPTION; $i++) { + $attemptoptions[$i] = $i; + } + $mform->addElement('select', 'attempts', + get_string('attemptsallowed', 'quiz'), $attemptoptions); + $mform->addHelpButton('attempts', 'attempts', 'quiz'); + $mform->setDefault('attempts', $this->quiz->attempts); + + // Submit buttons. + $mform->addElement('submit', 'resetbutton', + get_string('reverttodefaults', 'quiz')); + + $buttonarray = array(); + $buttonarray[] = $mform->createElement('submit', 'submitbutton', + get_string('save', 'quiz')); + $buttonarray[] = $mform->createElement('submit', 'againbutton', + get_string('saveoverrideandstay', 'quiz')); + $buttonarray[] = $mform->createElement('cancel'); + + $mform->addGroup($buttonarray, 'buttonbar', '', array(' '), false); + $mform->closeHeaderBefore('buttonbar'); + } + + /** + * Get a user's name and identity ready to display. + * + * @param stdClass $user a user object. + * @param array $extrauserfields (identity fields in user table only from the user_fields API) + * @return string User's name, with extra info, for display. + */ + public static function display_user_name(stdClass $user, array $extrauserfields): string { + $username = fullname($user); + $namefields = []; + foreach ($extrauserfields as $field) { + if (isset($user->$field) && $user->$field !== '') { + $namefields[] = s($user->$field); + } else if (strpos($field, 'profile_field_') === 0) { + $field = substr($field, 14); + if (isset($user->profile[$field]) && $user->profile[$field] !== '') { + $namefields[] = s($user->profile[$field]); + } + } + } + if ($namefields) { + $username .= ' (' . implode(', ', $namefields) . ')'; + } + return $username; + } + + public function validation($data, $files): array { + $errors = parent::validation($data, $files); + + $mform =& $this->_form; + $quiz = $this->quiz; + + if ($mform->elementExists('userid')) { + if (empty($data['userid'])) { + $errors['userid'] = get_string('required'); + } + } + + if ($mform->elementExists('groupid')) { + if (empty($data['groupid'])) { + $errors['groupid'] = get_string('required'); + } + } + + // Ensure that the dates make sense. + if (!empty($data['timeopen']) && !empty($data['timeclose'])) { + if ($data['timeclose'] < $data['timeopen'] ) { + $errors['timeclose'] = get_string('closebeforeopen', 'quiz'); + } + } + + // Ensure that at least one quiz setting was changed. + $changed = false; + $keys = array('timeopen', 'timeclose', 'timelimit', 'attempts', 'password'); + foreach ($keys as $key) { + if ($data[$key] != $quiz->{$key}) { + $changed = true; + break; + } + } + if (!$changed) { + $errors['timeopen'] = get_string('nooverridedata', 'quiz'); + } + + return $errors; + } +} diff --git a/mod/quiz/db/renamedclasses.php b/mod/quiz/db/renamedclasses.php index 7256a8b8890..d6b3086aa07 100644 --- a/mod/quiz/db/renamedclasses.php +++ b/mod/quiz/db/renamedclasses.php @@ -53,4 +53,5 @@ $renamedclasses = [ 'quiz_attempts_report_table' => 'mod_quiz\local\reports\attempts_report_table', 'quiz_access_manager' => 'mod_quiz\access_manager', 'mod_quiz_preflight_check_form' => 'mod_quiz\form\preflight_check_form', + 'quiz_override_form' => 'mod_quiz\form\edit_override_form', ]; diff --git a/mod/quiz/override_form.php b/mod/quiz/override_form.php index 04f33033bbf..4fcf92cd630 100644 --- a/mod/quiz/override_form.php +++ b/mod/quiz/override_form.php @@ -20,278 +20,10 @@ * @package mod_quiz * @copyright 2010 Matt Petro * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @todo MDL-76612 delete this file as part of Moodle 4.6 development. + * @deprecated This file is no longer required in Moodle 4.2+. */ - defined('MOODLE_INTERNAL') || die(); -require_once($CFG->libdir . '/formslib.php'); -require_once($CFG->dirroot . '/mod/quiz/mod_form.php'); - - -/** - * Form for editing settings overrides. - * - * @copyright 2010 Matt Petro - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class quiz_override_form extends moodleform { - - /** @var cm_info course module object. */ - protected $cm; - - /** @var stdClass the quiz settings object. */ - protected $quiz; - - /** @var context the quiz context. */ - protected $context; - - /** @var bool editing group override (true) or user override (false). */ - protected $groupmode; - - /** @var int groupid, if provided. */ - protected $groupid; - - /** @var int userid, if provided. */ - protected $userid; - - /** - * Constructor. - * @param moodle_url $submiturl the form action URL. - * @param object course module object. - * @param object the quiz settings object. - * @param context the quiz context. - * @param bool editing group override (true) or user override (false). - * @param object $override the override being edited, if it already exists. - */ - public function __construct($submiturl, $cm, $quiz, $context, $groupmode, $override) { - - $this->cm = $cm; - $this->quiz = $quiz; - $this->context = $context; - $this->groupmode = $groupmode; - $this->groupid = empty($override->groupid) ? 0 : $override->groupid; - $this->userid = empty($override->userid) ? 0 : $override->userid; - - parent::__construct($submiturl); - } - - protected function definition() { - global $DB; - - $cm = $this->cm; - $mform = $this->_form; - - $mform->addElement('header', 'override', get_string('override', 'quiz')); - - $quizgroupmode = groups_get_activity_groupmode($cm); - $accessallgroups = ($quizgroupmode == NOGROUPS) || has_capability('moodle/site:accessallgroups', $this->context); - - if ($this->groupmode) { - // Group override. - if ($this->groupid) { - // There is already a groupid, so freeze the selector. - $groupchoices = array(); - $groupchoices[$this->groupid] = groups_get_group_name($this->groupid); - $mform->addElement('select', 'groupid', - get_string('overridegroup', 'quiz'), $groupchoices); - $mform->freeze('groupid'); - } else { - // Prepare the list of groups. - // Only include the groups the current can access. - $groups = $accessallgroups ? groups_get_all_groups($cm->course) : groups_get_activity_allowed_groups($cm); - if (empty($groups)) { - // Generate an error. - $link = new moodle_url('/mod/quiz/overrides.php', array('cmid'=>$cm->id)); - throw new \moodle_exception('groupsnone', 'quiz', $link); - } - - $groupchoices = array(); - foreach ($groups as $group) { - $groupchoices[$group->id] = $group->name; - } - unset($groups); - - if (count($groupchoices) == 0) { - $groupchoices[0] = get_string('none'); - } - - $mform->addElement('select', 'groupid', - get_string('overridegroup', 'quiz'), $groupchoices); - $mform->addRule('groupid', get_string('required'), 'required', null, 'client'); - } - } else { - // User override. - $userfieldsapi = \core_user\fields::for_identity($this->context)->with_userpic()->with_name(); - $extrauserfields = $userfieldsapi->get_required_fields([\core_user\fields::PURPOSE_IDENTITY]); - if ($this->userid) { - // There is already a userid, so freeze the selector. - $user = $DB->get_record('user', ['id' => $this->userid]); - profile_load_custom_fields($user); - $userchoices = array(); - $userchoices[$this->userid] = self::display_user_name($user, $extrauserfields); - $mform->addElement('select', 'userid', - get_string('overrideuser', 'quiz'), $userchoices); - $mform->freeze('userid'); - } else { - // Prepare the list of users. - $groupids = 0; - if (!$accessallgroups) { - $groups = groups_get_activity_allowed_groups($cm); - $groupids = array_keys($groups); - } - $enrolledjoin = get_enrolled_with_capabilities_join( - $this->context, '', 'mod/quiz:attempt', $groupids, true); - $userfieldsql = $userfieldsapi->get_sql('u', true, '', '', false); - list($sort, $sortparams) = users_order_by_sql('u', null, - $this->context, $userfieldsql->mappings); - - $users = $DB->get_records_sql(" - SELECT $userfieldsql->selects - FROM {user} u - $enrolledjoin->joins - $userfieldsql->joins - LEFT JOIN {quiz_overrides} existingoverride ON - existingoverride.userid = u.id AND existingoverride.quiz = :quizid - WHERE existingoverride.id IS NULL - AND $enrolledjoin->wheres - ORDER BY $sort - ", array_merge(['quizid' => $this->quiz->id], $userfieldsql->params, $enrolledjoin->params, $sortparams)); - - // Filter users based on any fixed restrictions (groups, profile). - $info = new \core_availability\info_module($cm); - $users = $info->filter_user_list($users); - - if (empty($users)) { - // Generate an error. - $link = new moodle_url('/mod/quiz/overrides.php', array('cmid'=>$cm->id)); - throw new \moodle_exception('usersnone', 'quiz', $link); - } - - $userchoices = []; - foreach ($users as $id => $user) { - $userchoices[$id] = self::display_user_name($user, $extrauserfields); - } - unset($users); - - $mform->addElement('searchableselector', 'userid', - get_string('overrideuser', 'quiz'), $userchoices); - $mform->addRule('userid', get_string('required'), 'required', null, 'client'); - } - } - - // Password. - // This field has to be above the date and timelimit fields, - // otherwise browsers will clear it when those fields are changed. - $mform->addElement('passwordunmask', 'password', get_string('requirepassword', 'quiz')); - $mform->setType('password', PARAM_TEXT); - $mform->addHelpButton('password', 'requirepassword', 'quiz'); - $mform->setDefault('password', $this->quiz->password); - - // Open and close dates. - $mform->addElement('date_time_selector', 'timeopen', - get_string('quizopen', 'quiz'), mod_quiz_mod_form::$datefieldoptions); - $mform->setDefault('timeopen', $this->quiz->timeopen); - - $mform->addElement('date_time_selector', 'timeclose', - get_string('quizclose', 'quiz'), mod_quiz_mod_form::$datefieldoptions); - $mform->setDefault('timeclose', $this->quiz->timeclose); - - // Time limit. - $mform->addElement('duration', 'timelimit', - get_string('timelimit', 'quiz'), array('optional' => true)); - $mform->addHelpButton('timelimit', 'timelimit', 'quiz'); - $mform->setDefault('timelimit', $this->quiz->timelimit); - - // Number of attempts. - $attemptoptions = array('0' => get_string('unlimited')); - for ($i = 1; $i <= QUIZ_MAX_ATTEMPT_OPTION; $i++) { - $attemptoptions[$i] = $i; - } - $mform->addElement('select', 'attempts', - get_string('attemptsallowed', 'quiz'), $attemptoptions); - $mform->addHelpButton('attempts', 'attempts', 'quiz'); - $mform->setDefault('attempts', $this->quiz->attempts); - - // Submit buttons. - $mform->addElement('submit', 'resetbutton', - get_string('reverttodefaults', 'quiz')); - - $buttonarray = array(); - $buttonarray[] = $mform->createElement('submit', 'submitbutton', - get_string('save', 'quiz')); - $buttonarray[] = $mform->createElement('submit', 'againbutton', - get_string('saveoverrideandstay', 'quiz')); - $buttonarray[] = $mform->createElement('cancel'); - - $mform->addGroup($buttonarray, 'buttonbar', '', array(' '), false); - $mform->closeHeaderBefore('buttonbar'); - } - - /** - * Get a user's name and identity ready to display. - * - * @param stdClass $user a user object. - * @param array $extrauserfields (identity fields in user table only from the user_fields API) - * @return string User's name, with extra info, for display. - */ - public static function display_user_name(stdClass $user, array $extrauserfields): string { - $username = fullname($user); - $namefields = []; - foreach ($extrauserfields as $field) { - if (isset($user->$field) && $user->$field !== '') { - $namefields[] = s($user->$field); - } else if (strpos($field, 'profile_field_') === 0) { - $field = substr($field, 14); - if (isset($user->profile[$field]) && $user->profile[$field] !== '') { - $namefields[] = s($user->profile[$field]); - } - } - } - if ($namefields) { - $username .= ' (' . implode(', ', $namefields) . ')'; - } - return $username; - } - - public function validation($data, $files): array { - $errors = parent::validation($data, $files); - - $mform =& $this->_form; - $quiz = $this->quiz; - - if ($mform->elementExists('userid')) { - if (empty($data['userid'])) { - $errors['userid'] = get_string('required'); - } - } - - if ($mform->elementExists('groupid')) { - if (empty($data['groupid'])) { - $errors['groupid'] = get_string('required'); - } - } - - // Ensure that the dates make sense. - if (!empty($data['timeopen']) && !empty($data['timeclose'])) { - if ($data['timeclose'] < $data['timeopen'] ) { - $errors['timeclose'] = get_string('closebeforeopen', 'quiz'); - } - } - - // Ensure that at least one quiz setting was changed. - $changed = false; - $keys = array('timeopen', 'timeclose', 'timelimit', 'attempts', 'password'); - foreach ($keys as $key) { - if ($data[$key] != $quiz->{$key}) { - $changed = true; - break; - } - } - if (!$changed) { - $errors['timeopen'] = get_string('nooverridedata', 'quiz'); - } - - return $errors; - } -} +debugging('This file is no longer required in Moodle 4.2+. Please do not include/require it.', DEBUG_DEVELOPER); diff --git a/mod/quiz/overridedelete.php b/mod/quiz/overridedelete.php index 117ab70fafc..fab6698ac3d 100644 --- a/mod/quiz/overridedelete.php +++ b/mod/quiz/overridedelete.php @@ -22,11 +22,11 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +use mod_quiz\form\edit_override_form; require_once(__DIR__ . '/../../config.php'); require_once($CFG->dirroot.'/mod/quiz/lib.php'); require_once($CFG->dirroot.'/mod/quiz/locallib.php'); -require_once($CFG->dirroot.'/mod/quiz/override_form.php'); $overrideid = required_param('id', PARAM_INT); $confirm = optional_param('confirm', false, PARAM_BOOL); @@ -103,7 +103,7 @@ if ($override->groupid) { profile_load_custom_fields($user); $confirmstr = get_string('overridedeleteusersure', 'quiz', - quiz_override_form::display_user_name($user, + edit_override_form::display_user_name($user, \core_user\fields::get_identity_fields($context))); } diff --git a/mod/quiz/overrideedit.php b/mod/quiz/overrideedit.php index 3450be23ee5..d8e419d8bdc 100644 --- a/mod/quiz/overrideedit.php +++ b/mod/quiz/overrideedit.php @@ -22,12 +22,11 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +use mod_quiz\form\edit_override_form; require_once(__DIR__ . '/../../config.php'); require_once($CFG->dirroot.'/mod/quiz/lib.php'); require_once($CFG->dirroot.'/mod/quiz/locallib.php'); -require_once($CFG->dirroot.'/mod/quiz/override_form.php'); - $cmid = optional_param('cmid', 0, PARAM_INT); $overrideid = optional_param('id', 0, PARAM_INT); @@ -119,7 +118,7 @@ if (!$groupmode) { } // Setup the form. -$mform = new quiz_override_form($url, $cm, $quiz, $context, $groupmode, $override); +$mform = new edit_override_form($url, $cm, $quiz, $context, $groupmode, $override); $mform->set_data($data); if ($mform->is_cancelled()) { diff --git a/mod/quiz/overrides.php b/mod/quiz/overrides.php index d419a0d2410..8bb12c6ab58 100644 --- a/mod/quiz/overrides.php +++ b/mod/quiz/overrides.php @@ -25,8 +25,6 @@ require_once(__DIR__ . '/../../config.php'); require_once($CFG->dirroot.'/mod/quiz/lib.php'); require_once($CFG->dirroot.'/mod/quiz/locallib.php'); -require_once($CFG->dirroot.'/mod/quiz/override_form.php'); - $cmid = required_param('cmid', PARAM_INT); $mode = optional_param('mode', '', PARAM_ALPHA); // One of 'user' or 'group', default is 'group'. diff --git a/mod/quiz/upgrade.txt b/mod/quiz/upgrade.txt index aaf2da397f7..f80bbfd341a 100644 --- a/mod/quiz/upgrade.txt +++ b/mod/quiz/upgrade.txt @@ -37,6 +37,7 @@ This files describes API changes in the quiz code. - quiz_attempts_report_table => mod_quiz\local\reports\attempts_report_table - quiz_access_manager => mod_quiz\access_manager - mod_quiz_preflight_check_form => mod_quiz\form\preflight_check_form + - quiz_override_form => mod_quiz\form\edit_override_form * The following classes have been deprecated: - mod_quiz_overdue_attempt_updater - merged into mod_quiz\task\update_overdue_attempts @@ -52,6 +53,7 @@ This files describes API changes in the quiz code. - mod/quiz/accessmanager.php - mod/quiz/accessmanager_form.php - mod/quiz/cronlib.php + - mod/quiz/override_form.php === 4.1 === From 2ff112c6885a5fefa608fdd0289020c748f4627c Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Fri, 16 Dec 2022 18:07:38 +0000 Subject: [PATCH 08/30] MDL-76614 quiz: quiz_access_rule_base => mod_quiz\local\access_rule_base --- mod/quiz/accessrule/accessrulebase.php | 320 +--------------- .../accessrule/delaybetweenattempts/rule.php | 22 +- mod/quiz/accessrule/ipaddress/rule.php | 22 +- mod/quiz/accessrule/numattempts/rule.php | 22 +- mod/quiz/accessrule/offlineattempts/rule.php | 16 +- mod/quiz/accessrule/openclosedate/rule.php | 21 +- mod/quiz/accessrule/password/rule.php | 22 +- mod/quiz/accessrule/seb/rule.php | 29 +- .../seb/tests/test_helper_trait.php | 3 +- mod/quiz/accessrule/securewindow/rule.php | 18 +- .../securewindow/tests/rule_test.php | 2 +- mod/quiz/accessrule/timelimit/rule.php | 26 +- mod/quiz/accessrule/upgrade.txt | 5 + mod/quiz/classes/access_manager.php | 5 +- mod/quiz/classes/external.php | 4 +- mod/quiz/classes/local/access_rule_base.php | 344 ++++++++++++++++++ mod/quiz/db/renamedclasses.php | 1 + mod/quiz/upgrade.txt | 2 + 18 files changed, 413 insertions(+), 471 deletions(-) create mode 100644 mod/quiz/classes/local/access_rule_base.php diff --git a/mod/quiz/accessrule/accessrulebase.php b/mod/quiz/accessrule/accessrulebase.php index c433c9f8092..5c4c8c7455c 100644 --- a/mod/quiz/accessrule/accessrulebase.php +++ b/mod/quiz/accessrule/accessrulebase.php @@ -15,323 +15,13 @@ // along with Moodle. If not, see . /** - * Base class for rules that restrict the ability to attempt a quiz. + * File only retained to prevent fatal errors in code that tries to require/include this. * - * @package mod_quiz - * @copyright 2011 The Open University - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @todo MDL-76612 delete this file as part of Moodle 4.6 development. + * @deprecated This file is no longer required in Moodle 4.2+. */ - -use mod_quiz\form\preflight_check_form; - defined('MOODLE_INTERNAL') || die(); +debugging('This file is no longer required in Moodle 4.2+. Please do not include/require it.', DEBUG_DEVELOPER); + require_once($CFG->dirroot . '/mod/quiz/locallib.php'); - - -/** - * A base class that defines the interface for the various quiz access rules. - * Most of the methods are defined in a slightly unnatural way because we either - * want to say that access is allowed, or explain the reason why it is block. - * Therefore instead of is_access_allowed(...) we have prevent_access(...) that - * return false if access is permitted, or a string explanation (which is treated - * as true) if access should be blocked. Slighly unnatural, but actually the easiest - * way to implement this. - * - * @copyright 2009 Tim Hunt - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @since Moodle 2.2 - */ -abstract class quiz_access_rule_base { - /** @var stdClass the quiz settings. */ - protected $quiz; - /** @var quiz the quiz object. */ - protected $quizobj; - /** @var int the time to use as 'now'. */ - protected $timenow; - - /** - * Create an instance of this rule for a particular quiz. - * @param quiz $quizobj information about the quiz in question. - * @param int $timenow the time that should be considered as 'now'. - */ - public function __construct($quizobj, $timenow) { - $this->quizobj = $quizobj; - $this->quiz = $quizobj->get_quiz(); - $this->timenow = $timenow; - } - - /** - * Return an appropriately configured instance of this rule, if it is applicable - * to the given quiz, otherwise return null. - * @param quiz $quizobj information about the quiz in question. - * @param int $timenow the time that should be considered as 'now'. - * @param bool $canignoretimelimits whether the current user is exempt from - * time limits by the mod/quiz:ignoretimelimits capability. - * @return quiz_access_rule_base|null the rule, if applicable, else null. - */ - public static function make(quiz $quizobj, $timenow, $canignoretimelimits) { - return null; - } - - /** - * Whether or not a user should be allowed to start a new attempt at this quiz now. - * @param int $numattempts the number of previous attempts this user has made. - * @param object $lastattempt information about the user's last completed attempt. - * @return string false if access should be allowed, a message explaining the - * reason if access should be prevented. - */ - public function prevent_new_attempt($numprevattempts, $lastattempt) { - return false; - } - - /** - * Whether the user should be blocked from starting a new attempt or continuing - * an attempt now. - * @return string false if access should be allowed, a message explaining the - * reason if access should be prevented. - */ - public function prevent_access() { - return false; - } - - /** - * @param int|null $attemptid the id of the current attempt, if there is one, - * otherwise null. - * @return bool whether a check is required before the user starts/continues - * their attempt. - */ - public function is_preflight_check_required($attemptid) { - return false; - } - - /** - * Add any field you want to pre-flight check form. You should only do - * something here if {@link is_preflight_check_required()} returned true. - * - * @param preflight_check_form $quizform the form being built. - * @param MoodleQuickForm $mform The wrapped MoodleQuickForm. - * @param int|null $attemptid the id of the current attempt, if there is one, - * otherwise null. - */ - public function add_preflight_check_form_fields(preflight_check_form $quizform, - MoodleQuickForm $mform, $attemptid) { - // Do nothing by default. - } - - /** - * Validate the pre-flight check form submission. You should only do - * something here if {@link is_preflight_check_required()} returned true. - * - * If the form validates, the user will be allowed to continue. - * - * @param array $data the submitted form data. - * @param array $files any files in the submission. - * @param array $errors the list of validation errors that is being built up. - * @param int|null $attemptid the id of the current attempt, if there is one, - * otherwise null. - * @return array the update $errors array; - */ - public function validate_preflight_check($data, $files, $errors, $attemptid) { - return $errors; - } - - /** - * The pre-flight check has passed. This is a chance to record that fact in - * some way. - * @param int|null $attemptid the id of the current attempt, if there is one, - * otherwise null. - */ - public function notify_preflight_check_passed($attemptid) { - // Do nothing by default. - } - - /** - * This is called when the current attempt at the quiz is finished. This is - * used, for example by the password rule, to clear the flag in the session. - */ - public function current_attempt_finished() { - // Do nothing by default. - } - - /** - * Information, such as might be shown on the quiz view page, relating to this restriction. - * There is no obligation to return anything. If it is not appropriate to tell students - * about this rule, then just return ''. - * @return mixed a message, or array of messages, explaining the restriction - * (may be '' if no message is appropriate). - */ - public function description() { - return ''; - } - - /** - * If this rule can determine that this user will never be allowed another attempt at - * this quiz, then return true. This is used so we can know whether to display a - * final grade on the view page. This will only be called if there is not a currently - * active attempt for this user. - * @param int $numattempts the number of previous attempts this user has made. - * @param object $lastattempt information about the user's last completed attempt. - * @return bool true if this rule means that this user will never be allowed another - * attempt at this quiz. - */ - public function is_finished($numprevattempts, $lastattempt) { - return false; - } - - /** - * If, because of this rule, the user has to finish their attempt by a certain time, - * you should override this method to return the attempt end time. - * @param object $attempt the current attempt - * @return mixed the attempt close time, or false if there is no close time. - */ - public function end_time($attempt) { - return false; - } - - /** - * If the user should be shown a different amount of time than $timenow - $this->end_time(), then - * override this method. This is useful if the time remaining is large enough to be omitted. - * @param object $attempt the current attempt - * @param int $timenow the time now. We don't use $this->timenow, so we can - * give the user a more accurate indication of how much time is left. - * @return mixed the time left in seconds (can be negative) or false if there is no limit. - */ - public function time_left_display($attempt, $timenow) { - $endtime = $this->end_time($attempt); - if ($endtime === false) { - return false; - } - return $endtime - $timenow; - } - - /** - * @return boolean whether this rule requires that the attemp (and review) - * pages must be displayed in a pop-up window. - */ - public function attempt_must_be_in_popup() { - return false; - } - - /** - * @return array any options that are required for showing the attempt page - * in a popup window. - */ - public function get_popup_options() { - return array(); - } - - /** - * Sets up the attempt (review or summary) page with any special extra - * properties required by this rule. securewindow rule is an example of where - * this is used. - * - * @param moodle_page $page the page object to initialise. - */ - public function setup_attempt_page($page) { - // Do nothing by default. - } - - /** - * It is possible for one rule to override other rules. - * - * The aim is that third-party rules should be able to replace sandard rules - * if they want. See, for example MDL-13592. - * - * @return array plugin names of other rules that this one replaces. - * For example array('ipaddress', 'password'). - */ - public function get_superceded_rules() { - return array(); - } - - /** - * Add any fields that this rule requires to the quiz settings form. This - * method is called from {@link mod_quiz_mod_form::definition()}, while the - * security seciton is being built. - * @param mod_quiz_mod_form $quizform the quiz settings form that is being built. - * @param MoodleQuickForm $mform the wrapped MoodleQuickForm. - */ - public static function add_settings_form_fields( - mod_quiz_mod_form $quizform, MoodleQuickForm $mform) { - // By default do nothing. - } - - /** - * Validate the data from any form fields added using {@link add_settings_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 mod_quiz_mod_form $quizform the quiz form object. - * @return array $errors the updated $errors array. - */ - public static function validate_settings_form_fields(array $errors, - array $data, $files, mod_quiz_mod_form $quizform) { - - return $errors; - } - - /** - * @return array key => lang string any choices to add to the quiz Browser - * security settings menu. - */ - public static function get_browser_security_choices() { - return array(); - } - - /** - * Save any submitted settings when the quiz settings form is submitted. This - * is called from {@link quiz_after_add_or_update()} in lib.php. - * @param object $quiz the data from the quiz form, including $quiz->id - * which is the id of the quiz being saved. - */ - public static function save_settings($quiz) { - // By default do nothing. - } - - /** - * Delete any rule-specific settings when the quiz is deleted. This is called - * from {@link quiz_delete_instance()} in lib.php. - * @param object $quiz the data from the database, including $quiz->id - * which is the id of the quiz being deleted. - * @since Moodle 2.7.1, 2.6.4, 2.5.7 - */ - public static function delete_settings($quiz) { - // By default do nothing. - } - - /** - * Return the bits of SQL needed to load all the settings from all the access - * plugins in one DB query. The easiest way to understand what you need to do - * here is probably to read the code of {@see access_manager::load_settings()}. - * - * If you have some settings that cannot be loaded in this way, then you can - * use the {@link get_extra_settings()} method instead, but that has - * performance implications. - * - * @param int $quizid the id of the quiz we are loading settings for. This - * can also be accessed as quiz.id in the SQL. (quiz is a table alisas for {quiz}.) - * @return array with three elements: - * 1. fields: any fields to add to the select list. These should be alised - * if neccessary so that the field name starts the name of the plugin. - * 2. joins: any joins (should probably be LEFT JOINS) with other tables that - * are needed. - * 3. params: array of placeholder values that are needed by the SQL. You must - * used named placeholders, and the placeholder names should start with the - * plugin name, to avoid collisions. - */ - public static function get_settings_sql($quizid) { - return array('', '', array()); - } - - /** - * You can use this method to load any extra settings your plugin has that - * cannot be loaded efficiently with get_settings_sql(). - * @param int $quizid the quiz id. - * @return array setting value name => value. The value names should all - * start with the name of your plugin to avoid collisions. - */ - public static function get_extra_settings($quizid) { - return array(); - } -} diff --git a/mod/quiz/accessrule/delaybetweenattempts/rule.php b/mod/quiz/accessrule/delaybetweenattempts/rule.php index c5153aa0b21..81a0581fb26 100644 --- a/mod/quiz/accessrule/delaybetweenattempts/rule.php +++ b/mod/quiz/accessrule/delaybetweenattempts/rule.php @@ -14,28 +14,16 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Implementaton of the quizaccess_delaybetweenattempts plugin. - * - * @package quizaccess - * @subpackage delaybetweenattempts - * @copyright 2011 The Open University - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - - -defined('MOODLE_INTERNAL') || die(); - -require_once($CFG->dirroot . '/mod/quiz/accessrule/accessrulebase.php'); - +use mod_quiz\local\access_rule_base; /** * A rule imposing the delay between attempts settings. * - * @copyright 2009 Tim Hunt - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @package quizaccess_delaybetweenattempts + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class quizaccess_delaybetweenattempts extends quiz_access_rule_base { +class quizaccess_delaybetweenattempts extends access_rule_base { public static function make(quiz $quizobj, $timenow, $canignoretimelimits) { if (empty($quizobj->get_quiz()->delay1) && empty($quizobj->get_quiz()->delay2)) { diff --git a/mod/quiz/accessrule/ipaddress/rule.php b/mod/quiz/accessrule/ipaddress/rule.php index fa666a1e2e8..f6670d87a1d 100644 --- a/mod/quiz/accessrule/ipaddress/rule.php +++ b/mod/quiz/accessrule/ipaddress/rule.php @@ -14,28 +14,16 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Implementaton of the quizaccess_ipaddress plugin. - * - * @package quizaccess - * @subpackage ipaddress - * @copyright 2011 The Open University - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - - -defined('MOODLE_INTERNAL') || die(); - -require_once($CFG->dirroot . '/mod/quiz/accessrule/accessrulebase.php'); - +use mod_quiz\local\access_rule_base; /** * A rule implementing the ipaddress check against the ->subnet setting. * - * @copyright 2009 Tim Hunt - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @package quizaccess_ipaddress + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class quizaccess_ipaddress extends quiz_access_rule_base { +class quizaccess_ipaddress extends access_rule_base { public static function make(quiz $quizobj, $timenow, $canignoretimelimits) { if (empty($quizobj->get_quiz()->subnet)) { diff --git a/mod/quiz/accessrule/numattempts/rule.php b/mod/quiz/accessrule/numattempts/rule.php index e69faf74bfb..517ba24191b 100644 --- a/mod/quiz/accessrule/numattempts/rule.php +++ b/mod/quiz/accessrule/numattempts/rule.php @@ -14,28 +14,16 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Implementaton of the quizaccess_numattempts plugin. - * - * @package quizaccess - * @subpackage numattempts - * @copyright 2011 The Open University - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - - -defined('MOODLE_INTERNAL') || die(); - -require_once($CFG->dirroot . '/mod/quiz/accessrule/accessrulebase.php'); - +use mod_quiz\local\access_rule_base; /** * A rule controlling the number of attempts allowed. * - * @copyright 2009 Tim Hunt - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @package quizaccess_numattempts + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class quizaccess_numattempts extends quiz_access_rule_base { +class quizaccess_numattempts extends access_rule_base { public static function make(quiz $quizobj, $timenow, $canignoretimelimits) { diff --git a/mod/quiz/accessrule/offlineattempts/rule.php b/mod/quiz/accessrule/offlineattempts/rule.php index f88f3832854..095b668902a 100644 --- a/mod/quiz/accessrule/offlineattempts/rule.php +++ b/mod/quiz/accessrule/offlineattempts/rule.php @@ -14,28 +14,18 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Implementaton of the quizaccess_offlineattempts plugin. - * - * @package quizaccess_offlineattempts - * @copyright 2016 Juan Leyva - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - use mod_quiz\form\preflight_check_form; - -defined('MOODLE_INTERNAL') || die(); - -require_once($CFG->dirroot . '/mod/quiz/accessrule/accessrulebase.php'); +use mod_quiz\local\access_rule_base; /** * A rule implementing the offlineattempts check. * + * @package quizaccess_offlineattempts * @copyright 2016 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 3.2 */ -class quizaccess_offlineattempts extends quiz_access_rule_base { +class quizaccess_offlineattempts extends access_rule_base { public static function make(quiz $quizobj, $timenow, $canignoretimelimits) { global $CFG; diff --git a/mod/quiz/accessrule/openclosedate/rule.php b/mod/quiz/accessrule/openclosedate/rule.php index 2bb70dac93a..62f332e7b5b 100644 --- a/mod/quiz/accessrule/openclosedate/rule.php +++ b/mod/quiz/accessrule/openclosedate/rule.php @@ -14,28 +14,17 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Implementaton of the quizaccess_openclosedate plugin. - * - * @package quizaccess - * @subpackage openclosedate - * @copyright 2011 The Open University - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - - -defined('MOODLE_INTERNAL') || die(); - -require_once($CFG->dirroot . '/mod/quiz/accessrule/accessrulebase.php'); +use mod_quiz\local\access_rule_base; /** * A rule enforcing open and close dates. * - * @copyright 2009 Tim Hunt - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @package quizaccess_openclosedate + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class quizaccess_openclosedate extends quiz_access_rule_base { +class quizaccess_openclosedate extends access_rule_base { public static function make(quiz $quizobj, $timenow, $canignoretimelimits) { // This rule is always used, even if the quiz has no open or close date. diff --git a/mod/quiz/accessrule/password/rule.php b/mod/quiz/accessrule/password/rule.php index 6e48b904178..7c8c3f7cb99 100644 --- a/mod/quiz/accessrule/password/rule.php +++ b/mod/quiz/accessrule/password/rule.php @@ -14,29 +14,17 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Implementaton of the quizaccess_password plugin. - * - * @package quizaccess - * @subpackage password - * @copyright 2011 The Open University - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - use mod_quiz\form\preflight_check_form; - -defined('MOODLE_INTERNAL') || die(); - -require_once($CFG->dirroot . '/mod/quiz/accessrule/accessrulebase.php'); - +use mod_quiz\local\access_rule_base; /** * A rule implementing the password check. * - * @copyright 2009 Tim Hunt - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @package quizaccess_password + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class quizaccess_password extends quiz_access_rule_base { +class quizaccess_password extends access_rule_base { public static function make(quiz $quizobj, $timenow, $canignoretimelimits) { if (empty($quizobj->get_quiz()->password)) { diff --git a/mod/quiz/accessrule/seb/rule.php b/mod/quiz/accessrule/seb/rule.php index a9f53bf01c9..27cc12cfe5b 100644 --- a/mod/quiz/accessrule/seb/rule.php +++ b/mod/quiz/accessrule/seb/rule.php @@ -14,6 +14,12 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +use mod_quiz\local\access_rule_base; +use quizaccess_seb\access_manager; +use quizaccess_seb\quiz_settings; +use quizaccess_seb\settings_provider; +use quizaccess_seb\event\access_prevented; + /** * Implementation of the quizaccess_seb plugin. * @@ -23,24 +29,7 @@ * @copyright 2019 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - -use quizaccess_seb\access_manager; -use quizaccess_seb\quiz_settings; -use quizaccess_seb\settings_provider; -use \quizaccess_seb\event\access_prevented; - -defined('MOODLE_INTERNAL') || die(); - -global $CFG; -require_once($CFG->dirroot . '/mod/quiz/accessrule/accessrulebase.php'); - -/** - * Implementation of the quizaccess_seb plugin. - * - * @copyright 2020 Catalyst IT - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class quizaccess_seb extends quiz_access_rule_base { +class quizaccess_seb extends access_rule_base { /** @var access_manager $accessmanager Instance to manage the access to the quiz for this plugin. */ private $accessmanager; @@ -65,9 +54,9 @@ class quizaccess_seb extends quiz_access_rule_base { * @param int $timenow the time that should be considered as 'now'. * @param bool $canignoretimelimits whether the current user is exempt from * time limits by the mod/quiz:ignoretimelimits capability. - * @return quiz_access_rule_base|null the rule, if applicable, else null. + * @return access_rule_base|null the rule, if applicable, else null. */ - public static function make (quiz $quizobj, $timenow, $canignoretimelimits) { + public static function make(quiz $quizobj, $timenow, $canignoretimelimits) { $accessmanager = new access_manager($quizobj); // If Safe Exam Browser is not required, this access rule is not applicable. if (!$accessmanager->seb_required()) { diff --git a/mod/quiz/accessrule/seb/tests/test_helper_trait.php b/mod/quiz/accessrule/seb/tests/test_helper_trait.php index f09c2a7a51e..b4ee1137734 100644 --- a/mod/quiz/accessrule/seb/tests/test_helper_trait.php +++ b/mod/quiz/accessrule/seb/tests/test_helper_trait.php @@ -23,6 +23,7 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +use mod_quiz\local\access_rule_base; use quizaccess_seb\access_manager; use quizaccess_seb\settings_provider; @@ -247,7 +248,7 @@ trait quizaccess_seb_test_helper_trait { /** * A helper method to make the rule form the currently created quiz and course. * - * @return \quiz_access_rule_base|null + * @return access_rule_base|null */ protected function make_rule() { return \quizaccess_seb::make( diff --git a/mod/quiz/accessrule/securewindow/rule.php b/mod/quiz/accessrule/securewindow/rule.php index 1857dbcd7e5..75f8e2c4aa0 100644 --- a/mod/quiz/accessrule/securewindow/rule.php +++ b/mod/quiz/accessrule/securewindow/rule.php @@ -14,29 +14,17 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Implementaton of the quizaccess_securewindow plugin. - * - * @package quizaccess - * @subpackage securewindow - * @copyright 2011 The Open University - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - - -defined('MOODLE_INTERNAL') || die(); - -require_once($CFG->dirroot . '/mod/quiz/accessrule/accessrulebase.php'); - +use mod_quiz\local\access_rule_base; /** * A rule for ensuring that the quiz is opened in a popup, with some JavaScript * to prevent copying and pasting, etc. * + * @package quizaccess_securewindow * @copyright 2009 Tim Hunt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class quizaccess_securewindow extends quiz_access_rule_base { +class quizaccess_securewindow extends access_rule_base { /** @var array options that should be used for opening the secure popup. */ protected static $popupoptions = array( 'left' => 0, diff --git a/mod/quiz/accessrule/securewindow/tests/rule_test.php b/mod/quiz/accessrule/securewindow/tests/rule_test.php index 0b0c3e3c60f..8ec234c97b1 100644 --- a/mod/quiz/accessrule/securewindow/tests/rule_test.php +++ b/mod/quiz/accessrule/securewindow/tests/rule_test.php @@ -32,7 +32,7 @@ require_once($CFG->dirroot . '/mod/quiz/accessrule/securewindow/rule.php'); * @copyright 2008 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * - * @covers \quiz_access_rule_base + * @covers \mod_quiz\local\access_rule_base * @covers \quizaccess_securewindow */ class rule_test extends \basic_testcase { diff --git a/mod/quiz/accessrule/timelimit/rule.php b/mod/quiz/accessrule/timelimit/rule.php index 08dd92d4a6b..806d5591a7a 100644 --- a/mod/quiz/accessrule/timelimit/rule.php +++ b/mod/quiz/accessrule/timelimit/rule.php @@ -14,30 +14,20 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Implementaton of the quizaccess_timelimit plugin. - * - * @package quizaccess - * @subpackage timelimit - * @copyright 2011 The Open University - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - use mod_quiz\form\preflight_check_form; - -defined('MOODLE_INTERNAL') || die(); - -require_once($CFG->dirroot . '/mod/quiz/accessrule/accessrulebase.php'); - +use mod_quiz\local\access_rule_base; /** - * A rule representing the time limit. It does not actually restrict access, but we use this + * A rule representing the time limit. + * + * It does not actually restrict access, but we use this * class to encapsulate some of the relevant code. * - * @copyright 2009 Tim Hunt - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @package quizaccess_timelimit + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class quizaccess_timelimit extends quiz_access_rule_base { +class quizaccess_timelimit extends access_rule_base { public static function make(quiz $quizobj, $timenow, $canignoretimelimits) { diff --git a/mod/quiz/accessrule/upgrade.txt b/mod/quiz/accessrule/upgrade.txt index 41e505834bd..26a866c73e9 100644 --- a/mod/quiz/accessrule/upgrade.txt +++ b/mod/quiz/accessrule/upgrade.txt @@ -6,6 +6,11 @@ Overview of this plugin type at http://docs.moodle.org/dev/Quiz_access_rules * Note that class mod_quiz_preflight_check_form has been renamed to mod_quiz\form\preflight_check_form. +* The base class quiz_access_rule_base has been moved to mod_quiz\local\access_rule_base. + Please: + 1. update your class declaration to ... extends access_rule_base { + 2. Add use mod_quiz\local\access_rule_base; + 3. Remove require_once($CFG->dirroot . '/mod/quiz/accessrule/accessrulebase.php'); === 2.8, 2.7.1, 2.6.4 and 2.5.7 === diff --git a/mod/quiz/classes/access_manager.php b/mod/quiz/classes/access_manager.php index ea1c6e7fadc..33c1d62698a 100644 --- a/mod/quiz/classes/access_manager.php +++ b/mod/quiz/classes/access_manager.php @@ -18,6 +18,7 @@ namespace mod_quiz; use core_component; use mod_quiz\form\preflight_check_form; +use mod_quiz\local\access_rule_base; use mod_quiz\question\display_options; use mod_quiz_mod_form; use mod_quiz_renderer; @@ -46,7 +47,7 @@ class access_manager { /** @var int the time to be considered as 'now'. */ protected $timenow; - /** @var \quiz_access_rule_base instances of the active rules for this quiz. */ + /** @var access_rule_base instances of the active rules for this quiz. */ protected $rules = []; /** @@ -71,7 +72,7 @@ class access_manager { * @param int $timenow the time that should be considered as 'now'. * @param bool $canignoretimelimits whether the current user is exempt from * time limits by the mod/quiz:ignoretimelimits capability. - * @return array of {@see quiz_access_rule_base}s. + * @return access_rule_base[] rules that apply to this quiz. */ protected function make_rules(quiz $quizobj, int $timenow, bool $canignoretimelimits): array { diff --git a/mod/quiz/classes/external.php b/mod/quiz/classes/external.php index 289d88b279e..0b23ad3add2 100644 --- a/mod/quiz/classes/external.php +++ b/mod/quiz/classes/external.php @@ -822,7 +822,7 @@ class mod_quiz_external extends external_api { // Check that this attempt belongs to this user. if ($attemptobj->get_userid() != $USER->id) { - throw new moodle_exception('notyourattempt', 'quiz', $quizobj->view_url()); + throw new moodle_exception('notyourattempt', 'quiz', $attemptobj->view_url()); } // General capabilities check. @@ -840,7 +840,7 @@ class mod_quiz_external extends external_api { $messages = $accessmanager->prevent_access(); if (!$ispreviewuser && $messages) { - throw new moodle_exception('attempterror', 'quiz', $quizobj->view_url()); + throw new moodle_exception('attempterror', 'quiz', $attemptobj->view_url()); } } diff --git a/mod/quiz/classes/local/access_rule_base.php b/mod/quiz/classes/local/access_rule_base.php new file mode 100644 index 00000000000..bef681435ee --- /dev/null +++ b/mod/quiz/classes/local/access_rule_base.php @@ -0,0 +1,344 @@ +. + +/** + * Base class for rules that restrict the ability to attempt a quiz. + * + * @package mod_quiz + * @copyright 2011 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_quiz\local; + +use mod_quiz\form\preflight_check_form; +use mod_quiz_mod_form; +use moodle_page; +use MoodleQuickForm; +use quiz; +use stdClass; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/mod/quiz/locallib.php'); + + +/** + * A base class that defines the interface for the various quiz access rules. + * Most of the methods are defined in a slightly unnatural way because we either + * want to say that access is allowed, or explain the reason why it is block. + * Therefore instead of is_access_allowed(...) we have prevent_access(...) that + * return false if access is permitted, or a string explanation (which is treated + * as true) if access should be blocked. Slighly unnatural, but actually the easiest + * way to implement this. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 2.2 + */ +abstract class access_rule_base { + /** @var stdClass the quiz settings. */ + protected $quiz; + /** @var quiz the quiz object. */ + protected $quizobj; + /** @var int the time to use as 'now'. */ + protected $timenow; + + /** + * Create an instance of this rule for a particular quiz. + * @param quiz $quizobj information about the quiz in question. + * @param int $timenow the time that should be considered as 'now'. + */ + public function __construct($quizobj, $timenow) { + $this->quizobj = $quizobj; + $this->quiz = $quizobj->get_quiz(); + $this->timenow = $timenow; + } + + /** + * Return an appropriately configured instance of this rule, if it is applicable + * to the given quiz, otherwise return null. + * @param quiz $quizobj information about the quiz in question. + * @param int $timenow the time that should be considered as 'now'. + * @param bool $canignoretimelimits whether the current user is exempt from + * time limits by the mod/quiz:ignoretimelimits capability. + * @return self|null the rule, if applicable, else null. + */ + public static function make(quiz $quizobj, $timenow, $canignoretimelimits) { + return null; + } + + /** + * Whether or not a user should be allowed to start a new attempt at this quiz now. + * @param int $numattempts the number of previous attempts this user has made. + * @param object $lastattempt information about the user's last completed attempt. + * @return string false if access should be allowed, a message explaining the + * reason if access should be prevented. + */ + public function prevent_new_attempt($numprevattempts, $lastattempt) { + return false; + } + + /** + * Whether the user should be blocked from starting a new attempt or continuing + * an attempt now. + * @return string false if access should be allowed, a message explaining the + * reason if access should be prevented. + */ + public function prevent_access() { + return false; + } + + /** + * @param int|null $attemptid the id of the current attempt, if there is one, + * otherwise null. + * @return bool whether a check is required before the user starts/continues + * their attempt. + */ + public function is_preflight_check_required($attemptid) { + return false; + } + + /** + * Add any field you want to pre-flight check form. You should only do + * something here if {@link is_preflight_check_required()} returned true. + * + * @param preflight_check_form $quizform the form being built. + * @param MoodleQuickForm $mform The wrapped MoodleQuickForm. + * @param int|null $attemptid the id of the current attempt, if there is one, + * otherwise null. + */ + public function add_preflight_check_form_fields(preflight_check_form $quizform, + MoodleQuickForm $mform, $attemptid) { + // Do nothing by default. + } + + /** + * Validate the pre-flight check form submission. You should only do + * something here if {@link is_preflight_check_required()} returned true. + * + * If the form validates, the user will be allowed to continue. + * + * @param array $data the submitted form data. + * @param array $files any files in the submission. + * @param array $errors the list of validation errors that is being built up. + * @param int|null $attemptid the id of the current attempt, if there is one, + * otherwise null. + * @return array the update $errors array; + */ + public function validate_preflight_check($data, $files, $errors, $attemptid) { + return $errors; + } + + /** + * The pre-flight check has passed. This is a chance to record that fact in + * some way. + * @param int|null $attemptid the id of the current attempt, if there is one, + * otherwise null. + */ + public function notify_preflight_check_passed($attemptid) { + // Do nothing by default. + } + + /** + * This is called when the current attempt at the quiz is finished. This is + * used, for example by the password rule, to clear the flag in the session. + */ + public function current_attempt_finished() { + // Do nothing by default. + } + + /** + * Information, such as might be shown on the quiz view page, relating to this restriction. + * There is no obligation to return anything. If it is not appropriate to tell students + * about this rule, then just return ''. + * @return mixed a message, or array of messages, explaining the restriction + * (may be '' if no message is appropriate). + */ + public function description() { + return ''; + } + + /** + * If this rule can determine that this user will never be allowed another attempt at + * this quiz, then return true. This is used so we can know whether to display a + * final grade on the view page. This will only be called if there is not a currently + * active attempt for this user. + * @param int $numattempts the number of previous attempts this user has made. + * @param object $lastattempt information about the user's last completed attempt. + * @return bool true if this rule means that this user will never be allowed another + * attempt at this quiz. + */ + public function is_finished($numprevattempts, $lastattempt) { + return false; + } + + /** + * If, because of this rule, the user has to finish their attempt by a certain time, + * you should override this method to return the attempt end time. + * @param object $attempt the current attempt + * @return mixed the attempt close time, or false if there is no close time. + */ + public function end_time($attempt) { + return false; + } + + /** + * If the user should be shown a different amount of time than $timenow - $this->end_time(), then + * override this method. This is useful if the time remaining is large enough to be omitted. + * @param object $attempt the current attempt + * @param int $timenow the time now. We don't use $this->timenow, so we can + * give the user a more accurate indication of how much time is left. + * @return mixed the time left in seconds (can be negative) or false if there is no limit. + */ + public function time_left_display($attempt, $timenow) { + $endtime = $this->end_time($attempt); + if ($endtime === false) { + return false; + } + return $endtime - $timenow; + } + + /** + * @return boolean whether this rule requires that the attemp (and review) + * pages must be displayed in a pop-up window. + */ + public function attempt_must_be_in_popup() { + return false; + } + + /** + * @return array any options that are required for showing the attempt page + * in a popup window. + */ + public function get_popup_options() { + return array(); + } + + /** + * Sets up the attempt (review or summary) page with any special extra + * properties required by this rule. securewindow rule is an example of where + * this is used. + * + * @param moodle_page $page the page object to initialise. + */ + public function setup_attempt_page($page) { + // Do nothing by default. + } + + /** + * It is possible for one rule to override other rules. + * + * The aim is that third-party rules should be able to replace sandard rules + * if they want. See, for example MDL-13592. + * + * @return array plugin names of other rules that this one replaces. + * For example array('ipaddress', 'password'). + */ + public function get_superceded_rules() { + return array(); + } + + /** + * Add any fields that this rule requires to the quiz settings form. This + * method is called from {@link mod_quiz_mod_form::definition()}, while the + * security seciton is being built. + * @param mod_quiz_mod_form $quizform the quiz settings form that is being built. + * @param MoodleQuickForm $mform the wrapped MoodleQuickForm. + */ + public static function add_settings_form_fields( + mod_quiz_mod_form $quizform, MoodleQuickForm $mform) { + // By default do nothing. + } + + /** + * Validate the data from any form fields added using {@link add_settings_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 mod_quiz_mod_form $quizform the quiz form object. + * @return array $errors the updated $errors array. + */ + public static function validate_settings_form_fields(array $errors, + array $data, $files, mod_quiz_mod_form $quizform) { + + return $errors; + } + + /** + * @return array key => lang string any choices to add to the quiz Browser + * security settings menu. + */ + public static function get_browser_security_choices() { + return array(); + } + + /** + * Save any submitted settings when the quiz settings form is submitted. This + * is called from {@link quiz_after_add_or_update()} in lib.php. + * @param object $quiz the data from the quiz form, including $quiz->id + * which is the id of the quiz being saved. + */ + public static function save_settings($quiz) { + // By default do nothing. + } + + /** + * Delete any rule-specific settings when the quiz is deleted. This is called + * from {@link quiz_delete_instance()} in lib.php. + * @param object $quiz the data from the database, including $quiz->id + * which is the id of the quiz being deleted. + * @since Moodle 2.7.1, 2.6.4, 2.5.7 + */ + public static function delete_settings($quiz) { + // By default do nothing. + } + + /** + * Return the bits of SQL needed to load all the settings from all the access + * plugins in one DB query. The easiest way to understand what you need to do + * here is probably to read the code of {@see access_manager::load_settings()}. + * + * If you have some settings that cannot be loaded in this way, then you can + * use the {@link get_extra_settings()} method instead, but that has + * performance implications. + * + * @param int $quizid the id of the quiz we are loading settings for. This + * can also be accessed as quiz.id in the SQL. (quiz is a table alisas for {quiz}.) + * @return array with three elements: + * 1. fields: any fields to add to the select list. These should be alised + * if neccessary so that the field name starts the name of the plugin. + * 2. joins: any joins (should probably be LEFT JOINS) with other tables that + * are needed. + * 3. params: array of placeholder values that are needed by the SQL. You must + * used named placeholders, and the placeholder names should start with the + * plugin name, to avoid collisions. + */ + public static function get_settings_sql($quizid) { + return array('', '', array()); + } + + /** + * You can use this method to load any extra settings your plugin has that + * cannot be loaded efficiently with get_settings_sql(). + * @param int $quizid the quiz id. + * @return array setting value name => value. The value names should all + * start with the name of your plugin to avoid collisions. + */ + public static function get_extra_settings($quizid) { + return array(); + } +} diff --git a/mod/quiz/db/renamedclasses.php b/mod/quiz/db/renamedclasses.php index d6b3086aa07..98c68d5c571 100644 --- a/mod/quiz/db/renamedclasses.php +++ b/mod/quiz/db/renamedclasses.php @@ -54,4 +54,5 @@ $renamedclasses = [ 'quiz_access_manager' => 'mod_quiz\access_manager', 'mod_quiz_preflight_check_form' => 'mod_quiz\form\preflight_check_form', 'quiz_override_form' => 'mod_quiz\form\edit_override_form', + 'quiz_access_rule_base' => 'mod_quiz\local\access_rule_base', ]; diff --git a/mod/quiz/upgrade.txt b/mod/quiz/upgrade.txt index f80bbfd341a..498b6825019 100644 --- a/mod/quiz/upgrade.txt +++ b/mod/quiz/upgrade.txt @@ -38,6 +38,7 @@ This files describes API changes in the quiz code. - quiz_access_manager => mod_quiz\access_manager - mod_quiz_preflight_check_form => mod_quiz\form\preflight_check_form - quiz_override_form => mod_quiz\form\edit_override_form + - quiz_access_rule_base => mod_quiz\local\access_rule_base * The following classes have been deprecated: - mod_quiz_overdue_attempt_updater - merged into mod_quiz\task\update_overdue_attempts @@ -54,6 +55,7 @@ This files describes API changes in the quiz code. - mod/quiz/accessmanager_form.php - mod/quiz/cronlib.php - mod/quiz/override_form.php + - mod/quiz/accessrule/accessrulebase.php === 4.1 === From 8af8ef3b9f8caed66b2488a65e8970fea09bf0bc Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Mon, 19 Dec 2022 13:21:41 +0000 Subject: [PATCH 09/30] MDL-76614 quiz: clean up access_rule_base class --- mod/quiz/classes/local/access_rule_base.php | 82 ++++++++++++--------- 1 file changed, 46 insertions(+), 36 deletions(-) diff --git a/mod/quiz/classes/local/access_rule_base.php b/mod/quiz/classes/local/access_rule_base.php index bef681435ee..62e7d15bbb0 100644 --- a/mod/quiz/classes/local/access_rule_base.php +++ b/mod/quiz/classes/local/access_rule_base.php @@ -14,14 +14,6 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Base class for rules that restrict the ability to attempt a quiz. - * - * @package mod_quiz - * @copyright 2011 The Open University - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - namespace mod_quiz\local; use mod_quiz\form\preflight_check_form; @@ -37,7 +29,9 @@ require_once($CFG->dirroot . '/mod/quiz/locallib.php'); /** - * A base class that defines the interface for the various quiz access rules. + * Base class for rules that restrict the ability to attempt a quiz. + * + * Quiz access rule plugins must sublclass this one to form their main 'rule' class. * Most of the methods are defined in a slightly unnatural way because we either * want to say that access is allowed, or explain the reason why it is block. * Therefore instead of is_access_allowed(...) we have prevent_access(...) that @@ -45,6 +39,7 @@ require_once($CFG->dirroot . '/mod/quiz/locallib.php'); * as true) if access should be blocked. Slighly unnatural, but actually the easiest * way to implement this. * + * @package mod_quiz * @copyright 2009 Tim Hunt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 2.2 @@ -82,8 +77,9 @@ abstract class access_rule_base { } /** - * Whether or not a user should be allowed to start a new attempt at this quiz now. - * @param int $numattempts the number of previous attempts this user has made. + * Whether a user should be allowed to start a new attempt at this quiz now. + * + * @param int $numprevattempts the number of previous attempts this user has made. * @param object $lastattempt information about the user's last completed attempt. * @return string false if access should be allowed, a message explaining the * reason if access should be prevented. @@ -103,6 +99,8 @@ abstract class access_rule_base { } /** + * Does this rule require a UI check with the user before an attempt is started? + * * @param int|null $attemptid the id of the current attempt, if there is one, * otherwise null. * @return bool whether a check is required before the user starts/continues @@ -114,7 +112,7 @@ abstract class access_rule_base { /** * Add any field you want to pre-flight check form. You should only do - * something here if {@link is_preflight_check_required()} returned true. + * something here if {@see is_preflight_check_required()} returned true. * * @param preflight_check_form $quizform the form being built. * @param MoodleQuickForm $mform The wrapped MoodleQuickForm. @@ -128,7 +126,7 @@ abstract class access_rule_base { /** * Validate the pre-flight check form submission. You should only do - * something here if {@link is_preflight_check_required()} returned true. + * something here if {@see is_preflight_check_required()} returned true. * * If the form validates, the user will be allowed to continue. * @@ -162,10 +160,13 @@ abstract class access_rule_base { } /** - * Information, such as might be shown on the quiz view page, relating to this restriction. - * There is no obligation to return anything. If it is not appropriate to tell students - * about this rule, then just return ''. - * @return mixed a message, or array of messages, explaining the restriction + * Return a brief summary of this rule, to show to users, if required. + * + * This information is show shown, for example, on the quiz view page, to explain this + * restriction. There is no obligation to return anything. If it is not appropriate to + * tell students about this rule, then just return ''. + * + * @return string a message, or array of messages, explaining the restriction * (may be '' if no message is appropriate). */ public function description() { @@ -173,11 +174,15 @@ abstract class access_rule_base { } /** + * Is the current user unable to start any more attempts in future, because of this rule? + * * If this rule can determine that this user will never be allowed another attempt at - * this quiz, then return true. This is used so we can know whether to display a + * this quiz, for example because the last possible start time is past, or all attempts + * have been used up, then return true. This is used to know whether to display a * final grade on the view page. This will only be called if there is not a currently * active attempt for this user. - * @param int $numattempts the number of previous attempts this user has made. + * + * @param int $numprevattempts the number of previous attempts this user has made. * @param object $lastattempt information about the user's last completed attempt. * @return bool true if this rule means that this user will never be allowed another * attempt at this quiz. @@ -187,10 +192,10 @@ abstract class access_rule_base { } /** - * If, because of this rule, the user has to finish their attempt by a certain time, - * you should override this method to return the attempt end time. - * @param object $attempt the current attempt - * @return mixed the attempt close time, or false if there is no close time. + * Time by which, according to this rule, the user has to finish their attempt. + * + * @param stdClass $attempt the current attempt + * @return int|false the attempt close time, or false if there is no close time. */ public function end_time($attempt) { return false; @@ -213,19 +218,22 @@ abstract class access_rule_base { } /** - * @return boolean whether this rule requires that the attemp (and review) - * pages must be displayed in a pop-up window. + * Does this rule requires the attempt (and review) to be displayed in a pop-up window? + * + * @return bool true if it does. */ public function attempt_must_be_in_popup() { return false; } /** + * Any options required when showing the attempt in a pop-up. + * * @return array any options that are required for showing the attempt page * in a popup window. */ public function get_popup_options() { - return array(); + return []; } /** @@ -246,15 +254,15 @@ abstract class access_rule_base { * if they want. See, for example MDL-13592. * * @return array plugin names of other rules that this one replaces. - * For example array('ipaddress', 'password'). + * For example ['ipaddress', 'password']. */ public function get_superceded_rules() { - return array(); + return []; } /** * Add any fields that this rule requires to the quiz settings form. This - * method is called from {@link mod_quiz_mod_form::definition()}, while the + * method is called from {@see mod_quiz_mod_form::definition()}, while the * security seciton is being built. * @param mod_quiz_mod_form $quizform the quiz settings form that is being built. * @param MoodleQuickForm $mform the wrapped MoodleQuickForm. @@ -265,7 +273,7 @@ abstract class access_rule_base { } /** - * Validate the data from any form fields added using {@link add_settings_form_fields()}. + * Validate the data from any form fields added using {@see add_settings_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. @@ -279,16 +287,18 @@ abstract class access_rule_base { } /** + * Get any options this rule adds to the 'Browser security' quiz setting. + * * @return array key => lang string any choices to add to the quiz Browser * security settings menu. */ public static function get_browser_security_choices() { - return array(); + return []; } /** * Save any submitted settings when the quiz settings form is submitted. This - * is called from {@link quiz_after_add_or_update()} in lib.php. + * is called from {@see quiz_after_add_or_update()} in lib.php. * @param object $quiz the data from the quiz form, including $quiz->id * which is the id of the quiz being saved. */ @@ -298,7 +308,7 @@ abstract class access_rule_base { /** * Delete any rule-specific settings when the quiz is deleted. This is called - * from {@link quiz_delete_instance()} in lib.php. + * from {@see quiz_delete_instance()} in lib.php. * @param object $quiz the data from the database, including $quiz->id * which is the id of the quiz being deleted. * @since Moodle 2.7.1, 2.6.4, 2.5.7 @@ -313,7 +323,7 @@ abstract class access_rule_base { * here is probably to read the code of {@see access_manager::load_settings()}. * * If you have some settings that cannot be loaded in this way, then you can - * use the {@link get_extra_settings()} method instead, but that has + * use the {@see get_extra_settings()} method instead, but that has * performance implications. * * @param int $quizid the id of the quiz we are loading settings for. This @@ -328,7 +338,7 @@ abstract class access_rule_base { * plugin name, to avoid collisions. */ public static function get_settings_sql($quizid) { - return array('', '', array()); + return ['', '', []]; } /** @@ -339,6 +349,6 @@ abstract class access_rule_base { * start with the name of your plugin to avoid collisions. */ public static function get_extra_settings($quizid) { - return array(); + return []; } } From dd41b972c02384e4a6c90f22e811aecf25e81b27 Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Fri, 16 Dec 2022 23:57:08 +0000 Subject: [PATCH 10/30] MDL-76614 quiz: clean up edit_override_form class --- mod/quiz/classes/form/edit_override_form.php | 38 +++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/mod/quiz/classes/form/edit_override_form.php b/mod/quiz/classes/form/edit_override_form.php index 7cbfcc76293..b830ce73a47 100644 --- a/mod/quiz/classes/form/edit_override_form.php +++ b/mod/quiz/classes/form/edit_override_form.php @@ -18,6 +18,7 @@ namespace mod_quiz\form; use cm_info; use context; +use context_module; use mod_quiz_mod_form; use moodle_url; use moodleform; @@ -43,7 +44,7 @@ class edit_override_form extends moodleform { /** @var stdClass the quiz settings object. */ protected $quiz; - /** @var context the quiz context. */ + /** @var context_module the quiz context. */ protected $context; /** @var bool editing group override (true) or user override (false). */ @@ -57,14 +58,17 @@ class edit_override_form extends moodleform { /** * Constructor. + * * @param moodle_url $submiturl the form action URL. - * @param object course module object. - * @param object the quiz settings object. - * @param context the quiz context. - * @param bool editing group override (true) or user override (false). - * @param object $override the override being edited, if it already exists. + * @param cm_info $cm course module object. + * @param stdClass $quiz the quiz settings object. + * @param context_module $context the quiz context. + * @param bool $groupmode editing group override (true) or user override (false). + * @param stdClass|null $override the override being edited, if it already exists. */ - public function __construct($submiturl, $cm, $quiz, $context, $groupmode, $override) { + public function __construct(moodle_url $submiturl, + cm_info $cm, stdClass $quiz, context_module $context, + bool $groupmode, ?stdClass $override) { $this->cm = $cm; $this->quiz = $quiz; @@ -91,7 +95,7 @@ class edit_override_form extends moodleform { // Group override. if ($this->groupid) { // There is already a groupid, so freeze the selector. - $groupchoices = array(); + $groupchoices = []; $groupchoices[$this->groupid] = groups_get_group_name($this->groupid); $mform->addElement('select', 'groupid', get_string('overridegroup', 'quiz'), $groupchoices); @@ -102,11 +106,11 @@ class edit_override_form extends moodleform { $groups = $accessallgroups ? groups_get_all_groups($cm->course) : groups_get_activity_allowed_groups($cm); if (empty($groups)) { // Generate an error. - $link = new moodle_url('/mod/quiz/overrides.php', array('cmid'=>$cm->id)); + $link = new moodle_url('/mod/quiz/overrides.php', ['cmid' => $cm->id]); throw new \moodle_exception('groupsnone', 'quiz', $link); } - $groupchoices = array(); + $groupchoices = []; foreach ($groups as $group) { $groupchoices[$group->id] = $group->name; } @@ -128,7 +132,7 @@ class edit_override_form extends moodleform { // There is already a userid, so freeze the selector. $user = $DB->get_record('user', ['id' => $this->userid]); profile_load_custom_fields($user); - $userchoices = array(); + $userchoices = []; $userchoices[$this->userid] = self::display_user_name($user, $extrauserfields); $mform->addElement('select', 'userid', get_string('overrideuser', 'quiz'), $userchoices); @@ -164,7 +168,7 @@ class edit_override_form extends moodleform { if (empty($users)) { // Generate an error. - $link = new moodle_url('/mod/quiz/overrides.php', array('cmid'=>$cm->id)); + $link = new moodle_url('/mod/quiz/overrides.php', ['cmid' => $cm->id]); throw new \moodle_exception('usersnone', 'quiz', $link); } @@ -199,12 +203,12 @@ class edit_override_form extends moodleform { // Time limit. $mform->addElement('duration', 'timelimit', - get_string('timelimit', 'quiz'), array('optional' => true)); + get_string('timelimit', 'quiz'), ['optional' => true]); $mform->addHelpButton('timelimit', 'timelimit', 'quiz'); $mform->setDefault('timelimit', $this->quiz->timelimit); // Number of attempts. - $attemptoptions = array('0' => get_string('unlimited')); + $attemptoptions = ['0' => get_string('unlimited')]; for ($i = 1; $i <= QUIZ_MAX_ATTEMPT_OPTION; $i++) { $attemptoptions[$i] = $i; } @@ -217,14 +221,14 @@ class edit_override_form extends moodleform { $mform->addElement('submit', 'resetbutton', get_string('reverttodefaults', 'quiz')); - $buttonarray = array(); + $buttonarray = []; $buttonarray[] = $mform->createElement('submit', 'submitbutton', get_string('save', 'quiz')); $buttonarray[] = $mform->createElement('submit', 'againbutton', get_string('saveoverrideandstay', 'quiz')); $buttonarray[] = $mform->createElement('cancel'); - $mform->addGroup($buttonarray, 'buttonbar', '', array(' '), false); + $mform->addGroup($buttonarray, 'buttonbar', '', [' '], false); $mform->closeHeaderBefore('buttonbar'); } @@ -281,7 +285,7 @@ class edit_override_form extends moodleform { // Ensure that at least one quiz setting was changed. $changed = false; - $keys = array('timeopen', 'timeclose', 'timelimit', 'attempts', 'password'); + $keys = ['timeopen', 'timeclose', 'timelimit', 'attempts', 'password']; foreach ($keys as $key) { if ($data[$key] != $quiz->{$key}) { $changed = true; From 581a3bc7e98d0cf57488bfccb47a7b5b6f150245 Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Mon, 19 Dec 2022 11:55:10 +0000 Subject: [PATCH 11/30] MDL-76614 quiz: quiz_add_random_form => mod_quiz\form\add_random_form --- mod/quiz/addrandom.php | 4 +- mod/quiz/addrandomform.php | 131 +------------------ mod/quiz/classes/form/add_random_form.php | 145 ++++++++++++++++++++++ mod/quiz/db/renamedclasses.php | 1 + mod/quiz/edit.php | 1 - mod/quiz/lib.php | 4 +- mod/quiz/upgrade.txt | 1 + 7 files changed, 155 insertions(+), 132 deletions(-) create mode 100644 mod/quiz/classes/form/add_random_form.php diff --git a/mod/quiz/addrandom.php b/mod/quiz/addrandom.php index 3839ca47c06..8d57c0d6149 100644 --- a/mod/quiz/addrandom.php +++ b/mod/quiz/addrandom.php @@ -26,9 +26,9 @@ require_once(__DIR__ . '/../../config.php'); require_once($CFG->dirroot . '/mod/quiz/locallib.php'); -require_once($CFG->dirroot . '/mod/quiz/addrandomform.php'); require_once($CFG->dirroot . '/question/editlib.php'); +use mod_quiz\form\add_random_form; use qbank_managecategories\question_category_object; list($thispageurl, $contexts, $cmid, $cm, $quiz, $pagevars) = @@ -75,7 +75,7 @@ $qcobject = new question_category_object( null, $contexts->having_cap('moodle/question:add')); -$mform = new quiz_add_random_form(new moodle_url('/mod/quiz/addrandom.php'), +$mform = new add_random_form(new moodle_url('/mod/quiz/addrandom.php'), array('contexts' => $contexts, 'cat' => $pagevars['cat'])); if ($mform->is_cancelled()) { diff --git a/mod/quiz/addrandomform.php b/mod/quiz/addrandomform.php index d16121fc017..df1c42fe9f1 100644 --- a/mod/quiz/addrandomform.php +++ b/mod/quiz/addrandomform.php @@ -15,134 +15,11 @@ // along with Moodle. If not, see . /** - * Defines the Moodle forum used to add random questions to the quiz. + * File only retained to prevent fatal errors in code that tries to require/include this. * - * @package mod_quiz - * @copyright 2008 Olli Savolainen - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @todo MDL-76612 delete this file as part of Moodle 4.6 development. + * @deprecated This file is no longer required in Moodle 4.2+. */ - - defined('MOODLE_INTERNAL') || die(); -require_once($CFG->libdir.'/formslib.php'); - - -/** - * The add random questions form. - * - * @copyright 1999 onwards Martin Dougiamas and others {@link http://moodle.com} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class quiz_add_random_form extends moodleform { - - protected function definition() { - global $OUTPUT, $PAGE, $CFG; - - $mform = $this->_form; - $mform->setDisableShortforms(); - - $contexts = $this->_customdata['contexts']; - $usablecontexts = $contexts->having_cap('moodle/question:useall'); - - // Random from existing category section. - $mform->addElement('header', 'existingcategoryheader', - get_string('randomfromexistingcategory', 'quiz')); - - $mform->addElement('questioncategory', 'category', get_string('category'), - array('contexts' => $usablecontexts, 'top' => true)); - $mform->setDefault('category', $this->_customdata['cat']); - - $mform->addElement('checkbox', 'includesubcategories', '', get_string('recurse', 'quiz')); - - $tops = question_get_top_categories_for_contexts(array_column($contexts->all(), 'id')); - $mform->hideIf('includesubcategories', 'category', 'in', $tops); - - if ($CFG->usetags) { - $tagstrings = array(); - $tags = core_tag_tag::get_tags_by_area_in_contexts('core_question', 'question', $usablecontexts); - foreach ($tags as $tag) { - $tagstrings["{$tag->id},{$tag->name}"] = $tag->name; - } - $options = array( - 'multiple' => true, - 'noselectionstring' => get_string('anytags', 'quiz'), - ); - $mform->addElement('autocomplete', 'fromtags', get_string('randomquestiontags', 'mod_quiz'), $tagstrings, $options); - $mform->addHelpButton('fromtags', 'randomquestiontags', 'mod_quiz'); - } - - // TODO: in the past, the drop-down used to only show sensible choices for - // number of questions to add. That is, if the currently selected filter - // only matched 9 questions (not already in the quiz), then the drop-down would - // only offer choices 1..9. This nice UI hint got lost when the UI became Ajax-y. - // We should add it back. - $mform->addElement('select', 'numbertoadd', get_string('randomnumber', 'quiz'), - $this->get_number_of_questions_to_add_choices()); - - $previewhtml = $OUTPUT->render_from_template('mod_quiz/random_question_form_preview', []); - $mform->addElement('html', $previewhtml); - - $mform->addElement('submit', 'existingcategory', get_string('addrandomquestion', 'quiz')); - - // If the manage categories plugins is enabled, add the elements to create a new category in the form. - if (\core\plugininfo\qbank::is_plugin_enabled(\qbank_managecategories\helper::PLUGINNAME)) { - // Random from a new category section. - $mform->addElement('header', 'newcategoryheader', - get_string('randomquestionusinganewcategory', 'quiz')); - - $mform->addElement('text', 'name', get_string('name'), 'maxlength="254" size="50"'); - $mform->setType('name', PARAM_TEXT); - - $mform->addElement('questioncategory', 'parent', get_string('parentcategory', 'question'), - array('contexts' => $usablecontexts, 'top' => true)); - $mform->addHelpButton('parent', 'parentcategory', 'question'); - - $mform->addElement('submit', 'newcategory', - get_string('createcategoryandaddrandomquestion', 'quiz')); - } - - // Cancel button. - $mform->addElement('cancel'); - $mform->closeHeaderBefore('cancel'); - - $mform->addElement('hidden', 'addonpage', 0, 'id="rform_qpage"'); - $mform->setType('addonpage', PARAM_SEQUENCE); - $mform->addElement('hidden', 'cmid', 0); - $mform->setType('cmid', PARAM_INT); - $mform->addElement('hidden', 'returnurl', 0); - $mform->setType('returnurl', PARAM_LOCALURL); - - // Add the javascript required to enhance this mform. - $PAGE->requires->js_call_amd('mod_quiz/add_random_form', 'init', [ - $mform->getAttribute('id'), - $contexts->lowest()->id, - $tops, - $CFG->usetags - ]); - } - - public function validation($fromform, $files) { - $errors = parent::validation($fromform, $files); - - if (!empty($fromform['newcategory']) && trim($fromform['name']) == '') { - $errors['name'] = get_string('categorynamecantbeblank', 'question'); - } - - return $errors; - } - - /** - * Return an arbitrary array for the dropdown menu - * - * @param int $maxrand - * @return array of integers [1, 2, ..., 100] (or to the smaller of $maxrand and 100.) - */ - private function get_number_of_questions_to_add_choices($maxrand = 100) { - $randomcount = array(); - for ($i = 1; $i <= min(100, $maxrand); $i++) { - $randomcount[$i] = $i; - } - return $randomcount; - } -} +debugging('This file is no longer required in Moodle 4.2+. Please do not include/require it.', DEBUG_DEVELOPER); diff --git a/mod/quiz/classes/form/add_random_form.php b/mod/quiz/classes/form/add_random_form.php new file mode 100644 index 00000000000..2498abb0e2e --- /dev/null +++ b/mod/quiz/classes/form/add_random_form.php @@ -0,0 +1,145 @@ +. + +namespace mod_quiz\form; + +use core_tag_tag; +use moodleform; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir.'/formslib.php'); + + +/** + * The add random questions form. + * + * @package mod_quiz + * @copyright 1999 onwards Martin Dougiamas and others {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class add_random_form extends moodleform { + + protected function definition() { + global $OUTPUT, $PAGE, $CFG; + + $mform = $this->_form; + $mform->setDisableShortforms(); + + $contexts = $this->_customdata['contexts']; + $usablecontexts = $contexts->having_cap('moodle/question:useall'); + + // Random from existing category section. + $mform->addElement('header', 'existingcategoryheader', + get_string('randomfromexistingcategory', 'quiz')); + + $mform->addElement('questioncategory', 'category', get_string('category'), + array('contexts' => $usablecontexts, 'top' => true)); + $mform->setDefault('category', $this->_customdata['cat']); + + $mform->addElement('checkbox', 'includesubcategories', '', get_string('recurse', 'quiz')); + + $tops = question_get_top_categories_for_contexts(array_column($contexts->all(), 'id')); + $mform->hideIf('includesubcategories', 'category', 'in', $tops); + + if ($CFG->usetags) { + $tagstrings = array(); + $tags = core_tag_tag::get_tags_by_area_in_contexts('core_question', 'question', $usablecontexts); + foreach ($tags as $tag) { + $tagstrings["{$tag->id},{$tag->name}"] = $tag->name; + } + $options = array( + 'multiple' => true, + 'noselectionstring' => get_string('anytags', 'quiz'), + ); + $mform->addElement('autocomplete', 'fromtags', get_string('randomquestiontags', 'mod_quiz'), $tagstrings, $options); + $mform->addHelpButton('fromtags', 'randomquestiontags', 'mod_quiz'); + } + + // TODO: in the past, the drop-down used to only show sensible choices for + // number of questions to add. That is, if the currently selected filter + // only matched 9 questions (not already in the quiz), then the drop-down would + // only offer choices 1..9. This nice UI hint got lost when the UI became Ajax-y. + // We should add it back. + $mform->addElement('select', 'numbertoadd', get_string('randomnumber', 'quiz'), + $this->get_number_of_questions_to_add_choices()); + + $previewhtml = $OUTPUT->render_from_template('mod_quiz/random_question_form_preview', []); + $mform->addElement('html', $previewhtml); + + $mform->addElement('submit', 'existingcategory', get_string('addrandomquestion', 'quiz')); + + // If the manage categories plugins is enabled, add the elements to create a new category in the form. + if (\core\plugininfo\qbank::is_plugin_enabled(\qbank_managecategories\helper::PLUGINNAME)) { + // Random from a new category section. + $mform->addElement('header', 'newcategoryheader', + get_string('randomquestionusinganewcategory', 'quiz')); + + $mform->addElement('text', 'name', get_string('name'), 'maxlength="254" size="50"'); + $mform->setType('name', PARAM_TEXT); + + $mform->addElement('questioncategory', 'parent', get_string('parentcategory', 'question'), + array('contexts' => $usablecontexts, 'top' => true)); + $mform->addHelpButton('parent', 'parentcategory', 'question'); + + $mform->addElement('submit', 'newcategory', + get_string('createcategoryandaddrandomquestion', 'quiz')); + } + + // Cancel button. + $mform->addElement('cancel'); + $mform->closeHeaderBefore('cancel'); + + $mform->addElement('hidden', 'addonpage', 0, 'id="rform_qpage"'); + $mform->setType('addonpage', PARAM_SEQUENCE); + $mform->addElement('hidden', 'cmid', 0); + $mform->setType('cmid', PARAM_INT); + $mform->addElement('hidden', 'returnurl', 0); + $mform->setType('returnurl', PARAM_LOCALURL); + + // Add the javascript required to enhance this mform. + $PAGE->requires->js_call_amd('mod_quiz/add_random_form', 'init', [ + $mform->getAttribute('id'), + $contexts->lowest()->id, + $tops, + $CFG->usetags + ]); + } + + public function validation($fromform, $files) { + $errors = parent::validation($fromform, $files); + + if (!empty($fromform['newcategory']) && trim($fromform['name']) == '') { + $errors['name'] = get_string('categorynamecantbeblank', 'question'); + } + + return $errors; + } + + /** + * Return an arbitrary array for the dropdown menu + * + * @param int $maxrand + * @return array of integers [1, 2, ..., 100] (or to the smaller of $maxrand and 100.) + */ + private function get_number_of_questions_to_add_choices($maxrand = 100) { + $randomcount = array(); + for ($i = 1; $i <= min(100, $maxrand); $i++) { + $randomcount[$i] = $i; + } + return $randomcount; + } +} diff --git a/mod/quiz/db/renamedclasses.php b/mod/quiz/db/renamedclasses.php index 98c68d5c571..5752280611c 100644 --- a/mod/quiz/db/renamedclasses.php +++ b/mod/quiz/db/renamedclasses.php @@ -55,4 +55,5 @@ $renamedclasses = [ 'mod_quiz_preflight_check_form' => 'mod_quiz\form\preflight_check_form', 'quiz_override_form' => 'mod_quiz\form\edit_override_form', 'quiz_access_rule_base' => 'mod_quiz\local\access_rule_base', + 'quiz_add_random_form' => 'mod_quiz\form\add_random_form', ]; diff --git a/mod/quiz/edit.php b/mod/quiz/edit.php index b25d7ebfb0f..93c93eabb4d 100644 --- a/mod/quiz/edit.php +++ b/mod/quiz/edit.php @@ -43,7 +43,6 @@ require_once(__DIR__ . '/../../config.php'); require_once($CFG->dirroot . '/mod/quiz/locallib.php'); -require_once($CFG->dirroot . '/mod/quiz/addrandomform.php'); require_once($CFG->dirroot . '/question/editlib.php'); // These params are only passed from page request to request while we stay on diff --git a/mod/quiz/lib.php b/mod/quiz/lib.php index 7f0bf9daa1b..a4c5b2f5494 100644 --- a/mod/quiz/lib.php +++ b/mod/quiz/lib.php @@ -29,6 +29,7 @@ defined('MOODLE_INTERNAL') || die(); use mod_quiz\access_manager; +use mod_quiz\form\add_random_form; use mod_quiz\question\bank\custom_view; use mod_quiz\question\display_options; use mod_quiz\question\qubaids_for_quiz; @@ -2460,7 +2461,6 @@ function mod_quiz_output_fragment_quiz_question_bank($args) { */ function mod_quiz_output_fragment_add_random_question_form($args) { global $CFG; - require_once($CFG->dirroot . '/mod/quiz/addrandomform.php'); $contexts = new \core_question\local\bank\question_edit_contexts($args['context']); $formoptions = [ @@ -2474,7 +2474,7 @@ function mod_quiz_output_fragment_add_random_question_form($args) { 'cmid' => $args['cmid'] ]; - $form = new quiz_add_random_form( + $form = new add_random_form( new \moodle_url('/mod/quiz/addrandom.php'), $formoptions, 'post', diff --git a/mod/quiz/upgrade.txt b/mod/quiz/upgrade.txt index 498b6825019..b78063e9c71 100644 --- a/mod/quiz/upgrade.txt +++ b/mod/quiz/upgrade.txt @@ -39,6 +39,7 @@ This files describes API changes in the quiz code. - mod_quiz_preflight_check_form => mod_quiz\form\preflight_check_form - quiz_override_form => mod_quiz\form\edit_override_form - quiz_access_rule_base => mod_quiz\local\access_rule_base + - quiz_add_random_form => mod_quiz\form\add_random_form * The following classes have been deprecated: - mod_quiz_overdue_attempt_updater - merged into mod_quiz\task\update_overdue_attempts From aceae3a4fa8c6380f9ada6a65e9560d3dda55e7f Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Mon, 19 Dec 2022 12:29:56 +0000 Subject: [PATCH 12/30] MDL-76614 quiz: move mod_quiz_links_to_other_attempts => classes\output --- mod/quiz/attemptlib.php | 15 ++++---- .../output/links_to_other_attempts.php | 38 +++++++++++++++++++ mod/quiz/db/renamedclasses.php | 1 + mod/quiz/renderer.php | 24 +++++++----- mod/quiz/upgrade.txt | 1 + 5 files changed, 63 insertions(+), 16 deletions(-) create mode 100644 mod/quiz/classes/output/links_to_other_attempts.php diff --git a/mod/quiz/attemptlib.php b/mod/quiz/attemptlib.php index fd55f4c7cea..5813053aea6 100644 --- a/mod/quiz/attemptlib.php +++ b/mod/quiz/attemptlib.php @@ -28,6 +28,7 @@ defined('MOODLE_INTERNAL') || die(); use mod_quiz\access_manager; +use mod_quiz\output\links_to_other_attempts; use mod_quiz\question\bank\qbank_helper; use mod_quiz\question\display_options; @@ -1889,14 +1890,14 @@ class quiz_attempt { * * The $url passed in must contain an attempt parameter. * - * The {@link mod_quiz_links_to_other_attempts} object returned contains an + * The {@see links_to_other_attempts} object returned contains an * array with keys that are the attempt number, 1, 2, 3. * The array values are either a {@link moodle_url} with the attempt parameter * updated to point to the attempt id of the other attempt, or null corresponding * to the current attempt number. * * @param moodle_url $url a URL. - * @return mod_quiz_links_to_other_attempts|bool containing array int => null|moodle_url. + * @return links_to_other_attempts|bool containing array int => null|moodle_url. * False if none. */ public function links_to_other_attempts(moodle_url $url) { @@ -1905,7 +1906,7 @@ class quiz_attempt { return false; } - $links = new mod_quiz_links_to_other_attempts(); + $links = new links_to_other_attempts(); foreach ($attempts as $at) { if ($at->id == $this->attempt->id) { $links->links[$at->attempt] = null; @@ -1921,15 +1922,15 @@ class quiz_attempt { * * The $url passed in must contain a slot parameter. * - * The {@link mod_quiz_links_to_other_attempts} object returned contains an + * The {@see links_to_other_attempts} object returned contains an * array with keys that are the redo number, 1, 2, 3. - * The array values are either a {@link moodle_url} with the slot parameter + * The array values are either a {@see moodle_url} with the slot parameter * updated to point to the slot that has that redo of this question; or null * corresponding to the redo identified by $slot. * * @param int $slot identifies a question in this attempt. * @param moodle_url $baseurl the base URL to modify to generate each link. - * @return mod_quiz_links_to_other_attempts|null containing array int => null|moodle_url, + * @return links_to_other_attempts|null containing array int => null|moodle_url, * or null if the question in this slot has not been redone. */ public function links_to_other_redos($slot, moodle_url $baseurl) { @@ -1940,7 +1941,7 @@ class quiz_attempt { return null; } - $links = new mod_quiz_links_to_other_attempts(); + $links = new links_to_other_attempts(); $index = 1; foreach ($qas as $qa) { if ($qa->get_slot() == $slot) { diff --git a/mod/quiz/classes/output/links_to_other_attempts.php b/mod/quiz/classes/output/links_to_other_attempts.php new file mode 100644 index 00000000000..7a94410281d --- /dev/null +++ b/mod/quiz/classes/output/links_to_other_attempts.php @@ -0,0 +1,38 @@ +. + +namespace mod_quiz\output; + +use renderable; + +/** + * Represents the list of links to other attempts + * + * @package mod_quiz + * @category output + * @copyright 2011 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class links_to_other_attempts implements renderable { + /** + * @var array The list of links. string attempt number => one of three things: + * - null if this is the current attempt, and so should not be linked. (Just the number is output.) + * - moodle_url if this is a different attempt. (Output as a link to the URL with the number as link text.) + * - a renderable, in which case the results of rendering the renderable is output. + * (This is used by {@see quiz_attempt::links_to_other_redos()}.) + */ + public $links = []; +} diff --git a/mod/quiz/db/renamedclasses.php b/mod/quiz/db/renamedclasses.php index 5752280611c..d3dc759dc58 100644 --- a/mod/quiz/db/renamedclasses.php +++ b/mod/quiz/db/renamedclasses.php @@ -56,4 +56,5 @@ $renamedclasses = [ 'quiz_override_form' => 'mod_quiz\form\edit_override_form', 'quiz_access_rule_base' => 'mod_quiz\local\access_rule_base', 'quiz_add_random_form' => 'mod_quiz\form\add_random_form', + 'mod_quiz_links_to_other_attempts' => 'mod_quiz\output\links_to_other_attempts', ]; diff --git a/mod/quiz/renderer.php b/mod/quiz/renderer.php index be7f147b816..8e108fc7d94 100644 --- a/mod/quiz/renderer.php +++ b/mod/quiz/renderer.php @@ -27,6 +27,7 @@ defined('MOODLE_INTERNAL') || die(); use mod_quiz\access_manager; use mod_quiz\form\preflight_check_form; +use mod_quiz\output\links_to_other_attempts; use mod_quiz\question\display_options; @@ -410,12 +411,13 @@ class mod_quiz_renderer extends plugin_renderer_base { } /** - * outputs the link the other attempts. + * Renders a list of links the other attempts. * - * @param mod_quiz_links_to_other_attempts $links + * @param links_to_other_attempts $links + * @return string HTML fragment. */ - protected function render_mod_quiz_links_to_other_attempts( - mod_quiz_links_to_other_attempts $links) { + protected function render_links_to_other_attempts( + links_to_other_attempts $links) { $attemptlinks = array(); foreach ($links->links as $attempt => $url) { if (!$url) { @@ -1416,15 +1418,19 @@ class mod_quiz_renderer extends plugin_renderer_base { array('id' => 'connection-error', 'style' => 'display: none;', 'role' => 'alert')) . html_writer::tag('div', $ok, array('id' => 'connection-ok', 'style' => 'display: none;', 'role' => 'alert')); } -} -class mod_quiz_links_to_other_attempts implements renderable { /** - * @var array string attempt number => url, or null for the current attempt. - * url may be either a moodle_url, or a renderable. + * Deprecated version of render_links_to_other_attempts. + * + * @param links_to_other_attempts $links + * @return string HTML fragment. + * @deprecated since Moodle 4.2. Please use render_links_to_other_attempts instead. + * @todo MDL-76612 Final deprecation in Moodle 4.6 */ - public $links = array(); + protected function render_mod_quiz_links_to_other_attempts(links_to_other_attempts $links) { + return $this->render_links_to_other_attempts($links); + } } diff --git a/mod/quiz/upgrade.txt b/mod/quiz/upgrade.txt index b78063e9c71..409bf2717dc 100644 --- a/mod/quiz/upgrade.txt +++ b/mod/quiz/upgrade.txt @@ -40,6 +40,7 @@ This files describes API changes in the quiz code. - quiz_override_form => mod_quiz\form\edit_override_form - quiz_access_rule_base => mod_quiz\local\access_rule_base - quiz_add_random_form => mod_quiz\form\add_random_form + - mod_quiz_links_to_other_attempts => mod_quiz\output\links_to_other_attempts * The following classes have been deprecated: - mod_quiz_overdue_attempt_updater - merged into mod_quiz\task\update_overdue_attempts From abeeaef10a2c79fe49c166666bae1c5bbd422048 Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Mon, 19 Dec 2022 12:54:32 +0000 Subject: [PATCH 13/30] MDL-76614 quiz: mod_quiz_view_object => mod_quiz\output\view_page --- mod/quiz/classes/output/view_page.php | 109 ++++++++++++++++++++++++++ mod/quiz/db/renamedclasses.php | 1 + mod/quiz/renderer.php | 96 +++-------------------- mod/quiz/upgrade.txt | 1 + mod/quiz/view.php | 3 +- 5 files changed, 122 insertions(+), 88 deletions(-) create mode 100644 mod/quiz/classes/output/view_page.php diff --git a/mod/quiz/classes/output/view_page.php b/mod/quiz/classes/output/view_page.php new file mode 100644 index 00000000000..b7e8b85a206 --- /dev/null +++ b/mod/quiz/classes/output/view_page.php @@ -0,0 +1,109 @@ +. + +namespace mod_quiz\output; + +use mod_quiz\access_manager; +use mod_quiz\form\preflight_check_form; +use moodle_url; + +/** + * This class captures all the various information to render the front page of the quiz activity. + * + * This class is not currently renderable or templatable, but it very nearly could be, + * which is why it is in the output namespace. It is used to send data to the renderer. + * + * @package mod_quiz + * @category output + * @copyright 2011 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class view_page { + /** @var array $infomessages of messages with information to display about the quiz. */ + public $infomessages; + /** @var array $attempts contains all the user's attempts at this quiz. */ + public $attempts; + /** @var array $attemptobjs quiz_attempt objects corresponding to $attempts. */ + public $attemptobjs; + /** @var access_manager $accessmanager contains various access rules. */ + public $accessmanager; + /** @var bool $canreviewmine whether the current user has the capability to + * review their own attempts. */ + public $canreviewmine; + /** @var bool $canedit whether the current user has the capability to edit the quiz. */ + public $canedit; + /** @var moodle_url $editurl the URL for editing this quiz. */ + public $editurl; + /** @var int $attemptcolumn contains the number of attempts done. */ + public $attemptcolumn; + /** @var int $gradecolumn contains the grades of any attempts. */ + public $gradecolumn; + /** @var int $markcolumn contains the marks of any attempt. */ + public $markcolumn; + /** @var int $overallstats contains all marks for any attempt. */ + public $overallstats; + /** @var string $feedbackcolumn contains any feedback for and attempt. */ + public $feedbackcolumn; + /** @var string $timenow contains a timestamp in string format. */ + public $timenow; + /** @var int $numattempts contains the total number of attempts. */ + public $numattempts; + /** @var float $mygrade contains the user's final grade for a quiz. */ + public $mygrade; + /** @var bool $moreattempts whether this user is allowed more attempts. */ + public $moreattempts; + /** @var int $mygradeoverridden contains an overriden grade. */ + public $mygradeoverridden; + /** @var string $gradebookfeedback contains any feedback for a gradebook. */ + public $gradebookfeedback; + /** @var bool $unfinished contains 1 if an attempt is unfinished. */ + public $unfinished; + /** @var object $lastfinishedattempt the last attempt from the attempts array. */ + public $lastfinishedattempt; + /** @var array $preventmessages of messages telling the user why they can't + * attempt the quiz now. */ + public $preventmessages; + /** @var string $buttontext caption for the start attempt button. If this is null, show no + * button, or if it is '' show a back to the course button. */ + public $buttontext; + /** @var moodle_url $startattempturl URL to start an attempt. */ + public $startattempturl; + /** @var preflight_check_form|null $preflightcheckform confirmation form that must be + * submitted before an attempt is started, if required. */ + public $preflightcheckform; + /** @var moodle_url $startattempturl URL for any Back to the course button. */ + public $backtocourseurl; + /** @var bool $showbacktocourse should we show a back to the course button? */ + public $showbacktocourse; + /** @var bool whether the attempt must take place in a popup window. */ + public $popuprequired; + /** @var array options to use for the popup window, if required. */ + public $popupoptions; + /** @var bool $quizhasquestions whether the quiz has any questions. */ + public $quizhasquestions; + + public function __get($field) { + switch ($field) { + case 'startattemptwarning': + debugging('startattemptwarning has been deprecated. It is now always blank.'); + return ''; + + default: + debugging('Unknown property ' . $field); + return null; + } + } +} diff --git a/mod/quiz/db/renamedclasses.php b/mod/quiz/db/renamedclasses.php index d3dc759dc58..fe85ebd6298 100644 --- a/mod/quiz/db/renamedclasses.php +++ b/mod/quiz/db/renamedclasses.php @@ -57,4 +57,5 @@ $renamedclasses = [ 'quiz_access_rule_base' => 'mod_quiz\local\access_rule_base', 'quiz_add_random_form' => 'mod_quiz\form\add_random_form', 'mod_quiz_links_to_other_attempts' => 'mod_quiz\output\links_to_other_attempts', + 'mod_quiz_view_object' => 'mod_quiz\output\view_page', ]; diff --git a/mod/quiz/renderer.php b/mod/quiz/renderer.php index 8e108fc7d94..2ed18c39ac7 100644 --- a/mod/quiz/renderer.php +++ b/mod/quiz/renderer.php @@ -840,7 +840,7 @@ class mod_quiz_renderer extends plugin_renderer_base { * @param stdClass $quiz the quiz settings row from the database. * @param stdClass $cm the course_module settings row from the database. * @param context_module $context the quiz context. - * @param mod_quiz_view_object $viewobj + * @param view_page $viewobj * @return string HTML to display */ public function view_page($course, $quiz, $cm, $context, $viewobj) { @@ -857,10 +857,10 @@ class mod_quiz_renderer extends plugin_renderer_base { /** * Render the tertiary navigation for the view page. * - * @param mod_quiz_view_object $viewobj the information required to display the view page. + * @param view_page $viewobj the information required to display the view page. * @return string HTML to output. */ - public function view_page_tertiary_nav(mod_quiz_view_object $viewobj): string { + public function view_page_tertiary_nav(view_page $viewobj): string { $content = ''; if ($viewobj->buttontext) { @@ -886,10 +886,10 @@ class mod_quiz_renderer extends plugin_renderer_base { * Work out, and render, whatever buttons, and surrounding info, should appear * at the end of the review page. * - * @param mod_quiz_view_object $viewobj the information required to display the view page. + * @param view_page $viewobj the information required to display the view page. * @return string HTML to output. */ - public function view_page_buttons(mod_quiz_view_object $viewobj) { + public function view_page_buttons(view_page $viewobj) { $output = ''; if (!$viewobj->quizhasquestions) { @@ -981,7 +981,7 @@ class mod_quiz_renderer extends plugin_renderer_base { * @param stdClass $cm the course_module settings row from the database. * @param context_module $context the quiz context. * @param array $messages Array containing any messages - * @param mod_quiz_view_object $viewobj + * @param view_page $viewobj */ public function view_page_guest($course, $quiz, $cm, $context, $messages, $viewobj) { $output = ''; @@ -1002,7 +1002,7 @@ class mod_quiz_renderer extends plugin_renderer_base { * @param stdClass $cm the course_module settings row from the database. * @param context_module $context the quiz context. * @param array $messages Array containing any messages - * @param mod_quiz_view_object $viewobj + * @param view_page $viewobj */ public function view_page_notenrolled($course, $quiz, $cm, $context, $messages, $viewobj) { global $CFG; @@ -1078,7 +1078,7 @@ class mod_quiz_renderer extends plugin_renderer_base { * * @param array $quiz Array contining quiz data * @param int $context The page context ID - * @param mod_quiz_view_object $viewobj + * @param view_page $viewobj */ public function view_table($quiz, $context, $viewobj) { if (!$viewobj->attempts) { @@ -1229,7 +1229,7 @@ class mod_quiz_renderer extends plugin_renderer_base { * @param array $quiz Array containing quiz data * @param int $context The page context ID * @param int $cm The Course Module Id - * @param mod_quiz_view_object $viewobj + * @param view_page $viewobj */ public function view_result_info($quiz, $context, $cm, $viewobj) { $output = ''; @@ -1432,81 +1432,3 @@ class mod_quiz_renderer extends plugin_renderer_base { return $this->render_links_to_other_attempts($links); } } - - -class mod_quiz_view_object { - /** @var array $infomessages of messages with information to display about the quiz. */ - public $infomessages; - /** @var array $attempts contains all the user's attempts at this quiz. */ - public $attempts; - /** @var array $attemptobjs quiz_attempt objects corresponding to $attempts. */ - public $attemptobjs; - /** @var access_manager $accessmanager contains various access rules. */ - public $accessmanager; - /** @var bool $canreviewmine whether the current user has the capability to - * review their own attempts. */ - public $canreviewmine; - /** @var bool $canedit whether the current user has the capability to edit the quiz. */ - public $canedit; - /** @var moodle_url $editurl the URL for editing this quiz. */ - public $editurl; - /** @var int $attemptcolumn contains the number of attempts done. */ - public $attemptcolumn; - /** @var int $gradecolumn contains the grades of any attempts. */ - public $gradecolumn; - /** @var int $markcolumn contains the marks of any attempt. */ - public $markcolumn; - /** @var int $overallstats contains all marks for any attempt. */ - public $overallstats; - /** @var string $feedbackcolumn contains any feedback for and attempt. */ - public $feedbackcolumn; - /** @var string $timenow contains a timestamp in string format. */ - public $timenow; - /** @var int $numattempts contains the total number of attempts. */ - public $numattempts; - /** @var float $mygrade contains the user's final grade for a quiz. */ - public $mygrade; - /** @var bool $moreattempts whether this user is allowed more attempts. */ - public $moreattempts; - /** @var int $mygradeoverridden contains an overriden grade. */ - public $mygradeoverridden; - /** @var string $gradebookfeedback contains any feedback for a gradebook. */ - public $gradebookfeedback; - /** @var bool $unfinished contains 1 if an attempt is unfinished. */ - public $unfinished; - /** @var object $lastfinishedattempt the last attempt from the attempts array. */ - public $lastfinishedattempt; - /** @var array $preventmessages of messages telling the user why they can't - * attempt the quiz now. */ - public $preventmessages; - /** @var string $buttontext caption for the start attempt button. If this is null, show no - * button, or if it is '' show a back to the course button. */ - public $buttontext; - /** @var moodle_url $startattempturl URL to start an attempt. */ - public $startattempturl; - /** @var preflight_check_form|null $preflightcheckform confirmation form that must be - * submitted before an attempt is started, if required. */ - public $preflightcheckform; - /** @var moodle_url $startattempturl URL for any Back to the course button. */ - public $backtocourseurl; - /** @var bool $showbacktocourse should we show a back to the course button? */ - public $showbacktocourse; - /** @var bool whether the attempt must take place in a popup window. */ - public $popuprequired; - /** @var array options to use for the popup window, if required. */ - public $popupoptions; - /** @var bool $quizhasquestions whether the quiz has any questions. */ - public $quizhasquestions; - - public function __get($field) { - switch ($field) { - case 'startattemptwarning': - debugging('startattemptwarning has been deprecated. It is now always blank.'); - return ''; - - default: - debugging('Unknown property ' . $field); - return null; - } - } -} diff --git a/mod/quiz/upgrade.txt b/mod/quiz/upgrade.txt index 409bf2717dc..c301d840a19 100644 --- a/mod/quiz/upgrade.txt +++ b/mod/quiz/upgrade.txt @@ -41,6 +41,7 @@ This files describes API changes in the quiz code. - quiz_access_rule_base => mod_quiz\local\access_rule_base - quiz_add_random_form => mod_quiz\form\add_random_form - mod_quiz_links_to_other_attempts => mod_quiz\output\links_to_other_attempts + - mod_quiz_view_object => mod_quiz\output\view_page * The following classes have been deprecated: - mod_quiz_overdue_attempt_updater - merged into mod_quiz\task\update_overdue_attempts diff --git a/mod/quiz/view.php b/mod/quiz/view.php index a176e8d839b..96badfebe86 100644 --- a/mod/quiz/view.php +++ b/mod/quiz/view.php @@ -24,6 +24,7 @@ */ use mod_quiz\access_manager; +use mod_quiz\output\view_page; require_once(__DIR__ . '/../../config.php'); require_once($CFG->libdir.'/gradelib.php'); @@ -77,7 +78,7 @@ quiz_view($quiz, $course, $cm, $context); $PAGE->set_url('/mod/quiz/view.php', array('id' => $cm->id)); // Create view object which collects all the information the renderer will need. -$viewobj = new mod_quiz_view_object(); +$viewobj = new view_page(); $viewobj->accessmanager = $accessmanager; $viewobj->canreviewmine = $canreviewmine || $canpreview; From 6c989eb24c711ce438edd49cf363828e130acdf8 Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Mon, 19 Dec 2022 19:13:43 +0000 Subject: [PATCH 14/30] MDL-76614 quiz: Final deprecation of view_page::$startattemptwarning This waas deprecated in Moodle 3.1 MDL-46091 --- mod/quiz/classes/output/view_page.php | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/mod/quiz/classes/output/view_page.php b/mod/quiz/classes/output/view_page.php index b7e8b85a206..6475a31867d 100644 --- a/mod/quiz/classes/output/view_page.php +++ b/mod/quiz/classes/output/view_page.php @@ -94,16 +94,4 @@ class view_page { public $popupoptions; /** @var bool $quizhasquestions whether the quiz has any questions. */ public $quizhasquestions; - - public function __get($field) { - switch ($field) { - case 'startattemptwarning': - debugging('startattemptwarning has been deprecated. It is now always blank.'); - return ''; - - default: - debugging('Unknown property ' . $field); - return null; - } - } } From 5ef8b6978ed1e276a23ec868ad6ccd9952846d5e Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Mon, 19 Dec 2022 16:01:51 +0000 Subject: [PATCH 15/30] MDL-76614 quiz: mod_quiz_renderer => mod_quiz\output\renderer --- mod/quiz/attemptlib.php | 31 +- mod/quiz/classes/access_manager.php | 10 +- mod/quiz/classes/output/renderer.php | 1427 ++++++++++++++++++++++++++ mod/quiz/db/renamedclasses.php | 1 + mod/quiz/locallib.php | 1 - mod/quiz/renderer.php | 1421 +------------------------ mod/quiz/tests/locallib_test.php | 3 +- mod/quiz/upgrade.txt | 2 + mod/quiz/view.php | 3 +- 9 files changed, 1462 insertions(+), 1437 deletions(-) create mode 100644 mod/quiz/classes/output/renderer.php diff --git a/mod/quiz/attemptlib.php b/mod/quiz/attemptlib.php index 5813053aea6..bcc9d6f441a 100644 --- a/mod/quiz/attemptlib.php +++ b/mod/quiz/attemptlib.php @@ -29,6 +29,7 @@ defined('MOODLE_INTERNAL') || die(); use mod_quiz\access_manager; use mod_quiz\output\links_to_other_attempts; +use mod_quiz\output\renderer; use mod_quiz\question\bank\qbank_helper; use mod_quiz\question\display_options; @@ -1698,11 +1699,11 @@ class quiz_attempt { * * @param int $slot identifies the question in the attempt. * @param bool $reviewing is the being printed on an attempt or a review page. - * @param mod_quiz_renderer $renderer the quiz renderer. + * @param renderer $renderer the quiz renderer. * @param moodle_url $thispageurl the URL of the page this question is being printed on. * @return string HTML for the question in its current state. */ - public function render_question($slot, $reviewing, mod_quiz_renderer $renderer, $thispageurl = null) { + public function render_question($slot, $reviewing, renderer $renderer, $thispageurl = null) { if ($this->is_blocked_by_previous_question($slot)) { $placeholderqa = $this->make_blocked_question_placeholder($slot); @@ -1725,12 +1726,12 @@ class quiz_attempt { * @param int $slot identifies the question in the attempt. * @param bool $reviewing is the being printed on an attempt or a review page. * @param moodle_url $thispageurl the URL of the page this question is being printed on. - * @param mod_quiz_renderer $renderer the quiz renderer. + * @param renderer $renderer the quiz renderer. * @param int|null $seq the seq number of the past state to display. * @return string HTML fragment. */ protected function render_question_helper($slot, $reviewing, $thispageurl, - mod_quiz_renderer $renderer, $seq) { + renderer $renderer, $seq) { $originalslot = $this->get_original_slot($slot); $number = $this->get_question_number($originalslot); $displayoptions = $this->get_display_options_with_edit_link($reviewing, $slot, $thispageurl); @@ -1811,12 +1812,12 @@ class quiz_attempt { * @param int $slot the slot number of a question in this quiz attempt. * @param int $seq the seq number of the past state to display. * @param bool $reviewing is the being printed on an attempt or a review page. - * @param mod_quiz_renderer $renderer the quiz renderer. + * @param renderer $renderer the quiz renderer. * @param moodle_url $thispageurl the URL of the page this question is being printed on. * @return string HTML for the question in its current state. */ public function render_question_at_step($slot, $seq, $reviewing, - mod_quiz_renderer $renderer, $thispageurl = null) { + renderer $renderer, $thispageurl = null) { return $this->render_question_helper($slot, $reviewing, $thispageurl, $renderer, $seq); } @@ -1867,13 +1868,13 @@ class quiz_attempt { /** * Get the navigation panel object for this attempt. * - * @param mod_quiz_renderer $output the quiz renderer to use to output things. + * @param renderer $output the quiz renderer to use to output things. * @param string $panelclass The type of panel, quiz_attempt_nav_panel or quiz_review_nav_panel * @param int $page the current page number. * @param bool $showall whether we are showing the whole quiz on one page. (Used by review.php.) * @return block_contents the requested object. */ - public function get_navigation_panel(mod_quiz_renderer $output, + public function get_navigation_panel(renderer $output, $panelclass, $page, $showall = false) { $panel = new $panelclass($this, $this->get_display_options(true), $page, $showall); @@ -2845,19 +2846,19 @@ abstract class quiz_nav_panel_base { /** * Hook for subclasses to override. * - * @param mod_quiz_renderer $output the quiz renderer to use. + * @param renderer $output the quiz renderer to use. * @return string HTML to output. */ - public function render_before_button_bits(mod_quiz_renderer $output) { + public function render_before_button_bits(renderer $output) { return ''; } - abstract public function render_end_bits(mod_quiz_renderer $output); + abstract public function render_end_bits(renderer $output); /** * Render the restart preview button. * - * @param mod_quiz_renderer $output the quiz renderer to use. + * @param renderer $output the quiz renderer to use. * @return string HTML to output. */ protected function render_restart_preview_link($output) { @@ -2917,12 +2918,12 @@ class quiz_attempt_nav_panel extends quiz_nav_panel_base { } } - public function render_before_button_bits(mod_quiz_renderer $output) { + public function render_before_button_bits(renderer $output) { return html_writer::tag('div', get_string('navnojswarning', 'quiz'), array('id' => 'quiznojswarning')); } - public function render_end_bits(mod_quiz_renderer $output) { + public function render_end_bits(renderer $output) { if ($this->page == -1) { // Don't link from the summary page to itself. return ''; @@ -2946,7 +2947,7 @@ class quiz_review_nav_panel extends quiz_nav_panel_base { return $this->attemptobj->review_url($slot, -1, $this->showall, $this->page); } - public function render_end_bits(mod_quiz_renderer $output) { + public function render_end_bits(renderer $output) { $html = ''; if ($this->attemptobj->get_num_pages() > 1) { if ($this->showall) { diff --git a/mod/quiz/classes/access_manager.php b/mod/quiz/classes/access_manager.php index 33c1d62698a..c17893cd13f 100644 --- a/mod/quiz/classes/access_manager.php +++ b/mod/quiz/classes/access_manager.php @@ -21,7 +21,7 @@ use mod_quiz\form\preflight_check_form; use mod_quiz\local\access_rule_base; use mod_quiz\question\display_options; use mod_quiz_mod_form; -use mod_quiz_renderer; +use mod_quiz\output\renderer; use moodle_page; use moodle_url; use MoodleQuickForm; @@ -524,10 +524,10 @@ class access_manager { * * This method does not return; * - * @param mod_quiz_renderer $output the quiz renderer. + * @param renderer $output the quiz renderer. * @param string $message optional message to output while redirecting. */ - public function back_to_view_page(mod_quiz_renderer $output, string $message = ''): void { + public function back_to_view_page(renderer $output, string $message = ''): void { // Actually return type 'never' on the previous line, once 8.1 is our minimum PHP version. if ($this->attempt_must_be_in_popup()) { echo $output->close_attempt_popup(new moodle_url($this->quizobj->view_url()), $message); @@ -542,11 +542,11 @@ class access_manager { * * @param stdClass $attempt the attempt object * @param mixed $nolongerused not used any more. - * @param mod_quiz_renderer $output quiz renderer instance. + * @param renderer $output quiz renderer instance. * @return string some HTML, the $linktext either unmodified or wrapped in a * link to the review page. */ - public function make_review_link(stdClass $attempt, $nolongerused, mod_quiz_renderer $output): string { + public function make_review_link(stdClass $attempt, $nolongerused, renderer $output): string { // If the attempt is still open, don't link. if (in_array($attempt->state, [quiz_attempt::IN_PROGRESS, quiz_attempt::OVERDUE])) { diff --git a/mod/quiz/classes/output/renderer.php b/mod/quiz/classes/output/renderer.php new file mode 100644 index 00000000000..6d7bade579d --- /dev/null +++ b/mod/quiz/classes/output/renderer.php @@ -0,0 +1,1427 @@ +. + +namespace mod_quiz\output; + +use mod_quiz\access_manager; +use mod_quiz\form\preflight_check_form; +use mod_quiz\question\display_options; +use plugin_renderer_base; +use quiz_attempt; + +/** + * The main renderer for the quiz module. + * + * @package mod_quiz + * @category output + * @copyright 2011 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class renderer extends plugin_renderer_base { + /** + * Builds the review page + * + * @param quiz_attempt $attemptobj an instance of quiz_attempt. + * @param array $slots an array of intgers relating to questions. + * @param int $page the current page number + * @param bool $showall whether to show entire attempt on one page. + * @param bool $lastpage if true the current page is the last page. + * @param display_options $displayoptions instance of display_options. + * @param array $summarydata contains all table data + * @return $output containing html data. + */ + public function review_page(quiz_attempt $attemptobj, $slots, $page, $showall, + $lastpage, display_options $displayoptions, + $summarydata) { + + $output = ''; + $output .= $this->header(); + $output .= $this->review_summary_table($summarydata, $page); + $output .= $this->review_form($page, $showall, $displayoptions, + $this->questions($attemptobj, true, $slots, $page, $showall, $displayoptions), + $attemptobj); + + $output .= $this->review_next_navigation($attemptobj, $page, $lastpage, $showall); + $output .= $this->footer(); + return $output; + } + + /** + * Renders the review question pop-up. + * + * @param quiz_attempt $attemptobj an instance of quiz_attempt. + * @param int $slot which question to display. + * @param int $seq which step of the question attempt to show. null = latest. + * @param display_options $displayoptions instance of display_options. + * @param array $summarydata contains all table data + * @return $output containing html data. + */ + public function review_question_page(quiz_attempt $attemptobj, $slot, $seq, + display_options $displayoptions, $summarydata) { + + $output = ''; + $output .= $this->header(); + $output .= $this->review_summary_table($summarydata, 0); + + if (!is_null($seq)) { + $output .= $attemptobj->render_question_at_step($slot, $seq, true, $this); + } else { + $output .= $attemptobj->render_question($slot, true, $this); + } + + $output .= $this->close_window_button(); + $output .= $this->footer(); + return $output; + } + + /** + * Renders the review question pop-up. + * + * @param quiz_attempt $attemptobj an instance of quiz_attempt. + * @param string $message Why the review is not allowed. + * @return string html to output. + */ + public function review_question_not_allowed(quiz_attempt $attemptobj, $message) { + $output = ''; + $output .= $this->header(); + $output .= $this->heading(format_string($attemptobj->get_quiz_name(), true, + array("context" => $attemptobj->get_quizobj()->get_context()))); + $output .= $this->notification($message); + $output .= $this->close_window_button(); + $output .= $this->footer(); + return $output; + } + + /** + * Filters the summarydata array. + * + * @param array $summarydata contains row data for table + * @param int $page the current page number + * @return $summarydata containing filtered row data + */ + protected function filter_review_summary_table($summarydata, $page) { + if ($page == 0) { + return $summarydata; + } + + // Only show some of summary table on subsequent pages. + foreach ($summarydata as $key => $rowdata) { + if (!in_array($key, array('user', 'attemptlist'))) { + unset($summarydata[$key]); + } + } + + return $summarydata; + } + + /** + * Outputs the table containing data from summary data array + * + * @param array $summarydata contains row data for table + * @param int $page contains the current page number + */ + public function review_summary_table($summarydata, $page) { + $summarydata = $this->filter_review_summary_table($summarydata, $page); + if (empty($summarydata)) { + return ''; + } + + $output = ''; + $output .= html_writer::start_tag('table', array( + 'class' => 'generaltable generalbox quizreviewsummary')); + $output .= html_writer::start_tag('tbody'); + foreach ($summarydata as $rowdata) { + if ($rowdata['title'] instanceof renderable) { + $title = $this->render($rowdata['title']); + } else { + $title = $rowdata['title']; + } + + if ($rowdata['content'] instanceof renderable) { + $content = $this->render($rowdata['content']); + } else { + $content = $rowdata['content']; + } + + $output .= html_writer::tag('tr', + html_writer::tag('th', $title, array('class' => 'cell', 'scope' => 'row')) . + html_writer::tag('td', $content, array('class' => 'cell')) + ); + } + + $output .= html_writer::end_tag('tbody'); + $output .= html_writer::end_tag('table'); + return $output; + } + + /** + * Renders each question + * + * @param quiz_attempt $attemptobj instance of quiz_attempt + * @param bool $reviewing + * @param array $slots array of intgers relating to questions + * @param int $page current page number + * @param bool $showall if true shows attempt on single page + * @param display_options $displayoptions instance of display_options + */ + public function questions(quiz_attempt $attemptobj, $reviewing, $slots, $page, $showall, + display_options $displayoptions) { + $output = ''; + foreach ($slots as $slot) { + $output .= $attemptobj->render_question($slot, $reviewing, $this, + $attemptobj->review_url($slot, $page, $showall)); + } + return $output; + } + + /** + * Renders the main bit of the review page. + * + * @param array $summarydata contain row data for table + * @param int $page current page number + * @param display_options $displayoptions instance of display_options + * @param $content contains each question + * @param quiz_attempt $attemptobj instance of quiz_attempt + * @param bool $showall if true display attempt on one page + */ + public function review_form($page, $showall, $displayoptions, $content, $attemptobj) { + if ($displayoptions->flags != question_display_options::EDITABLE) { + return $content; + } + + $this->page->requires->js_init_call('M.mod_quiz.init_review_form', null, false, + quiz_get_js_module()); + + $output = ''; + $output .= html_writer::start_tag('form', array('action' => $attemptobj->review_url(null, + $page, $showall), 'method' => 'post', 'class' => 'questionflagsaveform')); + $output .= html_writer::start_tag('div'); + $output .= $content; + $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'sesskey', + 'value' => sesskey())); + $output .= html_writer::start_tag('div', array('class' => 'submitbtns')); + $output .= html_writer::empty_tag('input', array('type' => 'submit', + 'class' => 'questionflagsavebutton btn btn-secondary', 'name' => 'savingflags', + 'value' => get_string('saveflags', 'question'))); + $output .= html_writer::end_tag('div'); + $output .= html_writer::end_tag('div'); + $output .= html_writer::end_tag('form'); + + return $output; + } + + /** + * Returns either a liink or button + * + * @param quiz_attempt $attemptobj instance of quiz_attempt + */ + public function finish_review_link(quiz_attempt $attemptobj) { + $url = $attemptobj->view_url(); + + if ($attemptobj->get_access_manager(time())->attempt_must_be_in_popup()) { + $this->page->requires->js_init_call('M.mod_quiz.secure_window.init_close_button', + array($url), false, quiz_get_js_module()); + return html_writer::empty_tag('input', array('type' => 'button', + 'value' => get_string('finishreview', 'quiz'), + 'id' => 'secureclosebutton', + 'class' => 'mod_quiz-next-nav btn btn-primary')); + + } else { + return html_writer::link($url, get_string('finishreview', 'quiz'), + array('class' => 'mod_quiz-next-nav')); + } + } + + /** + * Creates the navigation links/buttons at the bottom of the reivew attempt page. + * + * Note, the name of this function is no longer accurate, but when the design + * changed, it was decided to keep the old name for backwards compatibility. + * + * @param quiz_attempt $attemptobj instance of quiz_attempt + * @param int $page the current page + * @param bool $lastpage if true current page is the last page + * @param bool|null $showall if true, the URL will be to review the entire attempt on one page, + * and $page will be ignored. If null, a sensible default will be chosen. + * + * @return string HTML fragment. + */ + public function review_next_navigation(quiz_attempt $attemptobj, $page, $lastpage, $showall = null) { + $nav = ''; + if ($page > 0) { + $nav .= link_arrow_left(get_string('navigateprevious', 'quiz'), + $attemptobj->review_url(null, $page - 1, $showall), false, 'mod_quiz-prev-nav'); + } + if ($lastpage) { + $nav .= $this->finish_review_link($attemptobj); + } else { + $nav .= link_arrow_right(get_string('navigatenext', 'quiz'), + $attemptobj->review_url(null, $page + 1, $showall), false, 'mod_quiz-next-nav'); + } + return html_writer::tag('div', $nav, array('class' => 'submitbtns')); + } + + /** + * Return the HTML of the quiz timer. + * @return string HTML content. + */ + public function countdown_timer(quiz_attempt $attemptobj, $timenow) { + + $timeleft = $attemptobj->get_time_left_display($timenow); + if ($timeleft !== false) { + $ispreview = $attemptobj->is_preview(); + $timerstartvalue = $timeleft; + if (!$ispreview) { + // Make sure the timer starts just above zero. If $timeleft was <= 0, then + // this will just have the effect of causing the quiz to be submitted immediately. + $timerstartvalue = max($timerstartvalue, 1); + } + $this->initialise_timer($timerstartvalue, $ispreview); + } + + + return $this->output->render_from_template('mod_quiz/timer', (object)[]); + } + + /** + * Create a preview link + * + * @param moodle_url $url contains a url to the given page + */ + public function restart_preview_button($url) { + return $this->single_button($url, get_string('startnewpreview', 'quiz')); + } + + /** + * Outputs the navigation block panel + * + * @param quiz_nav_panel_base $panel instance of quiz_nav_panel_base + */ + public function navigation_panel(quiz_nav_panel_base $panel) { + + $output = ''; + $userpicture = $panel->user_picture(); + if ($userpicture) { + $fullname = fullname($userpicture->user); + if ($userpicture->size === true) { + $fullname = html_writer::div($fullname); + } + $output .= html_writer::tag('div', $this->render($userpicture) . $fullname, + array('id' => 'user-picture', 'class' => 'clearfix')); + } + $output .= $panel->render_before_button_bits($this); + + $bcc = $panel->get_button_container_class(); + $output .= html_writer::start_tag('div', array('class' => "qn_buttons clearfix $bcc")); + foreach ($panel->get_question_buttons() as $button) { + $output .= $this->render($button); + } + $output .= html_writer::end_tag('div'); + + $output .= html_writer::tag('div', $panel->render_end_bits($this), + array('class' => 'othernav')); + + $this->page->requires->js_init_call('M.mod_quiz.nav.init', null, false, + quiz_get_js_module()); + + return $output; + } + + /** + * Display a quiz navigation button. + * + * @param quiz_nav_question_button $button + * @return string HTML fragment. + */ + protected function render_quiz_nav_question_button(quiz_nav_question_button $button) { + $classes = array('qnbutton', $button->stateclass, $button->navmethod, 'btn'); + $extrainfo = array(); + + if ($button->currentpage) { + $classes[] = 'thispage'; + $extrainfo[] = get_string('onthispage', 'quiz'); + } + + // Flagged? + if ($button->flagged) { + $classes[] = 'flagged'; + $flaglabel = get_string('flagged', 'question'); + } else { + $flaglabel = ''; + } + $extrainfo[] = html_writer::tag('span', $flaglabel, array('class' => 'flagstate')); + + if (is_numeric($button->number)) { + $qnostring = 'questionnonav'; + } else { + $qnostring = 'questionnonavinfo'; + } + + $a = new stdClass(); + $a->number = $button->number; + $a->attributes = implode(' ', $extrainfo); + $tagcontents = html_writer::tag('span', '', array('class' => 'thispageholder')) . + html_writer::tag('span', '', array('class' => 'trafficlight')) . + get_string($qnostring, 'quiz', $a); + $tagattributes = array('class' => implode(' ', $classes), 'id' => $button->id, + 'title' => $button->statestring, 'data-quiz-page' => $button->page); + + if ($button->url) { + return html_writer::link($button->url, $tagcontents, $tagattributes); + } else { + return html_writer::tag('span', $tagcontents, $tagattributes); + } + } + + /** + * Display a quiz navigation heading. + * + * @param quiz_nav_section_heading $heading the heading. + * @return string HTML fragment. + */ + protected function render_quiz_nav_section_heading(quiz_nav_section_heading $heading) { + if (empty($heading->heading)) { + $headingtext = get_string('sectionnoname', 'quiz'); + $class = ' dimmed_text'; + } else { + $headingtext = $heading->heading; + $class = ''; + } + return $this->heading($headingtext, 3, 'mod_quiz-section-heading' . $class); + } + + /** + * Renders a list of links the other attempts. + * + * @param links_to_other_attempts $links + * @return string HTML fragment. + */ + protected function render_links_to_other_attempts( + links_to_other_attempts $links) { + $attemptlinks = array(); + foreach ($links->links as $attempt => $url) { + if (!$url) { + $attemptlinks[] = html_writer::tag('strong', $attempt); + } else if ($url instanceof renderable) { + $attemptlinks[] = $this->render($url); + } else { + $attemptlinks[] = html_writer::link($url, $attempt); + } + } + return implode(', ', $attemptlinks); + } + + /** + * Render the 'start attempt' page. + * + * The student gets here if their interaction with the preflight check + * from fails in some way (e.g. they typed the wrong password). + * + * @param quiz $quizobj + * @param preflight_check_form $mform + * @return string + */ + public function start_attempt_page(quiz $quizobj, preflight_check_form $mform) { + $output = ''; + $output .= $this->header(); + $output .= $this->during_attempt_tertiary_nav($quizobj->view_url()); + $output .= $this->heading(format_string($quizobj->get_quiz_name(), true, + array("context" => $quizobj->get_context()))); + $output .= $this->quiz_intro($quizobj->get_quiz(), $quizobj->get_cm()); + $output .= $mform->render(); + $output .= $this->footer(); + return $output; + } + + /** + * Attempt Page + * + * @param quiz_attempt $attemptobj Instance of quiz_attempt + * @param int $page Current page number + * @param access_manager $accessmanager Instance of access_manager + * @param array $messages An array of messages + * @param array $slots Contains an array of integers that relate to questions + * @param int $id The ID of an attempt + * @param int $nextpage The number of the next page + * @return string HTML to output. + */ + public function attempt_page($attemptobj, $page, $accessmanager, $messages, $slots, $id, + $nextpage) { + $output = ''; + $output .= $this->header(); + $output .= $this->during_attempt_tertiary_nav($attemptobj->view_url()); + $output .= $this->quiz_notices($messages); + $output .= $this->countdown_timer($attemptobj, time()); + $output .= $this->attempt_form($attemptobj, $page, $slots, $id, $nextpage); + $output .= $this->footer(); + return $output; + } + + /** + * Render the tertiary navigation for pages during the attempt. + * + * @param string|moodle_url $quizviewurl url of the view.php page for this quiz. + * @return string HTML to output. + */ + public function during_attempt_tertiary_nav($quizviewurl): string { + $output = ''; + $output .= html_writer::start_div('container-fluid tertiary-navigation'); + $output .= html_writer::start_div('row'); + $output .= html_writer::start_div('navitem'); + $output .= html_writer::link($quizviewurl, get_string('back'), + ['class' => 'btn btn-secondary']); + $output .= html_writer::end_div(); + $output .= html_writer::end_div(); + $output .= html_writer::end_div(); + return $output; + } + + /** + * Returns any notices. + * + * @param array $messages + */ + public function quiz_notices($messages) { + if (!$messages) { + return ''; + } + return $this->notification( + html_writer::tag('p', get_string('accessnoticesheader', 'quiz')) . $this->access_messages($messages), + 'warning', + false + ); + } + + /** + * Ouputs the form for making an attempt + * + * @param quiz_attempt $attemptobj + * @param int $page Current page number + * @param array $slots Array of integers relating to questions + * @param int $id ID of the attempt + * @param int $nextpage Next page number + */ + public function attempt_form($attemptobj, $page, $slots, $id, $nextpage) { + $output = ''; + + // Start the form. + $output .= html_writer::start_tag('form', + array('action' => new moodle_url($attemptobj->processattempt_url(), + array('cmid' => $attemptobj->get_cmid())), 'method' => 'post', + 'enctype' => 'multipart/form-data', 'accept-charset' => 'utf-8', + 'id' => 'responseform')); + $output .= html_writer::start_tag('div'); + + // Print all the questions. + foreach ($slots as $slot) { + $output .= $attemptobj->render_question($slot, false, $this, + $attemptobj->attempt_url($slot, $page), $this); + } + + $navmethod = $attemptobj->get_quiz()->navmethod; + $output .= $this->attempt_navigation_buttons($page, $attemptobj->is_last_page($page), $navmethod); + + // Some hidden fields to trach what is going on. + $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'attempt', + 'value' => $attemptobj->get_attemptid())); + $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'thispage', + 'value' => $page, 'id' => 'followingpage')); + $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'nextpage', + 'value' => $nextpage)); + $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'timeup', + 'value' => '0', 'id' => 'timeup')); + $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'sesskey', + 'value' => sesskey())); + $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'scrollpos', + 'value' => '', 'id' => 'scrollpos')); + + // Add a hidden field with questionids. Do this at the end of the form, so + // if you navigate before the form has finished loading, it does not wipe all + // the student's answers. + $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'slots', + 'value' => implode(',', $attemptobj->get_active_slots($page)))); + + // Finish the form. + $output .= html_writer::end_tag('div'); + $output .= html_writer::end_tag('form'); + + $output .= $this->connection_warning(); + + return $output; + } + + /** + * Display the prev/next buttons that go at the bottom of each page of the attempt. + * + * @param int $page the page number. Starts at 0 for the first page. + * @param bool $lastpage is this the last page in the quiz? + * @param string $navmethod Optional quiz attribute, 'free' (default) or 'sequential' + * @return string HTML fragment. + */ + protected function attempt_navigation_buttons($page, $lastpage, $navmethod = 'free') { + $output = ''; + + $output .= html_writer::start_tag('div', array('class' => 'submitbtns')); + if ($page > 0 && $navmethod == 'free') { + $output .= html_writer::empty_tag('input', array('type' => 'submit', 'name' => 'previous', + 'value' => get_string('navigateprevious', 'quiz'), 'class' => 'mod_quiz-prev-nav btn btn-secondary', + 'id' => 'mod_quiz-prev-nav')); + $this->page->requires->js_call_amd('core_form/submit', 'init', ['mod_quiz-prev-nav']); + } + if ($lastpage) { + $nextlabel = get_string('endtest', 'quiz'); + } else { + $nextlabel = get_string('navigatenext', 'quiz'); + } + $output .= html_writer::empty_tag('input', array('type' => 'submit', 'name' => 'next', + 'value' => $nextlabel, 'class' => 'mod_quiz-next-nav btn btn-primary', 'id' => 'mod_quiz-next-nav')); + $output .= html_writer::end_tag('div'); + $this->page->requires->js_call_amd('core_form/submit', 'init', ['mod_quiz-next-nav']); + + return $output; + } + + /** + * Render a button which allows students to redo a question in the attempt. + * + * @param int $slot the number of the slot to generate the button for. + * @param bool $disabled if true, output the button disabled. + * @return string HTML fragment. + */ + public function redo_question_button($slot, $disabled) { + $attributes = array('type' => 'submit', 'name' => 'redoslot' . $slot, + 'value' => get_string('redoquestion', 'quiz'), + 'class' => 'mod_quiz-redo_question_button btn btn-secondary'); + if ($disabled) { + $attributes['disabled'] = 'disabled'; + } + return html_writer::div(html_writer::empty_tag('input', $attributes)); + } + + /** + * Output the JavaScript required to initialise the countdown timer. + * @param int $timerstartvalue time remaining, in seconds. + */ + public function initialise_timer($timerstartvalue, $ispreview) { + $options = array($timerstartvalue, (bool)$ispreview); + $this->page->requires->js_init_call('M.mod_quiz.timer.init', $options, false, quiz_get_js_module()); + } + + /** + * Output a page with an optional message, and JavaScript code to close the + * current window and redirect the parent window to a new URL. + * @param moodle_url $url the URL to redirect the parent window to. + * @param string $message message to display before closing the window. (optional) + * @return string HTML to output. + */ + public function close_attempt_popup($url, $message = '') { + $output = ''; + $output .= $this->header(); + $output .= $this->box_start(); + + if ($message) { + $output .= html_writer::tag('p', $message); + $output .= html_writer::tag('p', get_string('windowclosing', 'quiz')); + $delay = 5; + } else { + $output .= html_writer::tag('p', get_string('pleaseclose', 'quiz')); + $delay = 0; + } + $this->page->requires->js_init_call('M.mod_quiz.secure_window.close', + array($url, $delay), false, quiz_get_js_module()); + + $output .= $this->box_end(); + $output .= $this->footer(); + return $output; + } + + /** + * Print each message in an array, surrounded by <p>, </p> tags. + * + * @param array $messages the array of message strings. + * @param bool $return if true, return a string, instead of outputting. + * + * @return string HTML to output. + */ + public function access_messages($messages) { + $output = ''; + foreach ($messages as $message) { + $output .= html_writer::tag('p', $message, ['class' => 'text-left']); + } + return $output; + } + + /* + * Summary Page + */ + /** + * Create the summary page + * + * @param quiz_attempt $attemptobj + * @param display_options $displayoptions + */ + public function summary_page($attemptobj, $displayoptions) { + $output = ''; + $output .= $this->header(); + $output .= $this->during_attempt_tertiary_nav($attemptobj->view_url()); + $output .= $this->heading(format_string($attemptobj->get_quiz_name())); + $output .= $this->heading(get_string('summaryofattempt', 'quiz'), 3); + $output .= $this->summary_table($attemptobj, $displayoptions); + $output .= $this->summary_page_controls($attemptobj); + $output .= $this->footer(); + return $output; + } + + /** + * Generates the table of summarydata + * + * @param quiz_attempt $attemptobj + * @param display_options $displayoptions + */ + public function summary_table($attemptobj, $displayoptions) { + // Prepare the summary table header. + $table = new html_table(); + $table->attributes['class'] = 'generaltable quizsummaryofattempt boxaligncenter'; + $table->head = array(get_string('question', 'quiz'), get_string('status', 'quiz')); + $table->align = array('left', 'left'); + $table->size = array('', ''); + $markscolumn = $displayoptions->marks >= question_display_options::MARK_AND_MAX; + if ($markscolumn) { + $table->head[] = get_string('marks', 'quiz'); + $table->align[] = 'left'; + $table->size[] = ''; + } + $tablewidth = count($table->align); + $table->data = array(); + + // Get the summary info for each question. + $slots = $attemptobj->get_slots(); + foreach ($slots as $slot) { + // Add a section headings if we need one here. + $heading = $attemptobj->get_heading_before_slot($slot); + if ($heading !== null) { + // There is a heading here. + $rowclasses = 'quizsummaryheading'; + if ($heading) { + $heading = format_string($heading); + } else if (count($attemptobj->get_quizobj()->get_sections()) > 1) { + // If this is the start of an unnamed section, and the quiz has more + // than one section, then add a default heading. + $heading = get_string('sectionnoname', 'quiz'); + $rowclasses .= ' dimmed_text'; + } + $cell = new html_table_cell(format_string($heading)); + $cell->header = true; + $cell->colspan = $tablewidth; + $table->data[] = array($cell); + $table->rowclasses[] = $rowclasses; + } + + // Don't display information items. + if (!$attemptobj->is_real_question($slot)) { + continue; + } + + // Real question, show it. + $flag = ''; + if ($attemptobj->is_question_flagged($slot)) { + // Quiz has custom JS manipulating these image tags - so we can't use the pix_icon method here. + $flag = html_writer::empty_tag('img', array('src' => $this->image_url('i/flagged'), + 'alt' => get_string('flagged', 'question'), 'class' => 'questionflag icon-post')); + } + if ($attemptobj->can_navigate_to($slot)) { + $row = array(html_writer::link($attemptobj->attempt_url($slot), + $attemptobj->get_question_number($slot) . $flag), + $attemptobj->get_question_status($slot, $displayoptions->correctness)); + } else { + $row = array($attemptobj->get_question_number($slot) . $flag, + $attemptobj->get_question_status($slot, $displayoptions->correctness)); + } + if ($markscolumn) { + $row[] = $attemptobj->get_question_mark($slot); + } + $table->data[] = $row; + $table->rowclasses[] = 'quizsummary' . $slot . ' ' . $attemptobj->get_question_state_class( + $slot, $displayoptions->correctness); + } + + // Print the summary table. + $output = html_writer::table($table); + + return $output; + } + + /** + * Creates any controls a the page should have. + * + * @param quiz_attempt $attemptobj + */ + public function summary_page_controls($attemptobj) { + $output = ''; + + // Return to place button. + if ($attemptobj->get_state() == quiz_attempt::IN_PROGRESS) { + $button = new single_button( + new moodle_url($attemptobj->attempt_url(null, $attemptobj->get_currentpage())), + get_string('returnattempt', 'quiz')); + $output .= $this->container($this->container($this->render($button), + 'controls'), 'submitbtns mdl-align'); + } + + // Finish attempt button. + $options = array( + 'attempt' => $attemptobj->get_attemptid(), + 'finishattempt' => 1, + 'timeup' => 0, + 'slots' => '', + 'cmid' => $attemptobj->get_cmid(), + 'sesskey' => sesskey(), + ); + + $button = new single_button( + new moodle_url($attemptobj->processattempt_url(), $options), + get_string('submitallandfinish', 'quiz')); + $button->id = 'responseform'; + $button->class = 'btn-finishattempt'; + $button->formid = 'frm-finishattempt'; + if ($attemptobj->get_state() == quiz_attempt::IN_PROGRESS) { + $totalunanswered = 0; + if ($attemptobj->get_quiz()->navmethod == 'free') { + // Only count the unanswered question if the navigation method is set to free. + $totalunanswered = $attemptobj->get_number_of_unanswered_questions(); + } + $this->page->requires->js_call_amd('mod_quiz/submission_confirmation', 'init', [$totalunanswered]); + } + $button->primary = true; + + $duedate = $attemptobj->get_due_date(); + $message = ''; + if ($attemptobj->get_state() == quiz_attempt::OVERDUE) { + $message = get_string('overduemustbesubmittedby', 'quiz', userdate($duedate)); + + } else if ($duedate) { + $message = get_string('mustbesubmittedby', 'quiz', userdate($duedate)); + } + + $output .= $this->countdown_timer($attemptobj, time()); + $output .= $this->container($message . $this->container( + $this->render($button), 'controls'), 'submitbtns mdl-align'); + + return $output; + } + + /* + * View Page + */ + /** + * Generates the view page + * + * @param stdClass $course the course settings row from the database. + * @param stdClass $quiz the quiz settings row from the database. + * @param stdClass $cm the course_module settings row from the database. + * @param context_module $context the quiz context. + * @param view_page $viewobj + * @return string HTML to display + */ + public function view_page($course, $quiz, $cm, $context, $viewobj) { + $output = ''; + + $output .= $this->view_page_tertiary_nav($viewobj); + $output .= $this->view_information($quiz, $cm, $context, $viewobj->infomessages); + $output .= $this->view_table($quiz, $context, $viewobj); + $output .= $this->view_result_info($quiz, $context, $cm, $viewobj); + $output .= $this->box($this->view_page_buttons($viewobj), 'quizattempt'); + return $output; + } + + /** + * Render the tertiary navigation for the view page. + * + * @param view_page $viewobj the information required to display the view page. + * @return string HTML to output. + */ + public function view_page_tertiary_nav(view_page $viewobj): string { + $content = ''; + + if ($viewobj->buttontext) { + $attemptbtn = $this->start_attempt_button($viewobj->buttontext, + $viewobj->startattempturl, $viewobj->preflightcheckform, + $viewobj->popuprequired, $viewobj->popupoptions); + $content .= $attemptbtn; + } + + if ($viewobj->canedit && !$viewobj->quizhasquestions) { + $content .= html_writer::link($viewobj->editurl, get_string('addquestion', 'quiz'), + ['class' => 'btn btn-secondary']); + } + + if ($content) { + return html_writer::div(html_writer::div($content, 'row'), 'container-fluid tertiary-navigation'); + } else { + return ''; + } + } + + /** + * Work out, and render, whatever buttons, and surrounding info, should appear + * at the end of the review page. + * + * @param view_page $viewobj the information required to display the view page. + * @return string HTML to output. + */ + public function view_page_buttons(view_page $viewobj) { + $output = ''; + + if (!$viewobj->quizhasquestions) { + $output .= html_writer::div( + $this->notification(get_string('noquestions', 'quiz'), 'warning', false), + 'text-left mb-3'); + } + $output .= $this->access_messages($viewobj->preventmessages); + + if ($viewobj->showbacktocourse) { + $output .= $this->single_button($viewobj->backtocourseurl, + get_string('backtocourse', 'quiz'), 'get', + array('class' => 'continuebutton')); + } + + return $output; + } + + /** + * Generates the view attempt button + * + * @param string $buttontext the label to display on the button. + * @param moodle_url $url The URL to POST to in order to start the attempt. + * @param preflight_check_form $preflightcheckform deprecated. + * @param bool $popuprequired whether the attempt needs to be opened in a pop-up. + * @param array $popupoptions the options to use if we are opening a popup. + * @return string HTML fragment. + */ + public function start_attempt_button($buttontext, moodle_url $url, + preflight_check_form $preflightcheckform = null, + $popuprequired = false, $popupoptions = null) { + + $button = new single_button($url, $buttontext, 'post', true); + $button->class .= ' quizstartbuttondiv'; + if ($popuprequired) { + $button->class .= ' quizsecuremoderequired'; + } + + $popupjsoptions = null; + if ($popuprequired && $popupoptions) { + $action = new popup_action('click', $url, 'popup', $popupoptions); + $popupjsoptions = $action->get_js_options(); + } + + if ($preflightcheckform) { + $checkform = $preflightcheckform->render(); + } else { + $checkform = null; + } + + $this->page->requires->js_call_amd('mod_quiz/preflightcheck', 'init', + array('.quizstartbuttondiv [type=submit]', get_string('startattempt', 'quiz'), + '#mod_quiz_preflight_form', $popupjsoptions)); + + return $this->render($button) . $checkform; + } + + /** + * Generate a message saying that this quiz has no questions, with a button to + * go to the edit page, if the user has the right capability. + * + * @param bool $canedit can the current user edit the quiz? + * @param moodle_url $editurl URL of the edit quiz page. + * @return string HTML to output. + * + * @deprecated since Moodle 4.0 MDL-71915 - please do not use this function any more. + */ + public function no_questions_message($canedit, $editurl) { + debugging('no_questions_message() is deprecated, please use generate_no_questions_message() instead.', DEBUG_DEVELOPER); + + $output = html_writer::start_tag('div', array('class' => 'card text-center mb-3')); + $output .= html_writer::start_tag('div', array('class' => 'card-body')); + + $output .= $this->notification(get_string('noquestions', 'quiz'), 'warning', false); + if ($canedit) { + $output .= $this->single_button($editurl, get_string('editquiz', 'quiz'), 'get'); + } + $output .= html_writer::end_tag('div'); + $output .= html_writer::end_tag('div'); + + return $output; + } + + /** + * Outputs an error message for any guests accessing the quiz + * + * @param stdClass $course the course settings row from the database. + * @param stdClass $quiz the quiz settings row from the database. + * @param stdClass $cm the course_module settings row from the database. + * @param context_module $context the quiz context. + * @param array $messages Array containing any messages + * @param view_page $viewobj + */ + public function view_page_guest($course, $quiz, $cm, $context, $messages, $viewobj) { + $output = ''; + $output .= $this->view_page_tertiary_nav($viewobj); + $output .= $this->view_information($quiz, $cm, $context, $messages); + $guestno = html_writer::tag('p', get_string('guestsno', 'quiz')); + $liketologin = html_writer::tag('p', get_string('liketologin')); + $referer = get_local_referer(false); + $output .= $this->confirm($guestno."\n\n".$liketologin."\n", get_login_url(), $referer); + return $output; + } + + /** + * Outputs and error message for anyone who is not enrolle don the course + * + * @param stdClass $course the course settings row from the database. + * @param stdClass $quiz the quiz settings row from the database. + * @param stdClass $cm the course_module settings row from the database. + * @param context_module $context the quiz context. + * @param array $messages Array containing any messages + * @param view_page $viewobj + */ + public function view_page_notenrolled($course, $quiz, $cm, $context, $messages, $viewobj) { + global $CFG; + $output = ''; + $output .= $this->view_page_tertiary_nav($viewobj); + $output .= $this->view_information($quiz, $cm, $context, $messages); + $youneedtoenrol = html_writer::tag('p', get_string('youneedtoenrol', 'quiz')); + $button = html_writer::tag('p', + $this->continue_button($CFG->wwwroot . '/course/view.php?id=' . $course->id)); + $output .= $this->box($youneedtoenrol."\n\n".$button."\n", 'generalbox', 'notice'); + return $output; + } + + /** + * Output the page information + * + * @param object $quiz the quiz settings. + * @param object $cm the course_module object. + * @param context $context the quiz context. + * @param array $messages any access messages that should be described. + * @param bool $quizhasquestions does quiz has questions added. + * @return string HTML to output. + */ + public function view_information($quiz, $cm, $context, $messages, bool $quizhasquestions = false) { + $output = ''; + + // Output any access messages. + if ($messages) { + $output .= $this->box($this->access_messages($messages), 'quizinfo'); + } + + // Show number of attempts summary to those who can view reports. + if (has_capability('mod/quiz:viewreports', $context)) { + if ($strattemptnum = $this->quiz_attempt_summary_link_to_reports($quiz, $cm, + $context)) { + $output .= html_writer::tag('div', $strattemptnum, + array('class' => 'quizattemptcounts')); + } + } + + if (has_any_capability(['mod/quiz:manageoverrides', 'mod/quiz:viewoverrides'], $context)) { + if ($overrideinfo = $this->quiz_override_summary_links($quiz, $cm)) { + $output .= html_writer::tag('div', $overrideinfo, ['class' => 'quizattemptcounts']); + } + } + + return $output; + } + + /** + * Output the quiz intro. + * @param object $quiz the quiz settings. + * @param object $cm the course_module object. + * @return string HTML to output. + */ + public function quiz_intro($quiz, $cm) { + if (html_is_blank($quiz->intro)) { + return ''; + } + + return $this->box(format_module_intro('quiz', $quiz, $cm->id), 'generalbox', 'intro'); + } + + /** + * Generates the table heading. + */ + public function view_table_heading() { + return $this->heading(get_string('summaryofattempts', 'quiz'), 3); + } + + /** + * Generates the table of data + * + * @param array $quiz Array contining quiz data + * @param int $context The page context ID + * @param view_page $viewobj + */ + public function view_table($quiz, $context, $viewobj) { + if (!$viewobj->attempts) { + return ''; + } + + // Prepare table header. + $table = new html_table(); + $table->attributes['class'] = 'generaltable quizattemptsummary'; + $table->head = array(); + $table->align = array(); + $table->size = array(); + if ($viewobj->attemptcolumn) { + $table->head[] = get_string('attemptnumber', 'quiz'); + $table->align[] = 'center'; + $table->size[] = ''; + } + $table->head[] = get_string('attemptstate', 'quiz'); + $table->align[] = 'left'; + $table->size[] = ''; + if ($viewobj->markcolumn) { + $table->head[] = get_string('marks', 'quiz') . ' / ' . + quiz_format_grade($quiz, $quiz->sumgrades); + $table->align[] = 'center'; + $table->size[] = ''; + } + if ($viewobj->gradecolumn) { + $table->head[] = get_string('gradenoun') . ' / ' . + quiz_format_grade($quiz, $quiz->grade); + $table->align[] = 'center'; + $table->size[] = ''; + } + if ($viewobj->canreviewmine) { + $table->head[] = get_string('review', 'quiz'); + $table->align[] = 'center'; + $table->size[] = ''; + } + if ($viewobj->feedbackcolumn) { + $table->head[] = get_string('feedback', 'quiz'); + $table->align[] = 'left'; + $table->size[] = ''; + } + + // One row for each attempt. + foreach ($viewobj->attemptobjs as $attemptobj) { + $attemptoptions = $attemptobj->get_display_options(true); + $row = array(); + + // Add the attempt number. + if ($viewobj->attemptcolumn) { + if ($attemptobj->is_preview()) { + $row[] = get_string('preview', 'quiz'); + } else { + $row[] = $attemptobj->get_attempt_number(); + } + } + + $row[] = $this->attempt_state($attemptobj); + + if ($viewobj->markcolumn) { + if ($attemptoptions->marks >= question_display_options::MARK_AND_MAX && + $attemptobj->is_finished()) { + $row[] = quiz_format_grade($quiz, $attemptobj->get_sum_marks()); + } else { + $row[] = ''; + } + } + + // Ouside the if because we may be showing feedback but not grades. + $attemptgrade = quiz_rescale_grade($attemptobj->get_sum_marks(), $quiz, false); + + if ($viewobj->gradecolumn) { + if ($attemptoptions->marks >= question_display_options::MARK_AND_MAX && + $attemptobj->is_finished()) { + + // Highlight the highest grade if appropriate. + if ($viewobj->overallstats && !$attemptobj->is_preview() + && $viewobj->numattempts > 1 && !is_null($viewobj->mygrade) + && $attemptobj->get_state() == quiz_attempt::FINISHED + && $attemptgrade == $viewobj->mygrade + && $quiz->grademethod == QUIZ_GRADEHIGHEST) { + $table->rowclasses[$attemptobj->get_attempt_number()] = 'bestrow'; + } + + $row[] = quiz_format_grade($quiz, $attemptgrade); + } else { + $row[] = ''; + } + } + + if ($viewobj->canreviewmine) { + $row[] = $viewobj->accessmanager->make_review_link($attemptobj->get_attempt(), + $attemptoptions, $this); + } + + if ($viewobj->feedbackcolumn && $attemptobj->is_finished()) { + if ($attemptoptions->overallfeedback) { + $row[] = quiz_feedback_for_grade($attemptgrade, $quiz, $context); + } else { + $row[] = ''; + } + } + + if ($attemptobj->is_preview()) { + $table->data['preview'] = $row; + } else { + $table->data[$attemptobj->get_attempt_number()] = $row; + } + } // End of loop over attempts. + + $output = ''; + $output .= $this->view_table_heading(); + $output .= html_writer::table($table); + return $output; + } + + /** + * Generate a brief textual desciption of the current state of an attempt. + * @param quiz_attempt $attemptobj the attempt + * @param int $timenow the time to use as 'now'. + * @return string the appropriate lang string to describe the state. + */ + public function attempt_state($attemptobj) { + switch ($attemptobj->get_state()) { + case quiz_attempt::IN_PROGRESS: + return get_string('stateinprogress', 'quiz'); + + case quiz_attempt::OVERDUE: + return get_string('stateoverdue', 'quiz') . html_writer::tag('span', + get_string('stateoverduedetails', 'quiz', + userdate($attemptobj->get_due_date())), + array('class' => 'statedetails')); + + case quiz_attempt::FINISHED: + return get_string('statefinished', 'quiz') . html_writer::tag('span', + get_string('statefinisheddetails', 'quiz', + userdate($attemptobj->get_submitted_date())), + array('class' => 'statedetails')); + + case quiz_attempt::ABANDONED: + return get_string('stateabandoned', 'quiz'); + } + } + + /** + * Generates data pertaining to quiz results + * + * @param array $quiz Array containing quiz data + * @param int $context The page context ID + * @param int $cm The Course Module Id + * @param view_page $viewobj + */ + public function view_result_info($quiz, $context, $cm, $viewobj) { + $output = ''; + if (!$viewobj->numattempts && !$viewobj->gradecolumn && is_null($viewobj->mygrade)) { + return $output; + } + $resultinfo = ''; + + if ($viewobj->overallstats) { + if ($viewobj->moreattempts) { + $a = new stdClass(); + $a->method = quiz_get_grading_option_name($quiz->grademethod); + $a->mygrade = quiz_format_grade($quiz, $viewobj->mygrade); + $a->quizgrade = quiz_format_grade($quiz, $quiz->grade); + $resultinfo .= $this->heading(get_string('gradesofar', 'quiz', $a), 3); + } else { + $a = new stdClass(); + $a->grade = quiz_format_grade($quiz, $viewobj->mygrade); + $a->maxgrade = quiz_format_grade($quiz, $quiz->grade); + $a = get_string('outofshort', 'quiz', $a); + $resultinfo .= $this->heading(get_string('yourfinalgradeis', 'quiz', $a), 3); + } + } + + if ($viewobj->mygradeoverridden) { + + $resultinfo .= html_writer::tag('p', get_string('overriddennotice', 'grades'), + array('class' => 'overriddennotice'))."\n"; + } + if ($viewobj->gradebookfeedback) { + $resultinfo .= $this->heading(get_string('comment', 'quiz'), 3); + $resultinfo .= html_writer::div($viewobj->gradebookfeedback, 'quizteacherfeedback') . "\n"; + } + if ($viewobj->feedbackcolumn) { + $resultinfo .= $this->heading(get_string('overallfeedback', 'quiz'), 3); + $resultinfo .= html_writer::div( + quiz_feedback_for_grade($viewobj->mygrade, $quiz, $context), + 'quizgradefeedback') . "\n"; + } + + if ($resultinfo) { + $output .= $this->box($resultinfo, 'generalbox', 'feedback'); + } + return $output; + } + + /** + * Output either a link to the review page for an attempt, or a button to + * open the review in a popup window. + * + * @param moodle_url $url of the target page. + * @param bool $reviewinpopup whether a pop-up is required. + * @param array $popupoptions options to pass to the popup_action constructor. + * @return string HTML to output. + */ + public function review_link($url, $reviewinpopup, $popupoptions) { + if ($reviewinpopup) { + $button = new single_button($url, get_string('review', 'quiz')); + $button->add_action(new popup_action('click', $url, 'quizpopup', $popupoptions)); + return $this->render($button); + + } else { + return html_writer::link($url, get_string('review', 'quiz'), + array('title' => get_string('reviewthisattempt', 'quiz'))); + } + } + + /** + * Displayed where there might normally be a review link, to explain why the + * review is not available at this time. + * @param string $message optional message explaining why the review is not possible. + * @return string HTML to output. + */ + public function no_review_message($message) { + return html_writer::nonempty_tag('span', $message, + array('class' => 'noreviewmessage')); + } + + /** + * Returns the same as {@link quiz_num_attempt_summary()} but wrapped in a link + * to the quiz reports. + * + * @param stdClass $quiz the quiz object. Only $quiz->id is used at the moment. + * @param stdClass $cm the cm object. Only $cm->course, $cm->groupmode and $cm->groupingid + * fields are used at the moment. + * @param context $context the quiz context. + * @param bool $returnzero if false (default), when no attempts have been made '' is returned + * instead of 'Attempts: 0'. + * @param int $currentgroup if there is a concept of current group where this method is being + * called (e.g. a report) pass it in here. Default 0 which means no current group. + * @return string HTML fragment for the link. + */ + public function quiz_attempt_summary_link_to_reports($quiz, $cm, $context, + $returnzero = false, $currentgroup = 0) { + global $CFG; + $summary = quiz_num_attempt_summary($quiz, $cm, $returnzero, $currentgroup); + if (!$summary) { + return ''; + } + + require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php'); + $url = new moodle_url('/mod/quiz/report.php', array( + 'id' => $cm->id, 'mode' => quiz_report_default_report($context))); + return html_writer::link($url, $summary); + } + + /** + * Render a summary of the number of group and user overrides, with corresponding links. + * + * @param stdClass $quiz the quiz settings. + * @param stdClass|cm_info $cm the cm object. + * @param int $currentgroup currently selected group, if there is one. + * @return string HTML fragment for the link. + */ + public function quiz_override_summary_links(stdClass $quiz, stdClass $cm, $currentgroup = 0): string { + + $baseurl = new moodle_url('/mod/quiz/overrides.php', ['cmid' => $cm->id]); + $counts = quiz_override_summary($quiz, $cm, $currentgroup); + + $links = []; + if ($counts['group']) { + $links[] = html_writer::link(new moodle_url($baseurl, ['mode' => 'group']), + get_string('overridessummarygroup', 'quiz', $counts['group'])); + } + if ($counts['user']) { + $links[] = html_writer::link(new moodle_url($baseurl, ['mode' => 'user']), + get_string('overridessummaryuser', 'quiz', $counts['user'])); + } + + if (!$links) { + return ''; + } + + $links = implode(', ', $links); + switch ($counts['mode']) { + case 'onegroup': + return get_string('overridessummarythisgroup', 'quiz', $links); + + case 'somegroups': + return get_string('overridessummaryyourgroups', 'quiz', $links); + + case 'allgroups': + return get_string('overridessummary', 'quiz', $links); + + default: + throw new coding_exception('Unexpected mode ' . $counts['mode']); + } + } + + /** + * Outputs a chart. + * + * @param \core\chart_base $chart The chart. + * @param string $title The title to display above the graph. + * @param array $attrs extra container html attributes. + * @return string HTML fragment for the graph. + */ + public function chart(\core\chart_base $chart, $title, $attrs = []) { + return $this->heading($title, 3) . html_writer::tag('div', + $this->render($chart), array_merge(['class' => 'graph'], $attrs)); + } + + /** + * Output a graph, or a message saying that GD is required. + * @param moodle_url $url the URL of the graph. + * @param string $title the title to display above the graph. + * @return string HTML fragment for the graph. + */ + public function graph(moodle_url $url, $title) { + global $CFG; + + $graph = html_writer::empty_tag('img', array('src' => $url, 'alt' => $title)); + + return $this->heading($title, 3) . html_writer::tag('div', $graph, array('class' => 'graph')); + } + + /** + * Output the connection warning messages, which are initially hidden, and + * only revealed by JavaScript if necessary. + */ + public function connection_warning() { + $options = array('filter' => false, 'newlines' => false); + $warning = format_text(get_string('connectionerror', 'quiz'), FORMAT_MARKDOWN, $options); + $ok = format_text(get_string('connectionok', 'quiz'), FORMAT_MARKDOWN, $options); + return html_writer::tag('div', $warning, + array('id' => 'connection-error', 'style' => 'display: none;', 'role' => 'alert')) . + html_writer::tag('div', $ok, array('id' => 'connection-ok', 'style' => 'display: none;', 'role' => 'alert')); + } + + + /** + * Deprecated version of render_links_to_other_attempts. + * + * @param links_to_other_attempts $links + * @return string HTML fragment. + * @deprecated since Moodle 4.2. Please use render_links_to_other_attempts instead. + * @todo MDL-76612 Final deprecation in Moodle 4.6 + */ + protected function render_mod_quiz_links_to_other_attempts(links_to_other_attempts $links) { + return $this->render_links_to_other_attempts($links); + } +} diff --git a/mod/quiz/db/renamedclasses.php b/mod/quiz/db/renamedclasses.php index fe85ebd6298..3d964cc4963 100644 --- a/mod/quiz/db/renamedclasses.php +++ b/mod/quiz/db/renamedclasses.php @@ -58,4 +58,5 @@ $renamedclasses = [ 'quiz_add_random_form' => 'mod_quiz\form\add_random_form', 'mod_quiz_links_to_other_attempts' => 'mod_quiz\output\links_to_other_attempts', 'mod_quiz_view_object' => 'mod_quiz\output\view_page', + 'mod_quiz_renderer' => 'mod_quiz\output\renderer', ]; diff --git a/mod/quiz/locallib.php b/mod/quiz/locallib.php index 17d5adbc0da..ef979eadd1e 100644 --- a/mod/quiz/locallib.php +++ b/mod/quiz/locallib.php @@ -31,7 +31,6 @@ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/mod/quiz/lib.php'); -require_once($CFG->dirroot . '/mod/quiz/renderer.php'); require_once($CFG->dirroot . '/mod/quiz/attemptlib.php'); require_once($CFG->libdir . '/completionlib.php'); require_once($CFG->libdir . '/filelib.php'); diff --git a/mod/quiz/renderer.php b/mod/quiz/renderer.php index 2ed18c39ac7..49b549d0cf8 100644 --- a/mod/quiz/renderer.php +++ b/mod/quiz/renderer.php @@ -15,1420 +15,13 @@ // along with Moodle. If not, see . /** - * Defines the renderer for the quiz module. + * File only retained to prevent fatal errors in code that tries to require/include this. * - * @package mod_quiz - * @copyright 2011 The Open University - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @todo MDL-76612 delete this file as part of Moodle 4.6 development. + * @deprecated This file is no longer required in Moodle 4.2+. */ - -defined('MOODLE_INTERNAL') || die(); - -use mod_quiz\access_manager; -use mod_quiz\form\preflight_check_form; -use mod_quiz\output\links_to_other_attempts; -use mod_quiz\question\display_options; - - -/** - * The renderer for the quiz module. - * - * @copyright 2011 The Open University - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class mod_quiz_renderer extends plugin_renderer_base { - /** - * Builds the review page - * - * @param quiz_attempt $attemptobj an instance of quiz_attempt. - * @param array $slots an array of intgers relating to questions. - * @param int $page the current page number - * @param bool $showall whether to show entire attempt on one page. - * @param bool $lastpage if true the current page is the last page. - * @param display_options $displayoptions instance of display_options. - * @param array $summarydata contains all table data - * @return $output containing html data. - */ - public function review_page(quiz_attempt $attemptobj, $slots, $page, $showall, - $lastpage, display_options $displayoptions, - $summarydata) { - - $output = ''; - $output .= $this->header(); - $output .= $this->review_summary_table($summarydata, $page); - $output .= $this->review_form($page, $showall, $displayoptions, - $this->questions($attemptobj, true, $slots, $page, $showall, $displayoptions), - $attemptobj); - - $output .= $this->review_next_navigation($attemptobj, $page, $lastpage, $showall); - $output .= $this->footer(); - return $output; - } - - /** - * Renders the review question pop-up. - * - * @param quiz_attempt $attemptobj an instance of quiz_attempt. - * @param int $slot which question to display. - * @param int $seq which step of the question attempt to show. null = latest. - * @param display_options $displayoptions instance of display_options. - * @param array $summarydata contains all table data - * @return $output containing html data. - */ - public function review_question_page(quiz_attempt $attemptobj, $slot, $seq, - display_options $displayoptions, $summarydata) { - - $output = ''; - $output .= $this->header(); - $output .= $this->review_summary_table($summarydata, 0); - - if (!is_null($seq)) { - $output .= $attemptobj->render_question_at_step($slot, $seq, true, $this); - } else { - $output .= $attemptobj->render_question($slot, true, $this); - } - - $output .= $this->close_window_button(); - $output .= $this->footer(); - return $output; - } - - /** - * Renders the review question pop-up. - * - * @param quiz_attempt $attemptobj an instance of quiz_attempt. - * @param string $message Why the review is not allowed. - * @return string html to output. - */ - public function review_question_not_allowed(quiz_attempt $attemptobj, $message) { - $output = ''; - $output .= $this->header(); - $output .= $this->heading(format_string($attemptobj->get_quiz_name(), true, - array("context" => $attemptobj->get_quizobj()->get_context()))); - $output .= $this->notification($message); - $output .= $this->close_window_button(); - $output .= $this->footer(); - return $output; - } - - /** - * Filters the summarydata array. - * - * @param array $summarydata contains row data for table - * @param int $page the current page number - * @return $summarydata containing filtered row data - */ - protected function filter_review_summary_table($summarydata, $page) { - if ($page == 0) { - return $summarydata; - } - - // Only show some of summary table on subsequent pages. - foreach ($summarydata as $key => $rowdata) { - if (!in_array($key, array('user', 'attemptlist'))) { - unset($summarydata[$key]); - } - } - - return $summarydata; - } - - /** - * Outputs the table containing data from summary data array - * - * @param array $summarydata contains row data for table - * @param int $page contains the current page number - */ - public function review_summary_table($summarydata, $page) { - $summarydata = $this->filter_review_summary_table($summarydata, $page); - if (empty($summarydata)) { - return ''; - } - - $output = ''; - $output .= html_writer::start_tag('table', array( - 'class' => 'generaltable generalbox quizreviewsummary')); - $output .= html_writer::start_tag('tbody'); - foreach ($summarydata as $rowdata) { - if ($rowdata['title'] instanceof renderable) { - $title = $this->render($rowdata['title']); - } else { - $title = $rowdata['title']; - } - - if ($rowdata['content'] instanceof renderable) { - $content = $this->render($rowdata['content']); - } else { - $content = $rowdata['content']; - } - - $output .= html_writer::tag('tr', - html_writer::tag('th', $title, array('class' => 'cell', 'scope' => 'row')) . - html_writer::tag('td', $content, array('class' => 'cell')) - ); - } - - $output .= html_writer::end_tag('tbody'); - $output .= html_writer::end_tag('table'); - return $output; - } - - /** - * Renders each question - * - * @param quiz_attempt $attemptobj instance of quiz_attempt - * @param bool $reviewing - * @param array $slots array of intgers relating to questions - * @param int $page current page number - * @param bool $showall if true shows attempt on single page - * @param display_options $displayoptions instance of display_options - */ - public function questions(quiz_attempt $attemptobj, $reviewing, $slots, $page, $showall, - display_options $displayoptions) { - $output = ''; - foreach ($slots as $slot) { - $output .= $attemptobj->render_question($slot, $reviewing, $this, - $attemptobj->review_url($slot, $page, $showall)); - } - return $output; - } - - /** - * Renders the main bit of the review page. - * - * @param array $summarydata contain row data for table - * @param int $page current page number - * @param display_options $displayoptions instance of display_options - * @param $content contains each question - * @param quiz_attempt $attemptobj instance of quiz_attempt - * @param bool $showall if true display attempt on one page - */ - public function review_form($page, $showall, $displayoptions, $content, $attemptobj) { - if ($displayoptions->flags != question_display_options::EDITABLE) { - return $content; - } - - $this->page->requires->js_init_call('M.mod_quiz.init_review_form', null, false, - quiz_get_js_module()); - - $output = ''; - $output .= html_writer::start_tag('form', array('action' => $attemptobj->review_url(null, - $page, $showall), 'method' => 'post', 'class' => 'questionflagsaveform')); - $output .= html_writer::start_tag('div'); - $output .= $content; - $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'sesskey', - 'value' => sesskey())); - $output .= html_writer::start_tag('div', array('class' => 'submitbtns')); - $output .= html_writer::empty_tag('input', array('type' => 'submit', - 'class' => 'questionflagsavebutton btn btn-secondary', 'name' => 'savingflags', - 'value' => get_string('saveflags', 'question'))); - $output .= html_writer::end_tag('div'); - $output .= html_writer::end_tag('div'); - $output .= html_writer::end_tag('form'); - - return $output; - } - - /** - * Returns either a liink or button - * - * @param quiz_attempt $attemptobj instance of quiz_attempt - */ - public function finish_review_link(quiz_attempt $attemptobj) { - $url = $attemptobj->view_url(); - - if ($attemptobj->get_access_manager(time())->attempt_must_be_in_popup()) { - $this->page->requires->js_init_call('M.mod_quiz.secure_window.init_close_button', - array($url), false, quiz_get_js_module()); - return html_writer::empty_tag('input', array('type' => 'button', - 'value' => get_string('finishreview', 'quiz'), - 'id' => 'secureclosebutton', - 'class' => 'mod_quiz-next-nav btn btn-primary')); - - } else { - return html_writer::link($url, get_string('finishreview', 'quiz'), - array('class' => 'mod_quiz-next-nav')); - } - } - - /** - * Creates the navigation links/buttons at the bottom of the reivew attempt page. - * - * Note, the name of this function is no longer accurate, but when the design - * changed, it was decided to keep the old name for backwards compatibility. - * - * @param quiz_attempt $attemptobj instance of quiz_attempt - * @param int $page the current page - * @param bool $lastpage if true current page is the last page - * @param bool|null $showall if true, the URL will be to review the entire attempt on one page, - * and $page will be ignored. If null, a sensible default will be chosen. - * - * @return string HTML fragment. - */ - public function review_next_navigation(quiz_attempt $attemptobj, $page, $lastpage, $showall = null) { - $nav = ''; - if ($page > 0) { - $nav .= link_arrow_left(get_string('navigateprevious', 'quiz'), - $attemptobj->review_url(null, $page - 1, $showall), false, 'mod_quiz-prev-nav'); - } - if ($lastpage) { - $nav .= $this->finish_review_link($attemptobj); - } else { - $nav .= link_arrow_right(get_string('navigatenext', 'quiz'), - $attemptobj->review_url(null, $page + 1, $showall), false, 'mod_quiz-next-nav'); - } - return html_writer::tag('div', $nav, array('class' => 'submitbtns')); - } - - /** - * Return the HTML of the quiz timer. - * @return string HTML content. - */ - public function countdown_timer(quiz_attempt $attemptobj, $timenow) { - - $timeleft = $attemptobj->get_time_left_display($timenow); - if ($timeleft !== false) { - $ispreview = $attemptobj->is_preview(); - $timerstartvalue = $timeleft; - if (!$ispreview) { - // Make sure the timer starts just above zero. If $timeleft was <= 0, then - // this will just have the effect of causing the quiz to be submitted immediately. - $timerstartvalue = max($timerstartvalue, 1); - } - $this->initialise_timer($timerstartvalue, $ispreview); - } - - - return $this->output->render_from_template('mod_quiz/timer', (object)[]); - } - - /** - * Create a preview link - * - * @param moodle_url $url contains a url to the given page - */ - public function restart_preview_button($url) { - return $this->single_button($url, get_string('startnewpreview', 'quiz')); - } - - /** - * Outputs the navigation block panel - * - * @param quiz_nav_panel_base $panel instance of quiz_nav_panel_base - */ - public function navigation_panel(quiz_nav_panel_base $panel) { - - $output = ''; - $userpicture = $panel->user_picture(); - if ($userpicture) { - $fullname = fullname($userpicture->user); - if ($userpicture->size === true) { - $fullname = html_writer::div($fullname); - } - $output .= html_writer::tag('div', $this->render($userpicture) . $fullname, - array('id' => 'user-picture', 'class' => 'clearfix')); - } - $output .= $panel->render_before_button_bits($this); - - $bcc = $panel->get_button_container_class(); - $output .= html_writer::start_tag('div', array('class' => "qn_buttons clearfix $bcc")); - foreach ($panel->get_question_buttons() as $button) { - $output .= $this->render($button); - } - $output .= html_writer::end_tag('div'); - - $output .= html_writer::tag('div', $panel->render_end_bits($this), - array('class' => 'othernav')); - - $this->page->requires->js_init_call('M.mod_quiz.nav.init', null, false, - quiz_get_js_module()); - - return $output; - } - - /** - * Display a quiz navigation button. - * - * @param quiz_nav_question_button $button - * @return string HTML fragment. - */ - protected function render_quiz_nav_question_button(quiz_nav_question_button $button) { - $classes = array('qnbutton', $button->stateclass, $button->navmethod, 'btn'); - $extrainfo = array(); - - if ($button->currentpage) { - $classes[] = 'thispage'; - $extrainfo[] = get_string('onthispage', 'quiz'); - } - - // Flagged? - if ($button->flagged) { - $classes[] = 'flagged'; - $flaglabel = get_string('flagged', 'question'); - } else { - $flaglabel = ''; - } - $extrainfo[] = html_writer::tag('span', $flaglabel, array('class' => 'flagstate')); - - if (is_numeric($button->number)) { - $qnostring = 'questionnonav'; - } else { - $qnostring = 'questionnonavinfo'; - } - - $a = new stdClass(); - $a->number = $button->number; - $a->attributes = implode(' ', $extrainfo); - $tagcontents = html_writer::tag('span', '', array('class' => 'thispageholder')) . - html_writer::tag('span', '', array('class' => 'trafficlight')) . - get_string($qnostring, 'quiz', $a); - $tagattributes = array('class' => implode(' ', $classes), 'id' => $button->id, - 'title' => $button->statestring, 'data-quiz-page' => $button->page); - - if ($button->url) { - return html_writer::link($button->url, $tagcontents, $tagattributes); - } else { - return html_writer::tag('span', $tagcontents, $tagattributes); - } - } - - /** - * Display a quiz navigation heading. - * - * @param quiz_nav_section_heading $heading the heading. - * @return string HTML fragment. - */ - protected function render_quiz_nav_section_heading(quiz_nav_section_heading $heading) { - if (empty($heading->heading)) { - $headingtext = get_string('sectionnoname', 'quiz'); - $class = ' dimmed_text'; - } else { - $headingtext = $heading->heading; - $class = ''; - } - return $this->heading($headingtext, 3, 'mod_quiz-section-heading' . $class); - } - - /** - * Renders a list of links the other attempts. - * - * @param links_to_other_attempts $links - * @return string HTML fragment. - */ - protected function render_links_to_other_attempts( - links_to_other_attempts $links) { - $attemptlinks = array(); - foreach ($links->links as $attempt => $url) { - if (!$url) { - $attemptlinks[] = html_writer::tag('strong', $attempt); - } else if ($url instanceof renderable) { - $attemptlinks[] = $this->render($url); - } else { - $attemptlinks[] = html_writer::link($url, $attempt); - } - } - return implode(', ', $attemptlinks); - } - - /** - * Render the 'start attempt' page. - * - * The student gets here if their interaction with the preflight check - * from fails in some way (e.g. they typed the wrong password). - * - * @param quiz $quizobj - * @param preflight_check_form $mform - * @return string - */ - public function start_attempt_page(quiz $quizobj, preflight_check_form $mform) { - $output = ''; - $output .= $this->header(); - $output .= $this->during_attempt_tertiary_nav($quizobj->view_url()); - $output .= $this->heading(format_string($quizobj->get_quiz_name(), true, - array("context" => $quizobj->get_context()))); - $output .= $this->quiz_intro($quizobj->get_quiz(), $quizobj->get_cm()); - $output .= $mform->render(); - $output .= $this->footer(); - return $output; - } - - /** - * Attempt Page - * - * @param quiz_attempt $attemptobj Instance of quiz_attempt - * @param int $page Current page number - * @param access_manager $accessmanager Instance of access_manager - * @param array $messages An array of messages - * @param array $slots Contains an array of integers that relate to questions - * @param int $id The ID of an attempt - * @param int $nextpage The number of the next page - * @return string HTML to output. - */ - public function attempt_page($attemptobj, $page, $accessmanager, $messages, $slots, $id, - $nextpage) { - $output = ''; - $output .= $this->header(); - $output .= $this->during_attempt_tertiary_nav($attemptobj->view_url()); - $output .= $this->quiz_notices($messages); - $output .= $this->countdown_timer($attemptobj, time()); - $output .= $this->attempt_form($attemptobj, $page, $slots, $id, $nextpage); - $output .= $this->footer(); - return $output; - } - - /** - * Render the tertiary navigation for pages during the attempt. - * - * @param string|moodle_url $quizviewurl url of the view.php page for this quiz. - * @return string HTML to output. - */ - public function during_attempt_tertiary_nav($quizviewurl): string { - $output = ''; - $output .= html_writer::start_div('container-fluid tertiary-navigation'); - $output .= html_writer::start_div('row'); - $output .= html_writer::start_div('navitem'); - $output .= html_writer::link($quizviewurl, get_string('back'), - ['class' => 'btn btn-secondary']); - $output .= html_writer::end_div(); - $output .= html_writer::end_div(); - $output .= html_writer::end_div(); - return $output; - } - - /** - * Returns any notices. - * - * @param array $messages - */ - public function quiz_notices($messages) { - if (!$messages) { - return ''; - } - return $this->notification( - html_writer::tag('p', get_string('accessnoticesheader', 'quiz')) . $this->access_messages($messages), - 'warning', - false - ); - } - - /** - * Ouputs the form for making an attempt - * - * @param quiz_attempt $attemptobj - * @param int $page Current page number - * @param array $slots Array of integers relating to questions - * @param int $id ID of the attempt - * @param int $nextpage Next page number - */ - public function attempt_form($attemptobj, $page, $slots, $id, $nextpage) { - $output = ''; - - // Start the form. - $output .= html_writer::start_tag('form', - array('action' => new moodle_url($attemptobj->processattempt_url(), - array('cmid' => $attemptobj->get_cmid())), 'method' => 'post', - 'enctype' => 'multipart/form-data', 'accept-charset' => 'utf-8', - 'id' => 'responseform')); - $output .= html_writer::start_tag('div'); - - // Print all the questions. - foreach ($slots as $slot) { - $output .= $attemptobj->render_question($slot, false, $this, - $attemptobj->attempt_url($slot, $page), $this); - } - - $navmethod = $attemptobj->get_quiz()->navmethod; - $output .= $this->attempt_navigation_buttons($page, $attemptobj->is_last_page($page), $navmethod); - - // Some hidden fields to trach what is going on. - $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'attempt', - 'value' => $attemptobj->get_attemptid())); - $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'thispage', - 'value' => $page, 'id' => 'followingpage')); - $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'nextpage', - 'value' => $nextpage)); - $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'timeup', - 'value' => '0', 'id' => 'timeup')); - $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'sesskey', - 'value' => sesskey())); - $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'scrollpos', - 'value' => '', 'id' => 'scrollpos')); - - // Add a hidden field with questionids. Do this at the end of the form, so - // if you navigate before the form has finished loading, it does not wipe all - // the student's answers. - $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'slots', - 'value' => implode(',', $attemptobj->get_active_slots($page)))); - - // Finish the form. - $output .= html_writer::end_tag('div'); - $output .= html_writer::end_tag('form'); - - $output .= $this->connection_warning(); - - return $output; - } - - /** - * Display the prev/next buttons that go at the bottom of each page of the attempt. - * - * @param int $page the page number. Starts at 0 for the first page. - * @param bool $lastpage is this the last page in the quiz? - * @param string $navmethod Optional quiz attribute, 'free' (default) or 'sequential' - * @return string HTML fragment. - */ - protected function attempt_navigation_buttons($page, $lastpage, $navmethod = 'free') { - $output = ''; - - $output .= html_writer::start_tag('div', array('class' => 'submitbtns')); - if ($page > 0 && $navmethod == 'free') { - $output .= html_writer::empty_tag('input', array('type' => 'submit', 'name' => 'previous', - 'value' => get_string('navigateprevious', 'quiz'), 'class' => 'mod_quiz-prev-nav btn btn-secondary', - 'id' => 'mod_quiz-prev-nav')); - $this->page->requires->js_call_amd('core_form/submit', 'init', ['mod_quiz-prev-nav']); - } - if ($lastpage) { - $nextlabel = get_string('endtest', 'quiz'); - } else { - $nextlabel = get_string('navigatenext', 'quiz'); - } - $output .= html_writer::empty_tag('input', array('type' => 'submit', 'name' => 'next', - 'value' => $nextlabel, 'class' => 'mod_quiz-next-nav btn btn-primary', 'id' => 'mod_quiz-next-nav')); - $output .= html_writer::end_tag('div'); - $this->page->requires->js_call_amd('core_form/submit', 'init', ['mod_quiz-next-nav']); - - return $output; - } - - /** - * Render a button which allows students to redo a question in the attempt. - * - * @param int $slot the number of the slot to generate the button for. - * @param bool $disabled if true, output the button disabled. - * @return string HTML fragment. - */ - public function redo_question_button($slot, $disabled) { - $attributes = array('type' => 'submit', 'name' => 'redoslot' . $slot, - 'value' => get_string('redoquestion', 'quiz'), - 'class' => 'mod_quiz-redo_question_button btn btn-secondary'); - if ($disabled) { - $attributes['disabled'] = 'disabled'; - } - return html_writer::div(html_writer::empty_tag('input', $attributes)); - } - - /** - * Output the JavaScript required to initialise the countdown timer. - * @param int $timerstartvalue time remaining, in seconds. - */ - public function initialise_timer($timerstartvalue, $ispreview) { - $options = array($timerstartvalue, (bool)$ispreview); - $this->page->requires->js_init_call('M.mod_quiz.timer.init', $options, false, quiz_get_js_module()); - } - - /** - * Output a page with an optional message, and JavaScript code to close the - * current window and redirect the parent window to a new URL. - * @param moodle_url $url the URL to redirect the parent window to. - * @param string $message message to display before closing the window. (optional) - * @return string HTML to output. - */ - public function close_attempt_popup($url, $message = '') { - $output = ''; - $output .= $this->header(); - $output .= $this->box_start(); - - if ($message) { - $output .= html_writer::tag('p', $message); - $output .= html_writer::tag('p', get_string('windowclosing', 'quiz')); - $delay = 5; - } else { - $output .= html_writer::tag('p', get_string('pleaseclose', 'quiz')); - $delay = 0; - } - $this->page->requires->js_init_call('M.mod_quiz.secure_window.close', - array($url, $delay), false, quiz_get_js_module()); - - $output .= $this->box_end(); - $output .= $this->footer(); - return $output; - } - - /** - * Print each message in an array, surrounded by <p>, </p> tags. - * - * @param array $messages the array of message strings. - * @param bool $return if true, return a string, instead of outputting. - * - * @return string HTML to output. - */ - public function access_messages($messages) { - $output = ''; - foreach ($messages as $message) { - $output .= html_writer::tag('p', $message, ['class' => 'text-left']); - } - return $output; - } - - /* - * Summary Page - */ - /** - * Create the summary page - * - * @param quiz_attempt $attemptobj - * @param display_options $displayoptions - */ - public function summary_page($attemptobj, $displayoptions) { - $output = ''; - $output .= $this->header(); - $output .= $this->during_attempt_tertiary_nav($attemptobj->view_url()); - $output .= $this->heading(format_string($attemptobj->get_quiz_name())); - $output .= $this->heading(get_string('summaryofattempt', 'quiz'), 3); - $output .= $this->summary_table($attemptobj, $displayoptions); - $output .= $this->summary_page_controls($attemptobj); - $output .= $this->footer(); - return $output; - } - - /** - * Generates the table of summarydata - * - * @param quiz_attempt $attemptobj - * @param display_options $displayoptions - */ - public function summary_table($attemptobj, $displayoptions) { - // Prepare the summary table header. - $table = new html_table(); - $table->attributes['class'] = 'generaltable quizsummaryofattempt boxaligncenter'; - $table->head = array(get_string('question', 'quiz'), get_string('status', 'quiz')); - $table->align = array('left', 'left'); - $table->size = array('', ''); - $markscolumn = $displayoptions->marks >= question_display_options::MARK_AND_MAX; - if ($markscolumn) { - $table->head[] = get_string('marks', 'quiz'); - $table->align[] = 'left'; - $table->size[] = ''; - } - $tablewidth = count($table->align); - $table->data = array(); - - // Get the summary info for each question. - $slots = $attemptobj->get_slots(); - foreach ($slots as $slot) { - // Add a section headings if we need one here. - $heading = $attemptobj->get_heading_before_slot($slot); - if ($heading !== null) { - // There is a heading here. - $rowclasses = 'quizsummaryheading'; - if ($heading) { - $heading = format_string($heading); - } else if (count($attemptobj->get_quizobj()->get_sections()) > 1) { - // If this is the start of an unnamed section, and the quiz has more - // than one section, then add a default heading. - $heading = get_string('sectionnoname', 'quiz'); - $rowclasses .= ' dimmed_text'; - } - $cell = new html_table_cell(format_string($heading)); - $cell->header = true; - $cell->colspan = $tablewidth; - $table->data[] = array($cell); - $table->rowclasses[] = $rowclasses; - } - - // Don't display information items. - if (!$attemptobj->is_real_question($slot)) { - continue; - } - - // Real question, show it. - $flag = ''; - if ($attemptobj->is_question_flagged($slot)) { - // Quiz has custom JS manipulating these image tags - so we can't use the pix_icon method here. - $flag = html_writer::empty_tag('img', array('src' => $this->image_url('i/flagged'), - 'alt' => get_string('flagged', 'question'), 'class' => 'questionflag icon-post')); - } - if ($attemptobj->can_navigate_to($slot)) { - $row = array(html_writer::link($attemptobj->attempt_url($slot), - $attemptobj->get_question_number($slot) . $flag), - $attemptobj->get_question_status($slot, $displayoptions->correctness)); - } else { - $row = array($attemptobj->get_question_number($slot) . $flag, - $attemptobj->get_question_status($slot, $displayoptions->correctness)); - } - if ($markscolumn) { - $row[] = $attemptobj->get_question_mark($slot); - } - $table->data[] = $row; - $table->rowclasses[] = 'quizsummary' . $slot . ' ' . $attemptobj->get_question_state_class( - $slot, $displayoptions->correctness); - } - - // Print the summary table. - $output = html_writer::table($table); - - return $output; - } - - /** - * Creates any controls a the page should have. - * - * @param quiz_attempt $attemptobj - */ - public function summary_page_controls($attemptobj) { - $output = ''; - - // Return to place button. - if ($attemptobj->get_state() == quiz_attempt::IN_PROGRESS) { - $button = new single_button( - new moodle_url($attemptobj->attempt_url(null, $attemptobj->get_currentpage())), - get_string('returnattempt', 'quiz')); - $output .= $this->container($this->container($this->render($button), - 'controls'), 'submitbtns mdl-align'); - } - - // Finish attempt button. - $options = array( - 'attempt' => $attemptobj->get_attemptid(), - 'finishattempt' => 1, - 'timeup' => 0, - 'slots' => '', - 'cmid' => $attemptobj->get_cmid(), - 'sesskey' => sesskey(), - ); - - $button = new single_button( - new moodle_url($attemptobj->processattempt_url(), $options), - get_string('submitallandfinish', 'quiz')); - $button->id = 'responseform'; - $button->class = 'btn-finishattempt'; - $button->formid = 'frm-finishattempt'; - if ($attemptobj->get_state() == quiz_attempt::IN_PROGRESS) { - $totalunanswered = 0; - if ($attemptobj->get_quiz()->navmethod == 'free') { - // Only count the unanswered question if the navigation method is set to free. - $totalunanswered = $attemptobj->get_number_of_unanswered_questions(); - } - $this->page->requires->js_call_amd('mod_quiz/submission_confirmation', 'init', [$totalunanswered]); - } - $button->primary = true; - - $duedate = $attemptobj->get_due_date(); - $message = ''; - if ($attemptobj->get_state() == quiz_attempt::OVERDUE) { - $message = get_string('overduemustbesubmittedby', 'quiz', userdate($duedate)); - - } else if ($duedate) { - $message = get_string('mustbesubmittedby', 'quiz', userdate($duedate)); - } - - $output .= $this->countdown_timer($attemptobj, time()); - $output .= $this->container($message . $this->container( - $this->render($button), 'controls'), 'submitbtns mdl-align'); - - return $output; - } - - /* - * View Page - */ - /** - * Generates the view page - * - * @param stdClass $course the course settings row from the database. - * @param stdClass $quiz the quiz settings row from the database. - * @param stdClass $cm the course_module settings row from the database. - * @param context_module $context the quiz context. - * @param view_page $viewobj - * @return string HTML to display - */ - public function view_page($course, $quiz, $cm, $context, $viewobj) { - $output = ''; - - $output .= $this->view_page_tertiary_nav($viewobj); - $output .= $this->view_information($quiz, $cm, $context, $viewobj->infomessages); - $output .= $this->view_table($quiz, $context, $viewobj); - $output .= $this->view_result_info($quiz, $context, $cm, $viewobj); - $output .= $this->box($this->view_page_buttons($viewobj), 'quizattempt'); - return $output; - } - - /** - * Render the tertiary navigation for the view page. - * - * @param view_page $viewobj the information required to display the view page. - * @return string HTML to output. - */ - public function view_page_tertiary_nav(view_page $viewobj): string { - $content = ''; - - if ($viewobj->buttontext) { - $attemptbtn = $this->start_attempt_button($viewobj->buttontext, - $viewobj->startattempturl, $viewobj->preflightcheckform, - $viewobj->popuprequired, $viewobj->popupoptions); - $content .= $attemptbtn; - } - - if ($viewobj->canedit && !$viewobj->quizhasquestions) { - $content .= html_writer::link($viewobj->editurl, get_string('addquestion', 'quiz'), - ['class' => 'btn btn-secondary']); - } - - if ($content) { - return html_writer::div(html_writer::div($content, 'row'), 'container-fluid tertiary-navigation'); - } else { - return ''; - } - } - - /** - * Work out, and render, whatever buttons, and surrounding info, should appear - * at the end of the review page. - * - * @param view_page $viewobj the information required to display the view page. - * @return string HTML to output. - */ - public function view_page_buttons(view_page $viewobj) { - $output = ''; - - if (!$viewobj->quizhasquestions) { - $output .= html_writer::div( - $this->notification(get_string('noquestions', 'quiz'), 'warning', false), - 'text-left mb-3'); - } - $output .= $this->access_messages($viewobj->preventmessages); - - if ($viewobj->showbacktocourse) { - $output .= $this->single_button($viewobj->backtocourseurl, - get_string('backtocourse', 'quiz'), 'get', - array('class' => 'continuebutton')); - } - - return $output; - } - - /** - * Generates the view attempt button - * - * @param string $buttontext the label to display on the button. - * @param moodle_url $url The URL to POST to in order to start the attempt. - * @param preflight_check_form $preflightcheckform deprecated. - * @param bool $popuprequired whether the attempt needs to be opened in a pop-up. - * @param array $popupoptions the options to use if we are opening a popup. - * @return string HTML fragment. - */ - public function start_attempt_button($buttontext, moodle_url $url, - preflight_check_form $preflightcheckform = null, - $popuprequired = false, $popupoptions = null) { - - $button = new single_button($url, $buttontext, 'post', true); - $button->class .= ' quizstartbuttondiv'; - if ($popuprequired) { - $button->class .= ' quizsecuremoderequired'; - } - - $popupjsoptions = null; - if ($popuprequired && $popupoptions) { - $action = new popup_action('click', $url, 'popup', $popupoptions); - $popupjsoptions = $action->get_js_options(); - } - - if ($preflightcheckform) { - $checkform = $preflightcheckform->render(); - } else { - $checkform = null; - } - - $this->page->requires->js_call_amd('mod_quiz/preflightcheck', 'init', - array('.quizstartbuttondiv [type=submit]', get_string('startattempt', 'quiz'), - '#mod_quiz_preflight_form', $popupjsoptions)); - - return $this->render($button) . $checkform; - } - - /** - * Generate a message saying that this quiz has no questions, with a button to - * go to the edit page, if the user has the right capability. - * - * @param bool $canedit can the current user edit the quiz? - * @param moodle_url $editurl URL of the edit quiz page. - * @return string HTML to output. - * - * @deprecated since Moodle 4.0 MDL-71915 - please do not use this function any more. - */ - public function no_questions_message($canedit, $editurl) { - debugging('no_questions_message() is deprecated, please use generate_no_questions_message() instead.', DEBUG_DEVELOPER); - - $output = html_writer::start_tag('div', array('class' => 'card text-center mb-3')); - $output .= html_writer::start_tag('div', array('class' => 'card-body')); - - $output .= $this->notification(get_string('noquestions', 'quiz'), 'warning', false); - if ($canedit) { - $output .= $this->single_button($editurl, get_string('editquiz', 'quiz'), 'get'); - } - $output .= html_writer::end_tag('div'); - $output .= html_writer::end_tag('div'); - - return $output; - } - - /** - * Outputs an error message for any guests accessing the quiz - * - * @param stdClass $course the course settings row from the database. - * @param stdClass $quiz the quiz settings row from the database. - * @param stdClass $cm the course_module settings row from the database. - * @param context_module $context the quiz context. - * @param array $messages Array containing any messages - * @param view_page $viewobj - */ - public function view_page_guest($course, $quiz, $cm, $context, $messages, $viewobj) { - $output = ''; - $output .= $this->view_page_tertiary_nav($viewobj); - $output .= $this->view_information($quiz, $cm, $context, $messages); - $guestno = html_writer::tag('p', get_string('guestsno', 'quiz')); - $liketologin = html_writer::tag('p', get_string('liketologin')); - $referer = get_local_referer(false); - $output .= $this->confirm($guestno."\n\n".$liketologin."\n", get_login_url(), $referer); - return $output; - } - - /** - * Outputs and error message for anyone who is not enrolle don the course - * - * @param stdClass $course the course settings row from the database. - * @param stdClass $quiz the quiz settings row from the database. - * @param stdClass $cm the course_module settings row from the database. - * @param context_module $context the quiz context. - * @param array $messages Array containing any messages - * @param view_page $viewobj - */ - public function view_page_notenrolled($course, $quiz, $cm, $context, $messages, $viewobj) { - global $CFG; - $output = ''; - $output .= $this->view_page_tertiary_nav($viewobj); - $output .= $this->view_information($quiz, $cm, $context, $messages); - $youneedtoenrol = html_writer::tag('p', get_string('youneedtoenrol', 'quiz')); - $button = html_writer::tag('p', - $this->continue_button($CFG->wwwroot . '/course/view.php?id=' . $course->id)); - $output .= $this->box($youneedtoenrol."\n\n".$button."\n", 'generalbox', 'notice'); - return $output; - } - - /** - * Output the page information - * - * @param object $quiz the quiz settings. - * @param object $cm the course_module object. - * @param context $context the quiz context. - * @param array $messages any access messages that should be described. - * @param bool $quizhasquestions does quiz has questions added. - * @return string HTML to output. - */ - public function view_information($quiz, $cm, $context, $messages, bool $quizhasquestions = false) { - $output = ''; - - // Output any access messages. - if ($messages) { - $output .= $this->box($this->access_messages($messages), 'quizinfo'); - } - - // Show number of attempts summary to those who can view reports. - if (has_capability('mod/quiz:viewreports', $context)) { - if ($strattemptnum = $this->quiz_attempt_summary_link_to_reports($quiz, $cm, - $context)) { - $output .= html_writer::tag('div', $strattemptnum, - array('class' => 'quizattemptcounts')); - } - } - - if (has_any_capability(['mod/quiz:manageoverrides', 'mod/quiz:viewoverrides'], $context)) { - if ($overrideinfo = $this->quiz_override_summary_links($quiz, $cm)) { - $output .= html_writer::tag('div', $overrideinfo, ['class' => 'quizattemptcounts']); - } - } - - return $output; - } - - /** - * Output the quiz intro. - * @param object $quiz the quiz settings. - * @param object $cm the course_module object. - * @return string HTML to output. - */ - public function quiz_intro($quiz, $cm) { - if (html_is_blank($quiz->intro)) { - return ''; - } - - return $this->box(format_module_intro('quiz', $quiz, $cm->id), 'generalbox', 'intro'); - } - - /** - * Generates the table heading. - */ - public function view_table_heading() { - return $this->heading(get_string('summaryofattempts', 'quiz'), 3); - } - - /** - * Generates the table of data - * - * @param array $quiz Array contining quiz data - * @param int $context The page context ID - * @param view_page $viewobj - */ - public function view_table($quiz, $context, $viewobj) { - if (!$viewobj->attempts) { - return ''; - } - - // Prepare table header. - $table = new html_table(); - $table->attributes['class'] = 'generaltable quizattemptsummary'; - $table->head = array(); - $table->align = array(); - $table->size = array(); - if ($viewobj->attemptcolumn) { - $table->head[] = get_string('attemptnumber', 'quiz'); - $table->align[] = 'center'; - $table->size[] = ''; - } - $table->head[] = get_string('attemptstate', 'quiz'); - $table->align[] = 'left'; - $table->size[] = ''; - if ($viewobj->markcolumn) { - $table->head[] = get_string('marks', 'quiz') . ' / ' . - quiz_format_grade($quiz, $quiz->sumgrades); - $table->align[] = 'center'; - $table->size[] = ''; - } - if ($viewobj->gradecolumn) { - $table->head[] = get_string('gradenoun') . ' / ' . - quiz_format_grade($quiz, $quiz->grade); - $table->align[] = 'center'; - $table->size[] = ''; - } - if ($viewobj->canreviewmine) { - $table->head[] = get_string('review', 'quiz'); - $table->align[] = 'center'; - $table->size[] = ''; - } - if ($viewobj->feedbackcolumn) { - $table->head[] = get_string('feedback', 'quiz'); - $table->align[] = 'left'; - $table->size[] = ''; - } - - // One row for each attempt. - foreach ($viewobj->attemptobjs as $attemptobj) { - $attemptoptions = $attemptobj->get_display_options(true); - $row = array(); - - // Add the attempt number. - if ($viewobj->attemptcolumn) { - if ($attemptobj->is_preview()) { - $row[] = get_string('preview', 'quiz'); - } else { - $row[] = $attemptobj->get_attempt_number(); - } - } - - $row[] = $this->attempt_state($attemptobj); - - if ($viewobj->markcolumn) { - if ($attemptoptions->marks >= question_display_options::MARK_AND_MAX && - $attemptobj->is_finished()) { - $row[] = quiz_format_grade($quiz, $attemptobj->get_sum_marks()); - } else { - $row[] = ''; - } - } - - // Ouside the if because we may be showing feedback but not grades. - $attemptgrade = quiz_rescale_grade($attemptobj->get_sum_marks(), $quiz, false); - - if ($viewobj->gradecolumn) { - if ($attemptoptions->marks >= question_display_options::MARK_AND_MAX && - $attemptobj->is_finished()) { - - // Highlight the highest grade if appropriate. - if ($viewobj->overallstats && !$attemptobj->is_preview() - && $viewobj->numattempts > 1 && !is_null($viewobj->mygrade) - && $attemptobj->get_state() == quiz_attempt::FINISHED - && $attemptgrade == $viewobj->mygrade - && $quiz->grademethod == QUIZ_GRADEHIGHEST) { - $table->rowclasses[$attemptobj->get_attempt_number()] = 'bestrow'; - } - - $row[] = quiz_format_grade($quiz, $attemptgrade); - } else { - $row[] = ''; - } - } - - if ($viewobj->canreviewmine) { - $row[] = $viewobj->accessmanager->make_review_link($attemptobj->get_attempt(), - $attemptoptions, $this); - } - - if ($viewobj->feedbackcolumn && $attemptobj->is_finished()) { - if ($attemptoptions->overallfeedback) { - $row[] = quiz_feedback_for_grade($attemptgrade, $quiz, $context); - } else { - $row[] = ''; - } - } - - if ($attemptobj->is_preview()) { - $table->data['preview'] = $row; - } else { - $table->data[$attemptobj->get_attempt_number()] = $row; - } - } // End of loop over attempts. - - $output = ''; - $output .= $this->view_table_heading(); - $output .= html_writer::table($table); - return $output; - } - - /** - * Generate a brief textual desciption of the current state of an attempt. - * @param quiz_attempt $attemptobj the attempt - * @param int $timenow the time to use as 'now'. - * @return string the appropriate lang string to describe the state. - */ - public function attempt_state($attemptobj) { - switch ($attemptobj->get_state()) { - case quiz_attempt::IN_PROGRESS: - return get_string('stateinprogress', 'quiz'); - - case quiz_attempt::OVERDUE: - return get_string('stateoverdue', 'quiz') . html_writer::tag('span', - get_string('stateoverduedetails', 'quiz', - userdate($attemptobj->get_due_date())), - array('class' => 'statedetails')); - - case quiz_attempt::FINISHED: - return get_string('statefinished', 'quiz') . html_writer::tag('span', - get_string('statefinisheddetails', 'quiz', - userdate($attemptobj->get_submitted_date())), - array('class' => 'statedetails')); - - case quiz_attempt::ABANDONED: - return get_string('stateabandoned', 'quiz'); - } - } - - /** - * Generates data pertaining to quiz results - * - * @param array $quiz Array containing quiz data - * @param int $context The page context ID - * @param int $cm The Course Module Id - * @param view_page $viewobj - */ - public function view_result_info($quiz, $context, $cm, $viewobj) { - $output = ''; - if (!$viewobj->numattempts && !$viewobj->gradecolumn && is_null($viewobj->mygrade)) { - return $output; - } - $resultinfo = ''; - - if ($viewobj->overallstats) { - if ($viewobj->moreattempts) { - $a = new stdClass(); - $a->method = quiz_get_grading_option_name($quiz->grademethod); - $a->mygrade = quiz_format_grade($quiz, $viewobj->mygrade); - $a->quizgrade = quiz_format_grade($quiz, $quiz->grade); - $resultinfo .= $this->heading(get_string('gradesofar', 'quiz', $a), 3); - } else { - $a = new stdClass(); - $a->grade = quiz_format_grade($quiz, $viewobj->mygrade); - $a->maxgrade = quiz_format_grade($quiz, $quiz->grade); - $a = get_string('outofshort', 'quiz', $a); - $resultinfo .= $this->heading(get_string('yourfinalgradeis', 'quiz', $a), 3); - } - } - - if ($viewobj->mygradeoverridden) { - - $resultinfo .= html_writer::tag('p', get_string('overriddennotice', 'grades'), - array('class' => 'overriddennotice'))."\n"; - } - if ($viewobj->gradebookfeedback) { - $resultinfo .= $this->heading(get_string('comment', 'quiz'), 3); - $resultinfo .= html_writer::div($viewobj->gradebookfeedback, 'quizteacherfeedback') . "\n"; - } - if ($viewobj->feedbackcolumn) { - $resultinfo .= $this->heading(get_string('overallfeedback', 'quiz'), 3); - $resultinfo .= html_writer::div( - quiz_feedback_for_grade($viewobj->mygrade, $quiz, $context), - 'quizgradefeedback') . "\n"; - } - - if ($resultinfo) { - $output .= $this->box($resultinfo, 'generalbox', 'feedback'); - } - return $output; - } - - /** - * Output either a link to the review page for an attempt, or a button to - * open the review in a popup window. - * - * @param moodle_url $url of the target page. - * @param bool $reviewinpopup whether a pop-up is required. - * @param array $popupoptions options to pass to the popup_action constructor. - * @return string HTML to output. - */ - public function review_link($url, $reviewinpopup, $popupoptions) { - if ($reviewinpopup) { - $button = new single_button($url, get_string('review', 'quiz')); - $button->add_action(new popup_action('click', $url, 'quizpopup', $popupoptions)); - return $this->render($button); - - } else { - return html_writer::link($url, get_string('review', 'quiz'), - array('title' => get_string('reviewthisattempt', 'quiz'))); - } - } - - /** - * Displayed where there might normally be a review link, to explain why the - * review is not available at this time. - * @param string $message optional message explaining why the review is not possible. - * @return string HTML to output. - */ - public function no_review_message($message) { - return html_writer::nonempty_tag('span', $message, - array('class' => 'noreviewmessage')); - } - - /** - * Returns the same as {@link quiz_num_attempt_summary()} but wrapped in a link - * to the quiz reports. - * - * @param stdClass $quiz the quiz object. Only $quiz->id is used at the moment. - * @param stdClass $cm the cm object. Only $cm->course, $cm->groupmode and $cm->groupingid - * fields are used at the moment. - * @param context $context the quiz context. - * @param bool $returnzero if false (default), when no attempts have been made '' is returned - * instead of 'Attempts: 0'. - * @param int $currentgroup if there is a concept of current group where this method is being - * called (e.g. a report) pass it in here. Default 0 which means no current group. - * @return string HTML fragment for the link. - */ - public function quiz_attempt_summary_link_to_reports($quiz, $cm, $context, - $returnzero = false, $currentgroup = 0) { - global $CFG; - $summary = quiz_num_attempt_summary($quiz, $cm, $returnzero, $currentgroup); - if (!$summary) { - return ''; - } - - require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php'); - $url = new moodle_url('/mod/quiz/report.php', array( - 'id' => $cm->id, 'mode' => quiz_report_default_report($context))); - return html_writer::link($url, $summary); - } - - /** - * Render a summary of the number of group and user overrides, with corresponding links. - * - * @param stdClass $quiz the quiz settings. - * @param stdClass|cm_info $cm the cm object. - * @param int $currentgroup currently selected group, if there is one. - * @return string HTML fragment for the link. - */ - public function quiz_override_summary_links(stdClass $quiz, stdClass $cm, $currentgroup = 0): string { - - $baseurl = new moodle_url('/mod/quiz/overrides.php', ['cmid' => $cm->id]); - $counts = quiz_override_summary($quiz, $cm, $currentgroup); - - $links = []; - if ($counts['group']) { - $links[] = html_writer::link(new moodle_url($baseurl, ['mode' => 'group']), - get_string('overridessummarygroup', 'quiz', $counts['group'])); - } - if ($counts['user']) { - $links[] = html_writer::link(new moodle_url($baseurl, ['mode' => 'user']), - get_string('overridessummaryuser', 'quiz', $counts['user'])); - } - - if (!$links) { - return ''; - } - - $links = implode(', ', $links); - switch ($counts['mode']) { - case 'onegroup': - return get_string('overridessummarythisgroup', 'quiz', $links); - - case 'somegroups': - return get_string('overridessummaryyourgroups', 'quiz', $links); - - case 'allgroups': - return get_string('overridessummary', 'quiz', $links); - - default: - throw new coding_exception('Unexpected mode ' . $counts['mode']); - } - } - - /** - * Outputs a chart. - * - * @param \core\chart_base $chart The chart. - * @param string $title The title to display above the graph. - * @param array $attrs extra container html attributes. - * @return string HTML fragment for the graph. - */ - public function chart(\core\chart_base $chart, $title, $attrs = []) { - return $this->heading($title, 3) . html_writer::tag('div', - $this->render($chart), array_merge(['class' => 'graph'], $attrs)); - } - - /** - * Output a graph, or a message saying that GD is required. - * @param moodle_url $url the URL of the graph. - * @param string $title the title to display above the graph. - * @return string HTML fragment for the graph. - */ - public function graph(moodle_url $url, $title) { - global $CFG; - - $graph = html_writer::empty_tag('img', array('src' => $url, 'alt' => $title)); - - return $this->heading($title, 3) . html_writer::tag('div', $graph, array('class' => 'graph')); - } - - /** - * Output the connection warning messages, which are initially hidden, and - * only revealed by JavaScript if necessary. - */ - public function connection_warning() { - $options = array('filter' => false, 'newlines' => false); - $warning = format_text(get_string('connectionerror', 'quiz'), FORMAT_MARKDOWN, $options); - $ok = format_text(get_string('connectionok', 'quiz'), FORMAT_MARKDOWN, $options); - return html_writer::tag('div', $warning, - array('id' => 'connection-error', 'style' => 'display: none;', 'role' => 'alert')) . - html_writer::tag('div', $ok, array('id' => 'connection-ok', 'style' => 'display: none;', 'role' => 'alert')); - } - - - /** - * Deprecated version of render_links_to_other_attempts. - * - * @param links_to_other_attempts $links - * @return string HTML fragment. - * @deprecated since Moodle 4.2. Please use render_links_to_other_attempts instead. - * @todo MDL-76612 Final deprecation in Moodle 4.6 - */ - protected function render_mod_quiz_links_to_other_attempts(links_to_other_attempts $links) { - return $this->render_links_to_other_attempts($links); - } -} +// Normally I would debug-output +// 'This file is no longer required in Moodle 4.2+. Please do not include/require it.' +// but this file gets automatically included by the renderer factories in a way that +// is hard to prevent, so not doing that here. diff --git a/mod/quiz/tests/locallib_test.php b/mod/quiz/tests/locallib_test.php index a5c1809461a..79a93f7a5f4 100644 --- a/mod/quiz/tests/locallib_test.php +++ b/mod/quiz/tests/locallib_test.php @@ -24,6 +24,7 @@ */ namespace mod_quiz; +use mod_quiz\output\renderer; use quiz_attempt; use mod_quiz\question\display_options; @@ -517,7 +518,7 @@ class locallib_test extends \advanced_testcase { $generator = $this->getDataGenerator(); /** @var mod_quiz_generator $quizgenerator */ $quizgenerator = $generator->get_plugin_generator('mod_quiz'); - /** @var mod_quiz_renderer $renderer */ + /** @var renderer $renderer */ $renderer = $PAGE->get_renderer('mod_quiz'); // Course with quiz and a group - plus some others, to verify they don't get counted. diff --git a/mod/quiz/upgrade.txt b/mod/quiz/upgrade.txt index c301d840a19..99eb7ce8815 100644 --- a/mod/quiz/upgrade.txt +++ b/mod/quiz/upgrade.txt @@ -42,6 +42,7 @@ This files describes API changes in the quiz code. - quiz_add_random_form => mod_quiz\form\add_random_form - mod_quiz_links_to_other_attempts => mod_quiz\output\links_to_other_attempts - mod_quiz_view_object => mod_quiz\output\view_page + - mod_quiz_renderer => mod_quiz\output\renderer * The following classes have been deprecated: - mod_quiz_overdue_attempt_updater - merged into mod_quiz\task\update_overdue_attempts @@ -59,6 +60,7 @@ This files describes API changes in the quiz code. - mod/quiz/cronlib.php - mod/quiz/override_form.php - mod/quiz/accessrule/accessrulebase.php + - mod/quiz/renderer.php === 4.1 === diff --git a/mod/quiz/view.php b/mod/quiz/view.php index 96badfebe86..67a27968e2d 100644 --- a/mod/quiz/view.php +++ b/mod/quiz/view.php @@ -24,6 +24,7 @@ */ use mod_quiz\access_manager; +use mod_quiz\output\renderer; use mod_quiz\output\view_page; require_once(__DIR__ . '/../../config.php'); @@ -149,7 +150,7 @@ if (html_is_blank($quiz->intro)) { $PAGE->activityheader->set_description(''); } $PAGE->add_body_class('limitedwidth'); -/** @var mod_quiz_renderer $output */ +/** @var renderer $output */ $output = $PAGE->get_renderer('mod_quiz'); // Print table with existing attempts. From 89f97a7e2a7ec273975e790da390f3d845de1d7d Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Mon, 19 Dec 2022 16:51:56 +0000 Subject: [PATCH 16/30] MDL-76614 quiz: code cleanup in mod_quiz\output\renderer --- mod/quiz/classes/output/renderer.php | 293 ++++++++++++++------------- mod/quiz/lib.php | 2 +- mod/quiz/locallib.php | 7 +- 3 files changed, 156 insertions(+), 146 deletions(-) diff --git a/mod/quiz/classes/output/renderer.php b/mod/quiz/classes/output/renderer.php index 6d7bade579d..11a8b7fad5b 100644 --- a/mod/quiz/classes/output/renderer.php +++ b/mod/quiz/classes/output/renderer.php @@ -16,11 +16,28 @@ namespace mod_quiz\output; +use cm_info; +use coding_exception; +use context; +use context_module; +use html_table; +use html_table_cell; +use html_writer; use mod_quiz\access_manager; use mod_quiz\form\preflight_check_form; use mod_quiz\question\display_options; +use moodle_url; use plugin_renderer_base; +use popup_action; +use question_display_options; +use quiz; use quiz_attempt; +use quiz_nav_panel_base; +use quiz_nav_question_button; +use quiz_nav_section_heading; +use renderable; +use single_button; +use stdClass; /** * The main renderer for the quiz module. @@ -35,13 +52,13 @@ class renderer extends plugin_renderer_base { * Builds the review page * * @param quiz_attempt $attemptobj an instance of quiz_attempt. - * @param array $slots an array of intgers relating to questions. + * @param array $slots of slots to be displayed. * @param int $page the current page number * @param bool $showall whether to show entire attempt on one page. * @param bool $lastpage if true the current page is the last page. * @param display_options $displayoptions instance of display_options. * @param array $summarydata contains all table data - * @return $output containing html data. + * @return string HTML to display. */ public function review_page(quiz_attempt $attemptobj, $slots, $page, $showall, $lastpage, display_options $displayoptions, @@ -67,7 +84,7 @@ class renderer extends plugin_renderer_base { * @param int $seq which step of the question attempt to show. null = latest. * @param display_options $displayoptions instance of display_options. * @param array $summarydata contains all table data - * @return $output containing html data. + * @return string HTML to display. */ public function review_question_page(quiz_attempt $attemptobj, $slot, $seq, display_options $displayoptions, $summarydata) { @@ -98,7 +115,7 @@ class renderer extends plugin_renderer_base { $output = ''; $output .= $this->header(); $output .= $this->heading(format_string($attemptobj->get_quiz_name(), true, - array("context" => $attemptobj->get_quizobj()->get_context()))); + ["context" => $attemptobj->get_quizobj()->get_context()])); $output .= $this->notification($message); $output .= $this->close_window_button(); $output .= $this->footer(); @@ -110,7 +127,7 @@ class renderer extends plugin_renderer_base { * * @param array $summarydata contains row data for table * @param int $page the current page number - * @return $summarydata containing filtered row data + * @return array updated version of the $summarydata array. */ protected function filter_review_summary_table($summarydata, $page) { if ($page == 0) { @@ -119,7 +136,7 @@ class renderer extends plugin_renderer_base { // Only show some of summary table on subsequent pages. foreach ($summarydata as $key => $rowdata) { - if (!in_array($key, array('user', 'attemptlist'))) { + if (!in_array($key, ['user', 'attemptlist'])) { unset($summarydata[$key]); } } @@ -132,6 +149,7 @@ class renderer extends plugin_renderer_base { * * @param array $summarydata contains row data for table * @param int $page contains the current page number + * @return string HTML to display. */ public function review_summary_table($summarydata, $page) { $summarydata = $this->filter_review_summary_table($summarydata, $page); @@ -140,8 +158,8 @@ class renderer extends plugin_renderer_base { } $output = ''; - $output .= html_writer::start_tag('table', array( - 'class' => 'generaltable generalbox quizreviewsummary')); + $output .= html_writer::start_tag('table', [ + 'class' => 'generaltable generalbox quizreviewsummary']); $output .= html_writer::start_tag('tbody'); foreach ($summarydata as $rowdata) { if ($rowdata['title'] instanceof renderable) { @@ -157,8 +175,8 @@ class renderer extends plugin_renderer_base { } $output .= html_writer::tag('tr', - html_writer::tag('th', $title, array('class' => 'cell', 'scope' => 'row')) . - html_writer::tag('td', $content, array('class' => 'cell')) + html_writer::tag('th', $title, ['class' => 'cell', 'scope' => 'row']) . + html_writer::tag('td', $content, ['class' => 'cell']) ); } @@ -172,7 +190,7 @@ class renderer extends plugin_renderer_base { * * @param quiz_attempt $attemptobj instance of quiz_attempt * @param bool $reviewing - * @param array $slots array of intgers relating to questions + * @param array $slots array of integers relating to questions * @param int $page current page number * @param bool $showall if true shows attempt on single page * @param display_options $displayoptions instance of display_options @@ -190,12 +208,12 @@ class renderer extends plugin_renderer_base { /** * Renders the main bit of the review page. * - * @param array $summarydata contain row data for table * @param int $page current page number - * @param display_options $displayoptions instance of display_options - * @param $content contains each question - * @param quiz_attempt $attemptobj instance of quiz_attempt * @param bool $showall if true display attempt on one page + * @param display_options $displayoptions instance of display_options + * @param string $content the rendered display of each question + * @param quiz_attempt $attemptobj instance of quiz_attempt + * @return string HTML to display. */ public function review_form($page, $showall, $displayoptions, $content, $attemptobj) { if ($displayoptions->flags != question_display_options::EDITABLE) { @@ -206,16 +224,16 @@ class renderer extends plugin_renderer_base { quiz_get_js_module()); $output = ''; - $output .= html_writer::start_tag('form', array('action' => $attemptobj->review_url(null, - $page, $showall), 'method' => 'post', 'class' => 'questionflagsaveform')); + $output .= html_writer::start_tag('form', ['action' => $attemptobj->review_url(null, + $page, $showall), 'method' => 'post', 'class' => 'questionflagsaveform']); $output .= html_writer::start_tag('div'); $output .= $content; - $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'sesskey', - 'value' => sesskey())); - $output .= html_writer::start_tag('div', array('class' => 'submitbtns')); - $output .= html_writer::empty_tag('input', array('type' => 'submit', + $output .= html_writer::empty_tag('input', ['type' => 'hidden', 'name' => 'sesskey', + 'value' => sesskey()]); + $output .= html_writer::start_tag('div', ['class' => 'submitbtns']); + $output .= html_writer::empty_tag('input', ['type' => 'submit', 'class' => 'questionflagsavebutton btn btn-secondary', 'name' => 'savingflags', - 'value' => get_string('saveflags', 'question'))); + 'value' => get_string('saveflags', 'question')]); $output .= html_writer::end_tag('div'); $output .= html_writer::end_tag('div'); $output .= html_writer::end_tag('form'); @@ -224,7 +242,7 @@ class renderer extends plugin_renderer_base { } /** - * Returns either a liink or button + * Returns either a link or button. * * @param quiz_attempt $attemptobj instance of quiz_attempt */ @@ -233,20 +251,20 @@ class renderer extends plugin_renderer_base { if ($attemptobj->get_access_manager(time())->attempt_must_be_in_popup()) { $this->page->requires->js_init_call('M.mod_quiz.secure_window.init_close_button', - array($url), false, quiz_get_js_module()); - return html_writer::empty_tag('input', array('type' => 'button', + [$url], false, quiz_get_js_module()); + return html_writer::empty_tag('input', ['type' => 'button', 'value' => get_string('finishreview', 'quiz'), 'id' => 'secureclosebutton', - 'class' => 'mod_quiz-next-nav btn btn-primary')); + 'class' => 'mod_quiz-next-nav btn btn-primary']); } else { return html_writer::link($url, get_string('finishreview', 'quiz'), - array('class' => 'mod_quiz-next-nav')); + ['class' => 'mod_quiz-next-nav']); } } /** - * Creates the navigation links/buttons at the bottom of the reivew attempt page. + * Creates the navigation links/buttons at the bottom of the review attempt page. * * Note, the name of this function is no longer accurate, but when the design * changed, it was decided to keep the old name for backwards compatibility. @@ -271,11 +289,14 @@ class renderer extends plugin_renderer_base { $nav .= link_arrow_right(get_string('navigatenext', 'quiz'), $attemptobj->review_url(null, $page + 1, $showall), false, 'mod_quiz-next-nav'); } - return html_writer::tag('div', $nav, array('class' => 'submitbtns')); + return html_writer::tag('div', $nav, ['class' => 'submitbtns']); } /** * Return the HTML of the quiz timer. + * + * @param quiz_attempt $attemptobj instance of quiz_attempt + * @param int $timenow timestamp to use as 'now'. * @return string HTML content. */ public function countdown_timer(quiz_attempt $attemptobj, $timenow) { @@ -292,14 +313,13 @@ class renderer extends plugin_renderer_base { $this->initialise_timer($timerstartvalue, $ispreview); } - return $this->output->render_from_template('mod_quiz/timer', (object)[]); } /** * Create a preview link * - * @param moodle_url $url contains a url to the given page + * @param moodle_url $url URL to restart the attempt. */ public function restart_preview_button($url) { return $this->single_button($url, get_string('startnewpreview', 'quiz')); @@ -316,23 +336,23 @@ class renderer extends plugin_renderer_base { $userpicture = $panel->user_picture(); if ($userpicture) { $fullname = fullname($userpicture->user); - if ($userpicture->size === true) { + if ($userpicture->size) { $fullname = html_writer::div($fullname); } $output .= html_writer::tag('div', $this->render($userpicture) . $fullname, - array('id' => 'user-picture', 'class' => 'clearfix')); + ['id' => 'user-picture', 'class' => 'clearfix']); } $output .= $panel->render_before_button_bits($this); $bcc = $panel->get_button_container_class(); - $output .= html_writer::start_tag('div', array('class' => "qn_buttons clearfix $bcc")); + $output .= html_writer::start_tag('div', ['class' => "qn_buttons clearfix $bcc"]); foreach ($panel->get_question_buttons() as $button) { $output .= $this->render($button); } $output .= html_writer::end_tag('div'); $output .= html_writer::tag('div', $panel->render_end_bits($this), - array('class' => 'othernav')); + ['class' => 'othernav']); $this->page->requires->js_init_call('M.mod_quiz.nav.init', null, false, quiz_get_js_module()); @@ -347,8 +367,8 @@ class renderer extends plugin_renderer_base { * @return string HTML fragment. */ protected function render_quiz_nav_question_button(quiz_nav_question_button $button) { - $classes = array('qnbutton', $button->stateclass, $button->navmethod, 'btn'); - $extrainfo = array(); + $classes = ['qnbutton', $button->stateclass, $button->navmethod, 'btn']; + $extrainfo = []; if ($button->currentpage) { $classes[] = 'thispage'; @@ -362,7 +382,7 @@ class renderer extends plugin_renderer_base { } else { $flaglabel = ''; } - $extrainfo[] = html_writer::tag('span', $flaglabel, array('class' => 'flagstate')); + $extrainfo[] = html_writer::tag('span', $flaglabel, ['class' => 'flagstate']); if (is_numeric($button->number)) { $qnostring = 'questionnonav'; @@ -373,11 +393,11 @@ class renderer extends plugin_renderer_base { $a = new stdClass(); $a->number = $button->number; $a->attributes = implode(' ', $extrainfo); - $tagcontents = html_writer::tag('span', '', array('class' => 'thispageholder')) . - html_writer::tag('span', '', array('class' => 'trafficlight')) . + $tagcontents = html_writer::tag('span', '', ['class' => 'thispageholder']) . + html_writer::tag('span', '', ['class' => 'trafficlight']) . get_string($qnostring, 'quiz', $a); - $tagattributes = array('class' => implode(' ', $classes), 'id' => $button->id, - 'title' => $button->statestring, 'data-quiz-page' => $button->page); + $tagattributes = ['class' => implode(' ', $classes), 'id' => $button->id, + 'title' => $button->statestring, 'data-quiz-page' => $button->page]; if ($button->url) { return html_writer::link($button->url, $tagcontents, $tagattributes); @@ -411,7 +431,7 @@ class renderer extends plugin_renderer_base { */ protected function render_links_to_other_attempts( links_to_other_attempts $links) { - $attemptlinks = array(); + $attemptlinks = []; foreach ($links->links as $attempt => $url) { if (!$url) { $attemptlinks[] = html_writer::tag('strong', $attempt); @@ -439,7 +459,7 @@ class renderer extends plugin_renderer_base { $output .= $this->header(); $output .= $this->during_attempt_tertiary_nav($quizobj->view_url()); $output .= $this->heading(format_string($quizobj->get_quiz_name(), true, - array("context" => $quizobj->get_context()))); + ["context" => $quizobj->get_context()])); $output .= $this->quiz_intro($quizobj->get_quiz(), $quizobj->get_cm()); $output .= $mform->render(); $output .= $this->footer(); @@ -506,7 +526,7 @@ class renderer extends plugin_renderer_base { } /** - * Ouputs the form for making an attempt + * Outputs the form for making an attempt * * @param quiz_attempt $attemptobj * @param int $page Current page number @@ -519,40 +539,40 @@ class renderer extends plugin_renderer_base { // Start the form. $output .= html_writer::start_tag('form', - array('action' => new moodle_url($attemptobj->processattempt_url(), - array('cmid' => $attemptobj->get_cmid())), 'method' => 'post', + ['action' => new moodle_url($attemptobj->processattempt_url(), + ['cmid' => $attemptobj->get_cmid()]), 'method' => 'post', 'enctype' => 'multipart/form-data', 'accept-charset' => 'utf-8', - 'id' => 'responseform')); + 'id' => 'responseform']); $output .= html_writer::start_tag('div'); // Print all the questions. foreach ($slots as $slot) { $output .= $attemptobj->render_question($slot, false, $this, - $attemptobj->attempt_url($slot, $page), $this); + $attemptobj->attempt_url($slot, $page)); } $navmethod = $attemptobj->get_quiz()->navmethod; $output .= $this->attempt_navigation_buttons($page, $attemptobj->is_last_page($page), $navmethod); - // Some hidden fields to trach what is going on. - $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'attempt', - 'value' => $attemptobj->get_attemptid())); - $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'thispage', - 'value' => $page, 'id' => 'followingpage')); - $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'nextpage', - 'value' => $nextpage)); - $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'timeup', - 'value' => '0', 'id' => 'timeup')); - $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'sesskey', - 'value' => sesskey())); - $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'scrollpos', - 'value' => '', 'id' => 'scrollpos')); + // Some hidden fields to track what is going on. + $output .= html_writer::empty_tag('input', ['type' => 'hidden', 'name' => 'attempt', + 'value' => $attemptobj->get_attemptid()]); + $output .= html_writer::empty_tag('input', ['type' => 'hidden', 'name' => 'thispage', + 'value' => $page, 'id' => 'followingpage']); + $output .= html_writer::empty_tag('input', ['type' => 'hidden', 'name' => 'nextpage', + 'value' => $nextpage]); + $output .= html_writer::empty_tag('input', ['type' => 'hidden', 'name' => 'timeup', + 'value' => '0', 'id' => 'timeup']); + $output .= html_writer::empty_tag('input', ['type' => 'hidden', 'name' => 'sesskey', + 'value' => sesskey()]); + $output .= html_writer::empty_tag('input', ['type' => 'hidden', 'name' => 'scrollpos', + 'value' => '', 'id' => 'scrollpos']); // Add a hidden field with questionids. Do this at the end of the form, so // if you navigate before the form has finished loading, it does not wipe all // the student's answers. - $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'slots', - 'value' => implode(',', $attemptobj->get_active_slots($page)))); + $output .= html_writer::empty_tag('input', ['type' => 'hidden', 'name' => 'slots', + 'value' => implode(',', $attemptobj->get_active_slots($page))]); // Finish the form. $output .= html_writer::end_tag('div'); @@ -574,11 +594,11 @@ class renderer extends plugin_renderer_base { protected function attempt_navigation_buttons($page, $lastpage, $navmethod = 'free') { $output = ''; - $output .= html_writer::start_tag('div', array('class' => 'submitbtns')); + $output .= html_writer::start_tag('div', ['class' => 'submitbtns']); if ($page > 0 && $navmethod == 'free') { - $output .= html_writer::empty_tag('input', array('type' => 'submit', 'name' => 'previous', + $output .= html_writer::empty_tag('input', ['type' => 'submit', 'name' => 'previous', 'value' => get_string('navigateprevious', 'quiz'), 'class' => 'mod_quiz-prev-nav btn btn-secondary', - 'id' => 'mod_quiz-prev-nav')); + 'id' => 'mod_quiz-prev-nav']); $this->page->requires->js_call_amd('core_form/submit', 'init', ['mod_quiz-prev-nav']); } if ($lastpage) { @@ -586,8 +606,8 @@ class renderer extends plugin_renderer_base { } else { $nextlabel = get_string('navigatenext', 'quiz'); } - $output .= html_writer::empty_tag('input', array('type' => 'submit', 'name' => 'next', - 'value' => $nextlabel, 'class' => 'mod_quiz-next-nav btn btn-primary', 'id' => 'mod_quiz-next-nav')); + $output .= html_writer::empty_tag('input', ['type' => 'submit', 'name' => 'next', + 'value' => $nextlabel, 'class' => 'mod_quiz-next-nav btn btn-primary', 'id' => 'mod_quiz-next-nav']); $output .= html_writer::end_tag('div'); $this->page->requires->js_call_amd('core_form/submit', 'init', ['mod_quiz-next-nav']); @@ -602,9 +622,9 @@ class renderer extends plugin_renderer_base { * @return string HTML fragment. */ public function redo_question_button($slot, $disabled) { - $attributes = array('type' => 'submit', 'name' => 'redoslot' . $slot, + $attributes = ['type' => 'submit', 'name' => 'redoslot' . $slot, 'value' => get_string('redoquestion', 'quiz'), - 'class' => 'mod_quiz-redo_question_button btn btn-secondary'); + 'class' => 'mod_quiz-redo_question_button btn btn-secondary']; if ($disabled) { $attributes['disabled'] = 'disabled'; } @@ -612,11 +632,13 @@ class renderer extends plugin_renderer_base { } /** - * Output the JavaScript required to initialise the countdown timer. + * Initialise the JavaScript required to initialise the countdown timer. + * * @param int $timerstartvalue time remaining, in seconds. + * @param bool $ispreview true if this is a preview attempt. */ public function initialise_timer($timerstartvalue, $ispreview) { - $options = array($timerstartvalue, (bool)$ispreview); + $options = [$timerstartvalue, (bool)$ispreview]; $this->page->requires->js_init_call('M.mod_quiz.timer.init', $options, false, quiz_get_js_module()); } @@ -641,7 +663,7 @@ class renderer extends plugin_renderer_base { $delay = 0; } $this->page->requires->js_init_call('M.mod_quiz.secure_window.close', - array($url, $delay), false, quiz_get_js_module()); + [$url, $delay], false, quiz_get_js_module()); $output .= $this->box_end(); $output .= $this->footer(); @@ -652,8 +674,6 @@ class renderer extends plugin_renderer_base { * Print each message in an array, surrounded by <p>, </p> tags. * * @param array $messages the array of message strings. - * @param bool $return if true, return a string, instead of outputting. - * * @return string HTML to output. */ public function access_messages($messages) { @@ -695,9 +715,9 @@ class renderer extends plugin_renderer_base { // Prepare the summary table header. $table = new html_table(); $table->attributes['class'] = 'generaltable quizsummaryofattempt boxaligncenter'; - $table->head = array(get_string('question', 'quiz'), get_string('status', 'quiz')); - $table->align = array('left', 'left'); - $table->size = array('', ''); + $table->head = [get_string('question', 'quiz'), get_string('status', 'quiz')]; + $table->align = ['left', 'left']; + $table->size = ['', '']; $markscolumn = $displayoptions->marks >= question_display_options::MARK_AND_MAX; if ($markscolumn) { $table->head[] = get_string('marks', 'quiz'); @@ -705,7 +725,7 @@ class renderer extends plugin_renderer_base { $table->size[] = ''; } $tablewidth = count($table->align); - $table->data = array(); + $table->data = []; // Get the summary info for each question. $slots = $attemptobj->get_slots(); @@ -726,7 +746,7 @@ class renderer extends plugin_renderer_base { $cell = new html_table_cell(format_string($heading)); $cell->header = true; $cell->colspan = $tablewidth; - $table->data[] = array($cell); + $table->data[] = [$cell]; $table->rowclasses[] = $rowclasses; } @@ -739,16 +759,16 @@ class renderer extends plugin_renderer_base { $flag = ''; if ($attemptobj->is_question_flagged($slot)) { // Quiz has custom JS manipulating these image tags - so we can't use the pix_icon method here. - $flag = html_writer::empty_tag('img', array('src' => $this->image_url('i/flagged'), - 'alt' => get_string('flagged', 'question'), 'class' => 'questionflag icon-post')); + $flag = html_writer::empty_tag('img', ['src' => $this->image_url('i/flagged'), + 'alt' => get_string('flagged', 'question'), 'class' => 'questionflag icon-post']); } if ($attemptobj->can_navigate_to($slot)) { - $row = array(html_writer::link($attemptobj->attempt_url($slot), + $row = [html_writer::link($attemptobj->attempt_url($slot), $attemptobj->get_question_number($slot) . $flag), - $attemptobj->get_question_status($slot, $displayoptions->correctness)); + $attemptobj->get_question_status($slot, $displayoptions->correctness)]; } else { - $row = array($attemptobj->get_question_number($slot) . $flag, - $attemptobj->get_question_status($slot, $displayoptions->correctness)); + $row = [$attemptobj->get_question_number($slot) . $flag, + $attemptobj->get_question_status($slot, $displayoptions->correctness)]; } if ($markscolumn) { $row[] = $attemptobj->get_question_mark($slot); @@ -759,13 +779,11 @@ class renderer extends plugin_renderer_base { } // Print the summary table. - $output = html_writer::table($table); - - return $output; + return html_writer::table($table); } /** - * Creates any controls a the page should have. + * Creates any controls the page should have. * * @param quiz_attempt $attemptobj */ @@ -782,19 +800,18 @@ class renderer extends plugin_renderer_base { } // Finish attempt button. - $options = array( + $options = [ 'attempt' => $attemptobj->get_attemptid(), 'finishattempt' => 1, 'timeup' => 0, 'slots' => '', 'cmid' => $attemptobj->get_cmid(), 'sesskey' => sesskey(), - ); + ]; $button = new single_button( new moodle_url($attemptobj->processattempt_url(), $options), get_string('submitallandfinish', 'quiz')); - $button->id = 'responseform'; $button->class = 'btn-finishattempt'; $button->formid = 'frm-finishattempt'; if ($attemptobj->get_state() == quiz_attempt::IN_PROGRESS) { @@ -895,7 +912,7 @@ class renderer extends plugin_renderer_base { if ($viewobj->showbacktocourse) { $output .= $this->single_button($viewobj->backtocourseurl, get_string('backtocourse', 'quiz'), 'get', - array('class' => 'continuebutton')); + ['class' => 'continuebutton']); } return $output; @@ -906,7 +923,7 @@ class renderer extends plugin_renderer_base { * * @param string $buttontext the label to display on the button. * @param moodle_url $url The URL to POST to in order to start the attempt. - * @param preflight_check_form $preflightcheckform deprecated. + * @param preflight_check_form|null $preflightcheckform deprecated. * @param bool $popuprequired whether the attempt needs to be opened in a pop-up. * @param array $popupoptions the options to use if we are opening a popup. * @return string HTML fragment. @@ -927,17 +944,11 @@ class renderer extends plugin_renderer_base { $popupjsoptions = $action->get_js_options(); } - if ($preflightcheckform) { - $checkform = $preflightcheckform->render(); - } else { - $checkform = null; - } - $this->page->requires->js_call_amd('mod_quiz/preflightcheck', 'init', - array('.quizstartbuttondiv [type=submit]', get_string('startattempt', 'quiz'), - '#mod_quiz_preflight_form', $popupjsoptions)); + ['.quizstartbuttondiv [type=submit]', get_string('startattempt', 'quiz'), + '#mod_quiz_preflight_form', $popupjsoptions]); - return $this->render($button) . $checkform; + return $this->render($button) . ($preflightcheckform ? $preflightcheckform->render() : ''); } /** @@ -953,8 +964,8 @@ class renderer extends plugin_renderer_base { public function no_questions_message($canedit, $editurl) { debugging('no_questions_message() is deprecated, please use generate_no_questions_message() instead.', DEBUG_DEVELOPER); - $output = html_writer::start_tag('div', array('class' => 'card text-center mb-3')); - $output .= html_writer::start_tag('div', array('class' => 'card-body')); + $output = html_writer::start_tag('div', ['class' => 'card text-center mb-3']); + $output .= html_writer::start_tag('div', ['class' => 'card-body']); $output .= $this->notification(get_string('noquestions', 'quiz'), 'warning', false); if ($canedit) { @@ -988,7 +999,7 @@ class renderer extends plugin_renderer_base { } /** - * Outputs and error message for anyone who is not enrolle don the course + * Outputs and error message for anyone who is not enrolled on the course. * * @param stdClass $course the course settings row from the database. * @param stdClass $quiz the quiz settings row from the database. @@ -1032,7 +1043,7 @@ class renderer extends plugin_renderer_base { if ($strattemptnum = $this->quiz_attempt_summary_link_to_reports($quiz, $cm, $context)) { $output .= html_writer::tag('div', $strattemptnum, - array('class' => 'quizattemptcounts')); + ['class' => 'quizattemptcounts']); } } @@ -1069,8 +1080,8 @@ class renderer extends plugin_renderer_base { /** * Generates the table of data * - * @param array $quiz Array contining quiz data - * @param int $context The page context ID + * @param stdClass $quiz the quiz settings. + * @param context_module $context the quiz context. * @param view_page $viewobj */ public function view_table($quiz, $context, $viewobj) { @@ -1081,9 +1092,9 @@ class renderer extends plugin_renderer_base { // Prepare table header. $table = new html_table(); $table->attributes['class'] = 'generaltable quizattemptsummary'; - $table->head = array(); - $table->align = array(); - $table->size = array(); + $table->head = []; + $table->align = []; + $table->size = []; if ($viewobj->attemptcolumn) { $table->head[] = get_string('attemptnumber', 'quiz'); $table->align[] = 'center'; @@ -1118,7 +1129,7 @@ class renderer extends plugin_renderer_base { // One row for each attempt. foreach ($viewobj->attemptobjs as $attemptobj) { $attemptoptions = $attemptobj->get_display_options(true); - $row = array(); + $row = []; // Add the attempt number. if ($viewobj->attemptcolumn) { @@ -1140,7 +1151,7 @@ class renderer extends plugin_renderer_base { } } - // Ouside the if because we may be showing feedback but not grades. + // Outside the if because we may be showing feedback but not grades. $attemptgrade = quiz_rescale_grade($attemptobj->get_sum_marks(), $quiz, false); if ($viewobj->gradecolumn) { @@ -1189,9 +1200,9 @@ class renderer extends plugin_renderer_base { } /** - * Generate a brief textual desciption of the current state of an attempt. + * Generate a brief textual description of the current state of an attempt. + * * @param quiz_attempt $attemptobj the attempt - * @param int $timenow the time to use as 'now'. * @return string the appropriate lang string to describe the state. */ public function attempt_state($attemptobj) { @@ -1203,26 +1214,30 @@ class renderer extends plugin_renderer_base { return get_string('stateoverdue', 'quiz') . html_writer::tag('span', get_string('stateoverduedetails', 'quiz', userdate($attemptobj->get_due_date())), - array('class' => 'statedetails')); + ['class' => 'statedetails']); case quiz_attempt::FINISHED: return get_string('statefinished', 'quiz') . html_writer::tag('span', get_string('statefinisheddetails', 'quiz', userdate($attemptobj->get_submitted_date())), - array('class' => 'statedetails')); + ['class' => 'statedetails']); case quiz_attempt::ABANDONED: return get_string('stateabandoned', 'quiz'); + + default: + throw new coding_exception('Unexpected attempt state'); } } /** * Generates data pertaining to quiz results * - * @param array $quiz Array containing quiz data - * @param int $context The page context ID - * @param int $cm The Course Module Id + * @param stdClass $quiz Array containing quiz data + * @param context_module $context The quiz context. + * @param stdClass|cm_info $cm The course module information. * @param view_page $viewobj + * @return string HTML to display. */ public function view_result_info($quiz, $context, $cm, $viewobj) { $output = ''; @@ -1250,7 +1265,7 @@ class renderer extends plugin_renderer_base { if ($viewobj->mygradeoverridden) { $resultinfo .= html_writer::tag('p', get_string('overriddennotice', 'grades'), - array('class' => 'overriddennotice'))."\n"; + ['class' => 'overriddennotice'])."\n"; } if ($viewobj->gradebookfeedback) { $resultinfo .= $this->heading(get_string('comment', 'quiz'), 3); @@ -1286,7 +1301,7 @@ class renderer extends plugin_renderer_base { } else { return html_writer::link($url, get_string('review', 'quiz'), - array('title' => get_string('reviewthisattempt', 'quiz'))); + ['title' => get_string('reviewthisattempt', 'quiz')]); } } @@ -1298,12 +1313,11 @@ class renderer extends plugin_renderer_base { */ public function no_review_message($message) { return html_writer::nonempty_tag('span', $message, - array('class' => 'noreviewmessage')); + ['class' => 'noreviewmessage']); } /** - * Returns the same as {@link quiz_num_attempt_summary()} but wrapped in a link - * to the quiz reports. + * Returns the same as {@see quiz_num_attempt_summary()} but wrapped in a link to the quiz reports. * * @param stdClass $quiz the quiz object. Only $quiz->id is used at the moment. * @param stdClass $cm the cm object. Only $cm->course, $cm->groupmode and $cm->groupingid @@ -1324,8 +1338,8 @@ class renderer extends plugin_renderer_base { } require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php'); - $url = new moodle_url('/mod/quiz/report.php', array( - 'id' => $cm->id, 'mode' => quiz_report_default_report($context))); + $url = new moodle_url('/mod/quiz/report.php', [ + 'id' => $cm->id, 'mode' => quiz_report_default_report($context)]); return html_writer::link($url, $summary); } @@ -1333,7 +1347,7 @@ class renderer extends plugin_renderer_base { * Render a summary of the number of group and user overrides, with corresponding links. * * @param stdClass $quiz the quiz settings. - * @param stdClass|cm_info $cm the cm object. + * @param stdClass $cm the cm object. * @param int $currentgroup currently selected group, if there is one. * @return string HTML fragment for the link. */ @@ -1378,7 +1392,7 @@ class renderer extends plugin_renderer_base { * @param \core\chart_base $chart The chart. * @param string $title The title to display above the graph. * @param array $attrs extra container html attributes. - * @return string HTML fragment for the graph. + * @return string HTML of the graph. */ public function chart(\core\chart_base $chart, $title, $attrs = []) { return $this->heading($title, 3) . html_writer::tag('div', @@ -1389,14 +1403,12 @@ class renderer extends plugin_renderer_base { * Output a graph, or a message saying that GD is required. * @param moodle_url $url the URL of the graph. * @param string $title the title to display above the graph. - * @return string HTML fragment for the graph. + * @return string HTML of the graph. */ public function graph(moodle_url $url, $title) { - global $CFG; + $graph = html_writer::empty_tag('img', ['src' => $url, 'alt' => $title]); - $graph = html_writer::empty_tag('img', array('src' => $url, 'alt' => $title)); - - return $this->heading($title, 3) . html_writer::tag('div', $graph, array('class' => 'graph')); + return $this->heading($title, 3) . html_writer::tag('div', $graph, ['class' => 'graph']); } /** @@ -1404,15 +1416,14 @@ class renderer extends plugin_renderer_base { * only revealed by JavaScript if necessary. */ public function connection_warning() { - $options = array('filter' => false, 'newlines' => false); + $options = ['filter' => false, 'newlines' => false]; $warning = format_text(get_string('connectionerror', 'quiz'), FORMAT_MARKDOWN, $options); $ok = format_text(get_string('connectionok', 'quiz'), FORMAT_MARKDOWN, $options); return html_writer::tag('div', $warning, - array('id' => 'connection-error', 'style' => 'display: none;', 'role' => 'alert')) . - html_writer::tag('div', $ok, array('id' => 'connection-ok', 'style' => 'display: none;', 'role' => 'alert')); + ['id' => 'connection-error', 'style' => 'display: none;', 'role' => 'alert']) . + html_writer::tag('div', $ok, ['id' => 'connection-ok', 'style' => 'display: none;', 'role' => 'alert']); } - /** * Deprecated version of render_links_to_other_attempts. * diff --git a/mod/quiz/lib.php b/mod/quiz/lib.php index a4c5b2f5494..e90cd1fed3d 100644 --- a/mod/quiz/lib.php +++ b/mod/quiz/lib.php @@ -661,7 +661,7 @@ function quiz_get_user_grades($quiz, $userid = 0) { /** * Round a grade to to the correct number of decimal places, and format it for display. * - * @param object $quiz The quiz table row, only $quiz->decimalpoints is used. + * @param stdClass $quiz The quiz table row, only $quiz->decimalpoints is used. * @param float $grade The grade to round. * @return float */ diff --git a/mod/quiz/locallib.php b/mod/quiz/locallib.php index ef979eadd1e..ec9adb793b5 100644 --- a/mod/quiz/locallib.php +++ b/mod/quiz/locallib.php @@ -584,8 +584,8 @@ function quiz_feedback_record_for_grade($grade, $quiz) { * got this grade on this quiz. The feedback is processed ready for diplay. * * @param float $grade a grade on this quiz. - * @param object $quiz the quiz settings. - * @param object $context the quiz context. + * @param stdClass $quiz the quiz settings. + * @param context_module $context the quiz context. * @return string the comment that corresponds to this grade (empty string if there is not one. */ function quiz_feedback_for_grade($grade, $quiz, $context) { @@ -2692,8 +2692,7 @@ function quiz_retrieve_tags_for_slot_ids($slotids) { * * @param int $attemptid the id of the current attempt. * @param int|null $cmid the course_module id for this quiz. - * @return quiz_attempt $attemptobj all the data about the quiz attempt. - * @throws moodle_exception + * @return quiz_attempt all the data about the quiz attempt. */ function quiz_create_attempt_handling_errors($attemptid, $cmid = null) { try { From 6833a902df9905e8cacdc9793ee3f0ce62690720 Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Mon, 19 Dec 2022 19:34:26 +0000 Subject: [PATCH 17/30] MDL-76614 quiz: move quiz_nav_question_button to classes folder --- mod/quiz/attemptlib.php | 32 +---- .../output/navigation_question_button.php | 49 +++++++ mod/quiz/classes/output/renderer.php | 134 ++++++++++-------- mod/quiz/db/renamedclasses.php | 1 + mod/quiz/upgrade.txt | 1 + 5 files changed, 130 insertions(+), 87 deletions(-) create mode 100644 mod/quiz/classes/output/navigation_question_button.php diff --git a/mod/quiz/attemptlib.php b/mod/quiz/attemptlib.php index bcc9d6f441a..f6759fef6c8 100644 --- a/mod/quiz/attemptlib.php +++ b/mod/quiz/attemptlib.php @@ -29,6 +29,7 @@ defined('MOODLE_INTERNAL') || die(); use mod_quiz\access_manager; use mod_quiz\output\links_to_other_attempts; +use mod_quiz\output\navigation_question_button; use mod_quiz\output\renderer; use mod_quiz\question\bank\qbank_helper; use mod_quiz\question\display_options; @@ -2732,35 +2733,6 @@ class quiz_nav_section_heading implements renderable { } -/** - * Represents a single link in the navigation panel. - * - * @copyright 2011 The Open University - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @since Moodle 2.1 - */ -class quiz_nav_question_button implements renderable { - /** @var string id="..." to add to the HTML for this button. */ - public $id; - /** @var string number to display in this button. Either the question number of 'i'. */ - public $number; - /** @var string class to add to the class="" attribute to represnt the question state. */ - public $stateclass; - /** @var string Textual description of the question state, e.g. to use as a tool tip. */ - public $statestring; - /** @var int the page number this question is on. */ - public $page; - /** @var bool true if this question is on the current page. */ - public $currentpage; - /** @var bool true if this question has been flagged. */ - public $flagged; - /** @var moodle_url the link this button goes to, or null if there should not be a link. */ - public $url; - /** @var int QUIZ_NAVMETHOD_FREE or QUIZ_NAVMETHOD_SEQ. */ - public $navmethod; -} - - /** * Represents the navigation panel, and builds a {@link block_contents} to allow * it to be output. @@ -2806,7 +2778,7 @@ abstract class quiz_nav_panel_base { $qa = $this->attemptobj->get_question_attempt($slot); $showcorrectness = $this->options->correctness && $qa->has_marks(); - $button = new quiz_nav_question_button(); + $button = new navigation_question_button(); $button->id = 'quiznavbutton' . $slot; $button->number = $this->attemptobj->get_question_number($slot); $button->stateclass = $qa->get_state_class($showcorrectness); diff --git a/mod/quiz/classes/output/navigation_question_button.php b/mod/quiz/classes/output/navigation_question_button.php new file mode 100644 index 00000000000..e07a23d73d2 --- /dev/null +++ b/mod/quiz/classes/output/navigation_question_button.php @@ -0,0 +1,49 @@ +. + +namespace mod_quiz\output; + +use moodle_url; +use renderable; + +/** + * Represents a single link in the navigation panel. + * + * @package mod_quiz + * @category output + * @copyright 2011 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class navigation_question_button implements renderable { + /** @var string id="..." to add to the HTML for this button. */ + public $id; + /** @var string number to display in this button. Either the question number of 'i'. */ + public $number; + /** @var string class to add to the class="" attribute to represnt the question state. */ + public $stateclass; + /** @var string Textual description of the question state, e.g. to use as a tool tip. */ + public $statestring; + /** @var int the page number this question is on. */ + public $page; + /** @var bool true if this question is on the current page. */ + public $currentpage; + /** @var bool true if this question has been flagged. */ + public $flagged; + /** @var moodle_url the link this button goes to, or null if there should not be a link. */ + public $url; + /** @var int QUIZ_NAVMETHOD_FREE or QUIZ_NAVMETHOD_SEQ. */ + public $navmethod; +} diff --git a/mod/quiz/classes/output/renderer.php b/mod/quiz/classes/output/renderer.php index 11a8b7fad5b..895727edc3b 100644 --- a/mod/quiz/classes/output/renderer.php +++ b/mod/quiz/classes/output/renderer.php @@ -33,7 +33,6 @@ use question_display_options; use quiz; use quiz_attempt; use quiz_nav_panel_base; -use quiz_nav_question_button; use quiz_nav_section_heading; use renderable; use single_button; @@ -61,8 +60,7 @@ class renderer extends plugin_renderer_base { * @return string HTML to display. */ public function review_page(quiz_attempt $attemptobj, $slots, $page, $showall, - $lastpage, display_options $displayoptions, - $summarydata) { + $lastpage, display_options $displayoptions, $summarydata) { $output = ''; $output .= $this->header(); @@ -175,8 +173,8 @@ class renderer extends plugin_renderer_base { } $output .= html_writer::tag('tr', - html_writer::tag('th', $title, ['class' => 'cell', 'scope' => 'row']) . - html_writer::tag('td', $content, ['class' => 'cell']) + html_writer::tag('th', $title, ['class' => 'cell', 'scope' => 'row']) . + html_writer::tag('td', $content, ['class' => 'cell']) ); } @@ -196,7 +194,7 @@ class renderer extends plugin_renderer_base { * @param display_options $displayoptions instance of display_options */ public function questions(quiz_attempt $attemptobj, $reviewing, $slots, $page, $showall, - display_options $displayoptions) { + display_options $displayoptions) { $output = ''; foreach ($slots as $slot) { $output .= $attemptobj->render_question($slot, $reviewing, $this, @@ -313,7 +311,7 @@ class renderer extends plugin_renderer_base { $this->initialise_timer($timerstartvalue, $ispreview); } - return $this->output->render_from_template('mod_quiz/timer', (object)[]); + return $this->output->render_from_template('mod_quiz/timer', (object) []); } /** @@ -363,10 +361,10 @@ class renderer extends plugin_renderer_base { /** * Display a quiz navigation button. * - * @param quiz_nav_question_button $button + * @param navigation_question_button $button * @return string HTML fragment. */ - protected function render_quiz_nav_question_button(quiz_nav_question_button $button) { + protected function render_navigation_question_button(navigation_question_button $button) { $classes = ['qnbutton', $button->stateclass, $button->navmethod, 'btn']; $extrainfo = []; @@ -394,10 +392,10 @@ class renderer extends plugin_renderer_base { $a->number = $button->number; $a->attributes = implode(' ', $extrainfo); $tagcontents = html_writer::tag('span', '', ['class' => 'thispageholder']) . - html_writer::tag('span', '', ['class' => 'trafficlight']) . - get_string($qnostring, 'quiz', $a); + html_writer::tag('span', '', ['class' => 'trafficlight']) . + get_string($qnostring, 'quiz', $a); $tagattributes = ['class' => implode(' ', $classes), 'id' => $button->id, - 'title' => $button->statestring, 'data-quiz-page' => $button->page]; + 'title' => $button->statestring, 'data-quiz-page' => $button->page]; if ($button->url) { return html_writer::link($button->url, $tagcontents, $tagattributes); @@ -435,10 +433,12 @@ class renderer extends plugin_renderer_base { foreach ($links->links as $attempt => $url) { if (!$url) { $attemptlinks[] = html_writer::tag('strong', $attempt); - } else if ($url instanceof renderable) { - $attemptlinks[] = $this->render($url); } else { - $attemptlinks[] = html_writer::link($url, $attempt); + if ($url instanceof renderable) { + $attemptlinks[] = $this->render($url); + } else { + $attemptlinks[] = html_writer::link($url, $attempt); + } } } return implode(', ', $attemptlinks); @@ -519,9 +519,9 @@ class renderer extends plugin_renderer_base { return ''; } return $this->notification( - html_writer::tag('p', get_string('accessnoticesheader', 'quiz')) . $this->access_messages($messages), - 'warning', - false + html_writer::tag('p', get_string('accessnoticesheader', 'quiz')) . $this->access_messages($messages), + 'warning', + false ); } @@ -540,9 +540,9 @@ class renderer extends plugin_renderer_base { // Start the form. $output .= html_writer::start_tag('form', ['action' => new moodle_url($attemptobj->processattempt_url(), - ['cmid' => $attemptobj->get_cmid()]), 'method' => 'post', - 'enctype' => 'multipart/form-data', 'accept-charset' => 'utf-8', - 'id' => 'responseform']); + ['cmid' => $attemptobj->get_cmid()]), 'method' => 'post', + 'enctype' => 'multipart/form-data', 'accept-charset' => 'utf-8', + 'id' => 'responseform']); $output .= html_writer::start_tag('div'); // Print all the questions. @@ -622,9 +622,9 @@ class renderer extends plugin_renderer_base { * @return string HTML fragment. */ public function redo_question_button($slot, $disabled) { - $attributes = ['type' => 'submit', 'name' => 'redoslot' . $slot, - 'value' => get_string('redoquestion', 'quiz'), - 'class' => 'mod_quiz-redo_question_button btn btn-secondary']; + $attributes = ['type' => 'submit', 'name' => 'redoslot' . $slot, + 'value' => get_string('redoquestion', 'quiz'), + 'class' => 'mod_quiz-redo_question_button btn btn-secondary']; if ($disabled) { $attributes['disabled'] = 'disabled'; } @@ -638,13 +638,14 @@ class renderer extends plugin_renderer_base { * @param bool $ispreview true if this is a preview attempt. */ public function initialise_timer($timerstartvalue, $ispreview) { - $options = [$timerstartvalue, (bool)$ispreview]; + $options = [$timerstartvalue, (bool) $ispreview]; $this->page->requires->js_init_call('M.mod_quiz.timer.init', $options, false, quiz_get_js_module()); } /** * Output a page with an optional message, and JavaScript code to close the * current window and redirect the parent window to a new URL. + * * @param moodle_url $url the URL to redirect the parent window to. * @param string $message message to display before closing the window. (optional) * @return string HTML to output. @@ -737,11 +738,13 @@ class renderer extends plugin_renderer_base { $rowclasses = 'quizsummaryheading'; if ($heading) { $heading = format_string($heading); - } else if (count($attemptobj->get_quizobj()->get_sections()) > 1) { - // If this is the start of an unnamed section, and the quiz has more - // than one section, then add a default heading. - $heading = get_string('sectionnoname', 'quiz'); - $rowclasses .= ' dimmed_text'; + } else { + if (count($attemptobj->get_quizobj()->get_sections()) > 1) { + // If this is the start of an unnamed section, and the quiz has more + // than one section, then add a default heading. + $heading = get_string('sectionnoname', 'quiz'); + $rowclasses .= ' dimmed_text'; + } } $cell = new html_table_cell(format_string($heading)); $cell->header = true; @@ -768,14 +771,14 @@ class renderer extends plugin_renderer_base { $attemptobj->get_question_status($slot, $displayoptions->correctness)]; } else { $row = [$attemptobj->get_question_number($slot) . $flag, - $attemptobj->get_question_status($slot, $displayoptions->correctness)]; + $attemptobj->get_question_status($slot, $displayoptions->correctness)]; } if ($markscolumn) { $row[] = $attemptobj->get_question_mark($slot); } $table->data[] = $row; $table->rowclasses[] = 'quizsummary' . $slot . ' ' . $attemptobj->get_question_state_class( - $slot, $displayoptions->correctness); + $slot, $displayoptions->correctness); } // Print the summary table. @@ -801,12 +804,12 @@ class renderer extends plugin_renderer_base { // Finish attempt button. $options = [ - 'attempt' => $attemptobj->get_attemptid(), - 'finishattempt' => 1, - 'timeup' => 0, - 'slots' => '', - 'cmid' => $attemptobj->get_cmid(), - 'sesskey' => sesskey(), + 'attempt' => $attemptobj->get_attemptid(), + 'finishattempt' => 1, + 'timeup' => 0, + 'slots' => '', + 'cmid' => $attemptobj->get_cmid(), + 'sesskey' => sesskey(), ]; $button = new single_button( @@ -829,13 +832,15 @@ class renderer extends plugin_renderer_base { if ($attemptobj->get_state() == quiz_attempt::OVERDUE) { $message = get_string('overduemustbesubmittedby', 'quiz', userdate($duedate)); - } else if ($duedate) { - $message = get_string('mustbesubmittedby', 'quiz', userdate($duedate)); + } else { + if ($duedate) { + $message = get_string('mustbesubmittedby', 'quiz', userdate($duedate)); + } } $output .= $this->countdown_timer($attemptobj, time()); $output .= $this->container($message . $this->container( - $this->render($button), 'controls'), 'submitbtns mdl-align'); + $this->render($button), 'controls'), 'submitbtns mdl-align'); return $output; } @@ -946,7 +951,7 @@ class renderer extends plugin_renderer_base { $this->page->requires->js_call_amd('mod_quiz/preflightcheck', 'init', ['.quizstartbuttondiv [type=submit]', get_string('startattempt', 'quiz'), - '#mod_quiz_preflight_form', $popupjsoptions]); + '#mod_quiz_preflight_form', $popupjsoptions]); return $this->render($button) . ($preflightcheckform ? $preflightcheckform->render() : ''); } @@ -994,7 +999,7 @@ class renderer extends plugin_renderer_base { $guestno = html_writer::tag('p', get_string('guestsno', 'quiz')); $liketologin = html_writer::tag('p', get_string('liketologin')); $referer = get_local_referer(false); - $output .= $this->confirm($guestno."\n\n".$liketologin."\n", get_login_url(), $referer); + $output .= $this->confirm($guestno . "\n\n" . $liketologin . "\n", get_login_url(), $referer); return $output; } @@ -1016,7 +1021,7 @@ class renderer extends plugin_renderer_base { $youneedtoenrol = html_writer::tag('p', get_string('youneedtoenrol', 'quiz')); $button = html_writer::tag('p', $this->continue_button($CFG->wwwroot . '/course/view.php?id=' . $course->id)); - $output .= $this->box($youneedtoenrol."\n\n".$button."\n", 'generalbox', 'notice'); + $output .= $this->box($youneedtoenrol . "\n\n" . $button . "\n", 'generalbox', 'notice'); return $output; } @@ -1058,6 +1063,7 @@ class renderer extends plugin_renderer_base { /** * Output the quiz intro. + * * @param object $quiz the quiz settings. * @param object $cm the course_module object. * @return string HTML to output. @@ -1212,15 +1218,15 @@ class renderer extends plugin_renderer_base { case quiz_attempt::OVERDUE: return get_string('stateoverdue', 'quiz') . html_writer::tag('span', - get_string('stateoverduedetails', 'quiz', - userdate($attemptobj->get_due_date())), - ['class' => 'statedetails']); + get_string('stateoverduedetails', 'quiz', + userdate($attemptobj->get_due_date())), + ['class' => 'statedetails']); case quiz_attempt::FINISHED: return get_string('statefinished', 'quiz') . html_writer::tag('span', - get_string('statefinisheddetails', 'quiz', - userdate($attemptobj->get_submitted_date())), - ['class' => 'statedetails']); + get_string('statefinisheddetails', 'quiz', + userdate($attemptobj->get_submitted_date())), + ['class' => 'statedetails']); case quiz_attempt::ABANDONED: return get_string('stateabandoned', 'quiz'); @@ -1265,7 +1271,7 @@ class renderer extends plugin_renderer_base { if ($viewobj->mygradeoverridden) { $resultinfo .= html_writer::tag('p', get_string('overriddennotice', 'grades'), - ['class' => 'overriddennotice'])."\n"; + ['class' => 'overriddennotice']) . "\n"; } if ($viewobj->gradebookfeedback) { $resultinfo .= $this->heading(get_string('comment', 'quiz'), 3); @@ -1274,8 +1280,8 @@ class renderer extends plugin_renderer_base { if ($viewobj->feedbackcolumn) { $resultinfo .= $this->heading(get_string('overallfeedback', 'quiz'), 3); $resultinfo .= html_writer::div( - quiz_feedback_for_grade($viewobj->mygrade, $quiz, $context), - 'quizgradefeedback') . "\n"; + quiz_feedback_for_grade($viewobj->mygrade, $quiz, $context), + 'quizgradefeedback') . "\n"; } if ($resultinfo) { @@ -1308,6 +1314,7 @@ class renderer extends plugin_renderer_base { /** * Displayed where there might normally be a review link, to explain why the * review is not available at this time. + * * @param string $message optional message explaining why the review is not possible. * @return string HTML to output. */ @@ -1330,7 +1337,7 @@ class renderer extends plugin_renderer_base { * @return string HTML fragment for the link. */ public function quiz_attempt_summary_link_to_reports($quiz, $cm, $context, - $returnzero = false, $currentgroup = 0) { + $returnzero = false, $currentgroup = 0) { global $CFG; $summary = quiz_num_attempt_summary($quiz, $cm, $returnzero, $currentgroup); if (!$summary) { @@ -1396,11 +1403,12 @@ class renderer extends plugin_renderer_base { */ public function chart(\core\chart_base $chart, $title, $attrs = []) { return $this->heading($title, 3) . html_writer::tag('div', - $this->render($chart), array_merge(['class' => 'graph'], $attrs)); + $this->render($chart), array_merge(['class' => 'graph'], $attrs)); } /** * Output a graph, or a message saying that GD is required. + * * @param moodle_url $url the URL of the graph. * @param string $title the title to display above the graph. * @return string HTML of the graph. @@ -1420,8 +1428,8 @@ class renderer extends plugin_renderer_base { $warning = format_text(get_string('connectionerror', 'quiz'), FORMAT_MARKDOWN, $options); $ok = format_text(get_string('connectionok', 'quiz'), FORMAT_MARKDOWN, $options); return html_writer::tag('div', $warning, - ['id' => 'connection-error', 'style' => 'display: none;', 'role' => 'alert']) . - html_writer::tag('div', $ok, ['id' => 'connection-ok', 'style' => 'display: none;', 'role' => 'alert']); + ['id' => 'connection-error', 'style' => 'display: none;', 'role' => 'alert']) . + html_writer::tag('div', $ok, ['id' => 'connection-ok', 'style' => 'display: none;', 'role' => 'alert']); } /** @@ -1435,4 +1443,16 @@ class renderer extends plugin_renderer_base { protected function render_mod_quiz_links_to_other_attempts(links_to_other_attempts $links) { return $this->render_links_to_other_attempts($links); } + + /** + * Deprecated version of render_navigation_question_button. + * + * @param navigation_question_button $button + * @return string HTML fragment. + * @deprecated since Moodle 4.2. Please use render_links_to_other_attempts instead. + * @todo MDL-76612 Final deprecation in Moodle 4.6 + */ + protected function render_quiz_nav_question_button(navigation_question_button $button) { + return $this->render_navigation_question_button($button); + } } diff --git a/mod/quiz/db/renamedclasses.php b/mod/quiz/db/renamedclasses.php index 3d964cc4963..74503b65e65 100644 --- a/mod/quiz/db/renamedclasses.php +++ b/mod/quiz/db/renamedclasses.php @@ -59,4 +59,5 @@ $renamedclasses = [ 'mod_quiz_links_to_other_attempts' => 'mod_quiz\output\links_to_other_attempts', 'mod_quiz_view_object' => 'mod_quiz\output\view_page', 'mod_quiz_renderer' => 'mod_quiz\output\renderer', + 'quiz_nav_question_button' => 'mod_quiz\output\navigation_question_button', ]; diff --git a/mod/quiz/upgrade.txt b/mod/quiz/upgrade.txt index 99eb7ce8815..808a496e7a8 100644 --- a/mod/quiz/upgrade.txt +++ b/mod/quiz/upgrade.txt @@ -43,6 +43,7 @@ This files describes API changes in the quiz code. - mod_quiz_links_to_other_attempts => mod_quiz\output\links_to_other_attempts - mod_quiz_view_object => mod_quiz\output\view_page - mod_quiz_renderer => mod_quiz\output\renderer + - quiz_nav_question_button => mod_quiz\output\navigation_question_button * The following classes have been deprecated: - mod_quiz_overdue_attempt_updater - merged into mod_quiz\task\update_overdue_attempts From 600b17db2138d87bf15ecf827b13deccff193511 Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Mon, 19 Dec 2022 19:41:15 +0000 Subject: [PATCH 18/30] MDL-76614 quiz: move quiz_nav_section_heading to classes folder --- mod/quiz/attemptlib.php | 24 +---------- .../output/navigation_section_heading.php | 40 +++++++++++++++++++ mod/quiz/classes/output/renderer.php | 17 ++++++-- mod/quiz/db/renamedclasses.php | 1 + mod/quiz/upgrade.txt | 1 + 5 files changed, 58 insertions(+), 25 deletions(-) create mode 100644 mod/quiz/classes/output/navigation_section_heading.php diff --git a/mod/quiz/attemptlib.php b/mod/quiz/attemptlib.php index f6759fef6c8..112d64009d2 100644 --- a/mod/quiz/attemptlib.php +++ b/mod/quiz/attemptlib.php @@ -30,6 +30,7 @@ defined('MOODLE_INTERNAL') || die(); use mod_quiz\access_manager; use mod_quiz\output\links_to_other_attempts; use mod_quiz\output\navigation_question_button; +use mod_quiz\output\navigation_section_heading; use mod_quiz\output\renderer; use mod_quiz\question\bank\qbank_helper; use mod_quiz\question\display_options; @@ -2712,27 +2713,6 @@ class quiz_attempt { } -/** - * Represents a heading in the navigation panel. - * - * @copyright 2015 The Open University - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @since Moodle 2.9 - */ -class quiz_nav_section_heading implements renderable { - /** @var string the heading text. */ - public $heading; - - /** - * Constructor. - * @param string $heading the heading text - */ - public function __construct($heading) { - $this->heading = $heading; - } -} - - /** * Represents the navigation panel, and builds a {@link block_contents} to allow * it to be output. @@ -2771,7 +2751,7 @@ abstract class quiz_nav_panel_base { if (!is_null($heading)) { $sections = $this->attemptobj->get_quizobj()->get_sections(); if (!(empty($heading) && count($sections) == 1)) { - $buttons[] = new quiz_nav_section_heading(format_string($heading)); + $buttons[] = new navigation_section_heading(format_string($heading)); } } diff --git a/mod/quiz/classes/output/navigation_section_heading.php b/mod/quiz/classes/output/navigation_section_heading.php new file mode 100644 index 00000000000..e70c79fd39a --- /dev/null +++ b/mod/quiz/classes/output/navigation_section_heading.php @@ -0,0 +1,40 @@ +. + +namespace mod_quiz\output; + +use renderable; + +/** + * Represents a heading in the navigation panel. + * + * @package mod_quiz + * @category output + * @copyright 2015 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class navigation_section_heading implements renderable { + /** @var string the heading text. */ + public $heading; + + /** + * Constructor. + * @param string $heading the heading text + */ + public function __construct($heading) { + $this->heading = $heading; + } +} diff --git a/mod/quiz/classes/output/renderer.php b/mod/quiz/classes/output/renderer.php index 895727edc3b..e138ef817d5 100644 --- a/mod/quiz/classes/output/renderer.php +++ b/mod/quiz/classes/output/renderer.php @@ -33,7 +33,6 @@ use question_display_options; use quiz; use quiz_attempt; use quiz_nav_panel_base; -use quiz_nav_section_heading; use renderable; use single_button; use stdClass; @@ -407,10 +406,10 @@ class renderer extends plugin_renderer_base { /** * Display a quiz navigation heading. * - * @param quiz_nav_section_heading $heading the heading. + * @param navigation_section_heading $heading the heading. * @return string HTML fragment. */ - protected function render_quiz_nav_section_heading(quiz_nav_section_heading $heading) { + protected function render_navigation_section_heading(navigation_section_heading $heading) { if (empty($heading->heading)) { $headingtext = get_string('sectionnoname', 'quiz'); $class = ' dimmed_text'; @@ -1455,4 +1454,16 @@ class renderer extends plugin_renderer_base { protected function render_quiz_nav_question_button(navigation_question_button $button) { return $this->render_navigation_question_button($button); } + + /** + * Deprecated version of render_navigation_section_heading. + * + * @param navigation_section_heading $heading the heading. + * @return string HTML fragment. + * @deprecated since Moodle 4.2. Please use render_links_to_other_attempts instead. + * @todo MDL-76612 Final deprecation in Moodle 4.6 + */ + protected function render_quiz_nav_section_heading(navigation_section_heading $heading) { + return $this->render_navigation_section_heading($heading); + } } diff --git a/mod/quiz/db/renamedclasses.php b/mod/quiz/db/renamedclasses.php index 74503b65e65..61803901a6d 100644 --- a/mod/quiz/db/renamedclasses.php +++ b/mod/quiz/db/renamedclasses.php @@ -60,4 +60,5 @@ $renamedclasses = [ 'mod_quiz_view_object' => 'mod_quiz\output\view_page', 'mod_quiz_renderer' => 'mod_quiz\output\renderer', 'quiz_nav_question_button' => 'mod_quiz\output\navigation_question_button', + 'quiz_nav_section_heading' => 'mod_quiz\output\navigation_section_heading', ]; diff --git a/mod/quiz/upgrade.txt b/mod/quiz/upgrade.txt index 808a496e7a8..2915d4860f3 100644 --- a/mod/quiz/upgrade.txt +++ b/mod/quiz/upgrade.txt @@ -44,6 +44,7 @@ This files describes API changes in the quiz code. - mod_quiz_view_object => mod_quiz\output\view_page - mod_quiz_renderer => mod_quiz\output\renderer - quiz_nav_question_button => mod_quiz\output\navigation_question_button + - quiz_nav_section_heading => mod_quiz\output\navigation_section_heading * The following classes have been deprecated: - mod_quiz_overdue_attempt_updater - merged into mod_quiz\task\update_overdue_attempts From 74c6823bac16e479091dd62d82aef9dfa8d99853 Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Mon, 19 Dec 2022 20:08:10 +0000 Subject: [PATCH 19/30] MDL-76614 quiz: move quiz_nav_panel_base to classes folder --- mod/quiz/attemptlib.php | 150 +--------------- .../classes/output/navigation_panel_base.php | 168 ++++++++++++++++++ mod/quiz/classes/output/renderer.php | 5 +- mod/quiz/db/renamedclasses.php | 1 + mod/quiz/upgrade.txt | 1 + 5 files changed, 177 insertions(+), 148 deletions(-) create mode 100644 mod/quiz/classes/output/navigation_panel_base.php diff --git a/mod/quiz/attemptlib.php b/mod/quiz/attemptlib.php index 112d64009d2..b207e98d3a5 100644 --- a/mod/quiz/attemptlib.php +++ b/mod/quiz/attemptlib.php @@ -29,6 +29,7 @@ defined('MOODLE_INTERNAL') || die(); use mod_quiz\access_manager; use mod_quiz\output\links_to_other_attempts; +use mod_quiz\output\navigation_panel_base; use mod_quiz\output\navigation_question_button; use mod_quiz\output\navigation_section_heading; use mod_quiz\output\renderer; @@ -2714,154 +2715,13 @@ class quiz_attempt { /** - * Represents the navigation panel, and builds a {@link block_contents} to allow - * it to be output. + * Specialisation of {@see navigation_panel_base} for the attempt quiz page. * * @copyright 2008 Tim Hunt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 2.0 */ -abstract class quiz_nav_panel_base { - /** @var quiz_attempt */ - protected $attemptobj; - /** @var question_display_options */ - protected $options; - /** @var integer */ - protected $page; - /** @var boolean */ - protected $showall; - - public function __construct(quiz_attempt $attemptobj, - question_display_options $options, $page, $showall) { - $this->attemptobj = $attemptobj; - $this->options = $options; - $this->page = $page; - $this->showall = $showall; - } - - /** - * Get the buttons and section headings to go in the quiz navigation block. - * - * @return renderable[] the buttons, possibly interleaved with section headings. - */ - public function get_question_buttons() { - $buttons = array(); - foreach ($this->attemptobj->get_slots() as $slot) { - $heading = $this->attemptobj->get_heading_before_slot($slot); - if (!is_null($heading)) { - $sections = $this->attemptobj->get_quizobj()->get_sections(); - if (!(empty($heading) && count($sections) == 1)) { - $buttons[] = new navigation_section_heading(format_string($heading)); - } - } - - $qa = $this->attemptobj->get_question_attempt($slot); - $showcorrectness = $this->options->correctness && $qa->has_marks(); - - $button = new navigation_question_button(); - $button->id = 'quiznavbutton' . $slot; - $button->number = $this->attemptobj->get_question_number($slot); - $button->stateclass = $qa->get_state_class($showcorrectness); - $button->navmethod = $this->attemptobj->get_navigation_method(); - if (!$showcorrectness && $button->stateclass === 'notanswered') { - $button->stateclass = 'complete'; - } - $button->statestring = $this->get_state_string($qa, $showcorrectness); - $button->page = $this->attemptobj->get_question_page($slot); - $button->currentpage = $this->showall || $button->page == $this->page; - $button->flagged = $qa->is_flagged(); - $button->url = $this->get_question_url($slot); - if ($this->attemptobj->is_blocked_by_previous_question($slot)) { - $button->url = null; - $button->stateclass = 'blocked'; - $button->statestring = get_string('questiondependsonprevious', 'quiz'); - } - $buttons[] = $button; - } - - return $buttons; - } - - protected function get_state_string(question_attempt $qa, $showcorrectness) { - if ($qa->get_question(false)->length > 0) { - return $qa->get_state_string($showcorrectness); - } - - // Special case handling for 'information' items. - if ($qa->get_state() == question_state::$todo) { - return get_string('notyetviewed', 'quiz'); - } else { - return get_string('viewed', 'quiz'); - } - } - - /** - * Hook for subclasses to override. - * - * @param renderer $output the quiz renderer to use. - * @return string HTML to output. - */ - public function render_before_button_bits(renderer $output) { - return ''; - } - - abstract public function render_end_bits(renderer $output); - - /** - * Render the restart preview button. - * - * @param renderer $output the quiz renderer to use. - * @return string HTML to output. - */ - protected function render_restart_preview_link($output) { - if (!$this->attemptobj->is_own_preview()) { - return ''; - } - return $output->restart_preview_button(new moodle_url( - $this->attemptobj->start_attempt_url(), array('forcenew' => true))); - } - - protected abstract function get_question_url($slot); - - public function user_picture() { - global $DB; - if ($this->attemptobj->get_quiz()->showuserpicture == QUIZ_SHOWIMAGE_NONE) { - return null; - } - $user = $DB->get_record('user', array('id' => $this->attemptobj->get_userid())); - $userpicture = new user_picture($user); - $userpicture->courseid = $this->attemptobj->get_courseid(); - if ($this->attemptobj->get_quiz()->showuserpicture == QUIZ_SHOWIMAGE_LARGE) { - $userpicture->size = true; - } - return $userpicture; - } - - /** - * Return 'allquestionsononepage' as CSS class name when $showall is set, - * otherwise, return 'multipages' as CSS class name. - * - * @return string, CSS class name - */ - public function get_button_container_class() { - // Quiz navigation is set on 'Show all questions on one page'. - if ($this->showall) { - return 'allquestionsononepage'; - } - // Quiz navigation is set on 'Show one page at a time'. - return 'multipages'; - } -} - - -/** - * Specialisation of {@link quiz_nav_panel_base} for the attempt quiz page. - * - * @copyright 2008 Tim Hunt - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @since Moodle 2.0 - */ -class quiz_attempt_nav_panel extends quiz_nav_panel_base { +class quiz_attempt_nav_panel extends navigation_panel_base { public function get_question_url($slot) { if ($this->attemptobj->can_navigate_to($slot)) { return $this->attemptobj->attempt_url($slot, -1, $this->page); @@ -2888,13 +2748,13 @@ class quiz_attempt_nav_panel extends quiz_nav_panel_base { /** - * Specialisation of {@link quiz_nav_panel_base} for the review quiz page. + * Specialisation of {@see navigation_panel_base} for the review quiz page. * * @copyright 2008 Tim Hunt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 2.0 */ -class quiz_review_nav_panel extends quiz_nav_panel_base { +class quiz_review_nav_panel extends navigation_panel_base { public function get_question_url($slot) { return $this->attemptobj->review_url($slot, -1, $this->showall, $this->page); } diff --git a/mod/quiz/classes/output/navigation_panel_base.php b/mod/quiz/classes/output/navigation_panel_base.php new file mode 100644 index 00000000000..aff5f576a5a --- /dev/null +++ b/mod/quiz/classes/output/navigation_panel_base.php @@ -0,0 +1,168 @@ +. + +namespace mod_quiz\output; + +use moodle_url; +use question_attempt; +use question_display_options; +use question_state; +use quiz_attempt; +use renderable; +use user_picture; + +/** + * Represents the navigation panel, and builds a {@link block_contents} to allow it to be output. + * + * This class is not currently renderable or templatable, but it probably should be in the future, + * which is why it is already in the output namespace. + * + * @package mod_quiz + * @category output + * @copyright 2008 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class navigation_panel_base { + /** @var quiz_attempt */ + protected $attemptobj; + /** @var question_display_options */ + protected $options; + /** @var integer */ + protected $page; + /** @var boolean */ + protected $showall; + + public function __construct(quiz_attempt $attemptobj, + question_display_options $options, $page, $showall) { + $this->attemptobj = $attemptobj; + $this->options = $options; + $this->page = $page; + $this->showall = $showall; + } + + /** + * Get the buttons and section headings to go in the quiz navigation block. + * + * @return renderable[] the buttons, possibly interleaved with section headings. + */ + public function get_question_buttons() { + $buttons = array(); + foreach ($this->attemptobj->get_slots() as $slot) { + $heading = $this->attemptobj->get_heading_before_slot($slot); + if (!is_null($heading)) { + $sections = $this->attemptobj->get_quizobj()->get_sections(); + if (!(empty($heading) && count($sections) == 1)) { + $buttons[] = new navigation_section_heading(format_string($heading)); + } + } + + $qa = $this->attemptobj->get_question_attempt($slot); + $showcorrectness = $this->options->correctness && $qa->has_marks(); + + $button = new navigation_question_button(); + $button->id = 'quiznavbutton' . $slot; + $button->number = $this->attemptobj->get_question_number($slot); + $button->stateclass = $qa->get_state_class($showcorrectness); + $button->navmethod = $this->attemptobj->get_navigation_method(); + if (!$showcorrectness && $button->stateclass === 'notanswered') { + $button->stateclass = 'complete'; + } + $button->statestring = $this->get_state_string($qa, $showcorrectness); + $button->page = $this->attemptobj->get_question_page($slot); + $button->currentpage = $this->showall || $button->page == $this->page; + $button->flagged = $qa->is_flagged(); + $button->url = $this->get_question_url($slot); + if ($this->attemptobj->is_blocked_by_previous_question($slot)) { + $button->url = null; + $button->stateclass = 'blocked'; + $button->statestring = get_string('questiondependsonprevious', 'quiz'); + } + $buttons[] = $button; + } + + return $buttons; + } + + protected function get_state_string(question_attempt $qa, $showcorrectness) { + if ($qa->get_question(false)->length > 0) { + return $qa->get_state_string($showcorrectness); + } + + // Special case handling for 'information' items. + if ($qa->get_state() == question_state::$todo) { + return get_string('notyetviewed', 'quiz'); + } else { + return get_string('viewed', 'quiz'); + } + } + + /** + * Hook for subclasses to override. + * + * @param renderer $output the quiz renderer to use. + * @return string HTML to output. + */ + public function render_before_button_bits(renderer $output) { + return ''; + } + + abstract public function render_end_bits(renderer $output); + + /** + * Render the restart preview button. + * + * @param renderer $output the quiz renderer to use. + * @return string HTML to output. + */ + protected function render_restart_preview_link($output) { + if (!$this->attemptobj->is_own_preview()) { + return ''; + } + return $output->restart_preview_button(new moodle_url( + $this->attemptobj->start_attempt_url(), array('forcenew' => true))); + } + + protected abstract function get_question_url($slot); + + public function user_picture() { + global $DB; + if ($this->attemptobj->get_quiz()->showuserpicture == QUIZ_SHOWIMAGE_NONE) { + return null; + } + $user = $DB->get_record('user', array('id' => $this->attemptobj->get_userid())); + $userpicture = new user_picture($user); + $userpicture->courseid = $this->attemptobj->get_courseid(); + if ($this->attemptobj->get_quiz()->showuserpicture == QUIZ_SHOWIMAGE_LARGE) { + $userpicture->size = true; + } + return $userpicture; + } + + /** + * Return 'allquestionsononepage' as CSS class name when $showall is set, + * otherwise, return 'multipages' as CSS class name. + * + * @return string, CSS class name + */ + public function get_button_container_class() { + // Quiz navigation is set on 'Show all questions on one page'. + if ($this->showall) { + return 'allquestionsononepage'; + } + // Quiz navigation is set on 'Show one page at a time'. + return 'multipages'; + } +} diff --git a/mod/quiz/classes/output/renderer.php b/mod/quiz/classes/output/renderer.php index e138ef817d5..9c691d899d7 100644 --- a/mod/quiz/classes/output/renderer.php +++ b/mod/quiz/classes/output/renderer.php @@ -32,7 +32,6 @@ use popup_action; use question_display_options; use quiz; use quiz_attempt; -use quiz_nav_panel_base; use renderable; use single_button; use stdClass; @@ -325,9 +324,9 @@ class renderer extends plugin_renderer_base { /** * Outputs the navigation block panel * - * @param quiz_nav_panel_base $panel instance of quiz_nav_panel_base + * @param navigation_panel_base $panel */ - public function navigation_panel(quiz_nav_panel_base $panel) { + public function navigation_panel(navigation_panel_base $panel) { $output = ''; $userpicture = $panel->user_picture(); diff --git a/mod/quiz/db/renamedclasses.php b/mod/quiz/db/renamedclasses.php index 61803901a6d..c2d3605782d 100644 --- a/mod/quiz/db/renamedclasses.php +++ b/mod/quiz/db/renamedclasses.php @@ -61,4 +61,5 @@ $renamedclasses = [ 'mod_quiz_renderer' => 'mod_quiz\output\renderer', 'quiz_nav_question_button' => 'mod_quiz\output\navigation_question_button', 'quiz_nav_section_heading' => 'mod_quiz\output\navigation_section_heading', + 'quiz_nav_panel_base' => 'mod_quiz\output\navigation_panel_base', ]; diff --git a/mod/quiz/upgrade.txt b/mod/quiz/upgrade.txt index 2915d4860f3..c1cb5192b3c 100644 --- a/mod/quiz/upgrade.txt +++ b/mod/quiz/upgrade.txt @@ -45,6 +45,7 @@ This files describes API changes in the quiz code. - mod_quiz_renderer => mod_quiz\output\renderer - quiz_nav_question_button => mod_quiz\output\navigation_question_button - quiz_nav_section_heading => mod_quiz\output\navigation_section_heading + - quiz_nav_panel_base => mod_quiz\output\navigation_panel_base * The following classes have been deprecated: - mod_quiz_overdue_attempt_updater - merged into mod_quiz\task\update_overdue_attempts From adbd0daddbb96eeb48d01f059332f3da6ed7722a Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Mon, 19 Dec 2022 20:38:51 +0000 Subject: [PATCH 20/30] MDL-76614 quiz: clean up navigation_panel_base class --- .../classes/output/navigation_panel_base.php | 39 +++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/mod/quiz/classes/output/navigation_panel_base.php b/mod/quiz/classes/output/navigation_panel_base.php index aff5f576a5a..057729f9415 100644 --- a/mod/quiz/classes/output/navigation_panel_base.php +++ b/mod/quiz/classes/output/navigation_panel_base.php @@ -16,6 +16,7 @@ namespace mod_quiz\output; +use moodle_page; use moodle_url; use question_attempt; use question_display_options; @@ -25,7 +26,7 @@ use renderable; use user_picture; /** - * Represents the navigation panel, and builds a {@link block_contents} to allow it to be output. + * Represents the navigation panel, and builds a {@see block_contents} to allow it to be output. * * This class is not currently renderable or templatable, but it probably should be in the future, * which is why it is already in the output namespace. @@ -45,6 +46,14 @@ abstract class navigation_panel_base { /** @var boolean */ protected $showall; + /** + * Constructor. + * + * @param quiz_attempt $attemptobj construct the panel for this attempt. + * @param question_display_options $options display options in force. + * @param int $page which page of the quiz attempt is being shown, -1 if all. + * @param bool $showall whether all pages are being shown at once. + */ public function __construct(quiz_attempt $attemptobj, question_display_options $options, $page, $showall) { $this->attemptobj = $attemptobj; @@ -96,6 +105,13 @@ abstract class navigation_panel_base { return $buttons; } + /** + * Get the human-readable description of the current state of a particular question. + * + * @param question_attempt $qa the attempt at the question of interest. + * @param bool $showcorrectness whether the current use is allowed to see if they have got the question right. + * @return string Human-readable description of the state. + */ protected function get_state_string(question_attempt $qa, $showcorrectness) { if ($qa->get_question(false)->length > 0) { return $qa->get_state_string($showcorrectness); @@ -110,7 +126,7 @@ abstract class navigation_panel_base { } /** - * Hook for subclasses to override. + * Hook for subclasses to override to do output above the question buttons. * * @param renderer $output the quiz renderer to use. * @return string HTML to output. @@ -119,6 +135,12 @@ abstract class navigation_panel_base { return ''; } + /** + * Hook that subclasses must override to do output after the question buttons. + * + * @param renderer $output the quiz renderer to use. + * @return string HTML to output. + */ abstract public function render_end_bits(renderer $output); /** @@ -135,8 +157,19 @@ abstract class navigation_panel_base { $this->attemptobj->start_attempt_url(), array('forcenew' => true))); } - protected abstract function get_question_url($slot); + /** + * Get the URL to navigate to a particular question. + * + * @param int $slot slot number, to identify the question. + * @return moodle_url|null URL if the user can navigate there, or null if they cannot. + */ + abstract protected function get_question_url($slot); + /** + * Get the user picture which should be displayed, if required. + * + * @return user_picture|null + */ public function user_picture() { global $DB; if ($this->attemptobj->get_quiz()->showuserpicture == QUIZ_SHOWIMAGE_NONE) { From ccbb5da7848352caab7406b9b7ea826d2e5f074b Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Mon, 19 Dec 2022 20:21:37 +0000 Subject: [PATCH 21/30] MDL-76614 quiz: move quiz_attempt_nav_panel to classes --- mod/quiz/attempt.php | 6 +- mod/quiz/attemptlib.php | 37 +------------ .../output/navigation_panel_attempt.php | 55 +++++++++++++++++++ mod/quiz/db/renamedclasses.php | 1 + mod/quiz/summary.php | 5 +- mod/quiz/upgrade.txt | 1 + 6 files changed, 67 insertions(+), 38 deletions(-) create mode 100644 mod/quiz/classes/output/navigation_panel_attempt.php diff --git a/mod/quiz/attempt.php b/mod/quiz/attempt.php index 4375498aab3..9cd4e0e286c 100644 --- a/mod/quiz/attempt.php +++ b/mod/quiz/attempt.php @@ -22,6 +22,9 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +use mod_quiz\output\navigation_panel_attempt; +use mod_quiz\output\renderer; + require_once(__DIR__ . '/../../config.php'); require_once($CFG->dirroot . '/mod/quiz/locallib.php'); @@ -82,6 +85,7 @@ if ($attemptobj->is_finished()) { // Check the access rules. $accessmanager = $attemptobj->get_access_manager(time()); $accessmanager->setup_attempt_page($PAGE); +/** @var renderer $output */ $output = $PAGE->get_renderer('mod_quiz'); $messages = $accessmanager->prevent_access(); if (!$attemptobj->is_preview_user() && $messages) { @@ -121,7 +125,7 @@ $PAGE->requires->js_init_call('M.mod_quiz.init_attempt_form', null, false, quiz_ \core\session\manager::keepalive(); // Try to prevent sessions expiring during quiz attempts. // Arrange for the navigation to be displayed in the first region on the page. -$navbc = $attemptobj->get_navigation_panel($output, 'quiz_attempt_nav_panel', $page); +$navbc = $attemptobj->get_navigation_panel($output, navigation_panel_attempt::class, $page); $regions = $PAGE->blocks->get_regions(); $PAGE->blocks->add_fake_block($navbc, reset($regions)); diff --git a/mod/quiz/attemptlib.php b/mod/quiz/attemptlib.php index b207e98d3a5..12ce2c69223 100644 --- a/mod/quiz/attemptlib.php +++ b/mod/quiz/attemptlib.php @@ -30,8 +30,6 @@ defined('MOODLE_INTERNAL') || die(); use mod_quiz\access_manager; use mod_quiz\output\links_to_other_attempts; use mod_quiz\output\navigation_panel_base; -use mod_quiz\output\navigation_question_button; -use mod_quiz\output\navigation_section_heading; use mod_quiz\output\renderer; use mod_quiz\question\bank\qbank_helper; use mod_quiz\question\display_options; @@ -1872,7 +1870,7 @@ class quiz_attempt { * Get the navigation panel object for this attempt. * * @param renderer $output the quiz renderer to use to output things. - * @param string $panelclass The type of panel, quiz_attempt_nav_panel or quiz_review_nav_panel + * @param string $panelclass The type of panel, navigation_panel_attempt::class or quiz_review_nav_panel * @param int $page the current page number. * @param bool $showall whether we are showing the whole quiz on one page. (Used by review.php.) * @return block_contents the requested object. @@ -2714,39 +2712,6 @@ class quiz_attempt { } -/** - * Specialisation of {@see navigation_panel_base} for the attempt quiz page. - * - * @copyright 2008 Tim Hunt - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @since Moodle 2.0 - */ -class quiz_attempt_nav_panel extends navigation_panel_base { - public function get_question_url($slot) { - if ($this->attemptobj->can_navigate_to($slot)) { - return $this->attemptobj->attempt_url($slot, -1, $this->page); - } else { - return null; - } - } - - public function render_before_button_bits(renderer $output) { - return html_writer::tag('div', get_string('navnojswarning', 'quiz'), - array('id' => 'quiznojswarning')); - } - - public function render_end_bits(renderer $output) { - if ($this->page == -1) { - // Don't link from the summary page to itself. - return ''; - } - return html_writer::link($this->attemptobj->summary_url(), - get_string('endtest', 'quiz'), array('class' => 'endtestlink aalink')) . - $this->render_restart_preview_link($output); - } -} - - /** * Specialisation of {@see navigation_panel_base} for the review quiz page. * diff --git a/mod/quiz/classes/output/navigation_panel_attempt.php b/mod/quiz/classes/output/navigation_panel_attempt.php new file mode 100644 index 00000000000..1fd6fbfc80a --- /dev/null +++ b/mod/quiz/classes/output/navigation_panel_attempt.php @@ -0,0 +1,55 @@ +. + +namespace mod_quiz\output; + +use html_writer; + +/** + * Specialisation of {@see navigation_panel_base} for the attempt quiz page. + * + * This class is not currently renderable or templatable, but it probably should be in the future, + * which is why it is already in the output namespace. + * + * @package mod_quiz + * @category output + * @copyright 2008 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class navigation_panel_attempt extends navigation_panel_base { + public function get_question_url($slot) { + if ($this->attemptobj->can_navigate_to($slot)) { + return $this->attemptobj->attempt_url($slot, -1, $this->page); + } else { + return null; + } + } + + public function render_before_button_bits(renderer $output) { + return html_writer::tag('div', get_string('navnojswarning', 'quiz'), + array('id' => 'quiznojswarning')); + } + + public function render_end_bits(renderer $output) { + if ($this->page == -1) { + // Don't link from the summary page to itself. + return ''; + } + return html_writer::link($this->attemptobj->summary_url(), + get_string('endtest', 'quiz'), array('class' => 'endtestlink aalink')) . + $this->render_restart_preview_link($output); + } +} diff --git a/mod/quiz/db/renamedclasses.php b/mod/quiz/db/renamedclasses.php index c2d3605782d..1e79ecdead5 100644 --- a/mod/quiz/db/renamedclasses.php +++ b/mod/quiz/db/renamedclasses.php @@ -62,4 +62,5 @@ $renamedclasses = [ 'quiz_nav_question_button' => 'mod_quiz\output\navigation_question_button', 'quiz_nav_section_heading' => 'mod_quiz\output\navigation_section_heading', 'quiz_nav_panel_base' => 'mod_quiz\output\navigation_panel_base', + 'quiz_attempt_nav_panel' => 'mod_quiz\output\navigation_panel_attempt', ]; diff --git a/mod/quiz/summary.php b/mod/quiz/summary.php index dc429195f8d..f3c22ac05df 100644 --- a/mod/quiz/summary.php +++ b/mod/quiz/summary.php @@ -22,6 +22,8 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +use mod_quiz\output\navigation_panel_attempt; +use mod_quiz\output\renderer; require_once(__DIR__ . '/../../config.php'); require_once($CFG->dirroot . '/mod/quiz/locallib.php'); @@ -60,6 +62,7 @@ if ($attemptobj->is_preview_user()) { // Check access. $accessmanager = $attemptobj->get_access_manager(time()); $accessmanager->setup_attempt_page($PAGE); +/** @var renderer $output */ $output = $PAGE->get_renderer('mod_quiz'); $messages = $accessmanager->prevent_access(); if (!$attemptobj->is_preview_user() && $messages) { @@ -87,7 +90,7 @@ if (empty($attemptobj->get_quiz()->showblocks)) { $PAGE->blocks->show_only_fake_blocks(); } -$navbc = $attemptobj->get_navigation_panel($output, 'quiz_attempt_nav_panel', -1); +$navbc = $attemptobj->get_navigation_panel($output, navigation_panel_attempt::class, -1); $regions = $PAGE->blocks->get_regions(); $PAGE->blocks->add_fake_block($navbc, reset($regions)); diff --git a/mod/quiz/upgrade.txt b/mod/quiz/upgrade.txt index c1cb5192b3c..cee3520cabc 100644 --- a/mod/quiz/upgrade.txt +++ b/mod/quiz/upgrade.txt @@ -46,6 +46,7 @@ This files describes API changes in the quiz code. - quiz_nav_question_button => mod_quiz\output\navigation_question_button - quiz_nav_section_heading => mod_quiz\output\navigation_section_heading - quiz_nav_panel_base => mod_quiz\output\navigation_panel_base + - quiz_attempt_nav_panel => mod_quiz\output\navigation_panel_attempt * The following classes have been deprecated: - mod_quiz_overdue_attempt_updater - merged into mod_quiz\task\update_overdue_attempts From 03116995d5b04bab9730c86ea909d6ec6e5a7168 Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Mon, 19 Dec 2022 20:25:32 +0000 Subject: [PATCH 22/30] MDL-76614 quiz: move quiz_review_nav_panel to classes --- mod/quiz/attemptlib.php | 32 +----------- .../output/navigation_panel_review.php | 52 +++++++++++++++++++ mod/quiz/db/renamedclasses.php | 1 + mod/quiz/review.php | 5 +- mod/quiz/upgrade.txt | 1 + 5 files changed, 59 insertions(+), 32 deletions(-) create mode 100644 mod/quiz/classes/output/navigation_panel_review.php diff --git a/mod/quiz/attemptlib.php b/mod/quiz/attemptlib.php index 12ce2c69223..9e9ae61f595 100644 --- a/mod/quiz/attemptlib.php +++ b/mod/quiz/attemptlib.php @@ -1870,7 +1870,7 @@ class quiz_attempt { * Get the navigation panel object for this attempt. * * @param renderer $output the quiz renderer to use to output things. - * @param string $panelclass The type of panel, navigation_panel_attempt::class or quiz_review_nav_panel + * @param string $panelclass The type of panel, navigation_panel_attempt::class or navigation_panel_review::class * @param int $page the current page number. * @param bool $showall whether we are showing the whole quiz on one page. (Used by review.php.) * @return block_contents the requested object. @@ -2710,33 +2710,3 @@ class quiz_attempt { return $totalunanswered; } } - - -/** - * Specialisation of {@see navigation_panel_base} for the review quiz page. - * - * @copyright 2008 Tim Hunt - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @since Moodle 2.0 - */ -class quiz_review_nav_panel extends navigation_panel_base { - public function get_question_url($slot) { - return $this->attemptobj->review_url($slot, -1, $this->showall, $this->page); - } - - public function render_end_bits(renderer $output) { - $html = ''; - if ($this->attemptobj->get_num_pages() > 1) { - if ($this->showall) { - $html .= html_writer::link($this->attemptobj->review_url(null, 0, false), - get_string('showeachpage', 'quiz')); - } else { - $html .= html_writer::link($this->attemptobj->review_url(null, 0, true), - get_string('showall', 'quiz')); - } - } - $html .= $output->finish_review_link($this->attemptobj); - $html .= $this->render_restart_preview_link($output); - return $html; - } -} diff --git a/mod/quiz/classes/output/navigation_panel_review.php b/mod/quiz/classes/output/navigation_panel_review.php new file mode 100644 index 00000000000..d07de8e4172 --- /dev/null +++ b/mod/quiz/classes/output/navigation_panel_review.php @@ -0,0 +1,52 @@ +. + +namespace mod_quiz\output; + +use html_writer; + +/** + * Specialisation of {@see navigation_panel_base} for the review quiz page. + * + * This class is not currently renderable or templatable, but it probably should be in the future, + * which is why it is already in the output namespace. + * + * @package mod_quiz + * @category output + * @copyright 2008 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class navigation_panel_review extends navigation_panel_base { + public function get_question_url($slot) { + return $this->attemptobj->review_url($slot, -1, $this->showall, $this->page); + } + + public function render_end_bits(renderer $output) { + $html = ''; + if ($this->attemptobj->get_num_pages() > 1) { + if ($this->showall) { + $html .= html_writer::link($this->attemptobj->review_url(null, 0, false), + get_string('showeachpage', 'quiz')); + } else { + $html .= html_writer::link($this->attemptobj->review_url(null, 0, true), + get_string('showall', 'quiz')); + } + } + $html .= $output->finish_review_link($this->attemptobj); + $html .= $this->render_restart_preview_link($output); + return $html; + } +} diff --git a/mod/quiz/db/renamedclasses.php b/mod/quiz/db/renamedclasses.php index 1e79ecdead5..7fac769cd1b 100644 --- a/mod/quiz/db/renamedclasses.php +++ b/mod/quiz/db/renamedclasses.php @@ -63,4 +63,5 @@ $renamedclasses = [ 'quiz_nav_section_heading' => 'mod_quiz\output\navigation_section_heading', 'quiz_nav_panel_base' => 'mod_quiz\output\navigation_panel_base', 'quiz_attempt_nav_panel' => 'mod_quiz\output\navigation_panel_attempt', + 'quiz_review_nav_panel' => 'mod_quiz\output\navigation_panel_review', ]; diff --git a/mod/quiz/review.php b/mod/quiz/review.php index b230b6eb590..ddb9d25fe00 100644 --- a/mod/quiz/review.php +++ b/mod/quiz/review.php @@ -25,6 +25,8 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +use mod_quiz\output\navigation_panel_review; +use mod_quiz\output\renderer; require_once(__DIR__ . '/../../config.php'); require_once($CFG->dirroot . '/mod/quiz/locallib.php'); @@ -254,10 +256,11 @@ if ($showall) { $lastpage = $attemptobj->is_last_page($page); } +/** @var renderer $output */ $output = $PAGE->get_renderer('mod_quiz'); // Arrange for the navigation to be displayed. -$navbc = $attemptobj->get_navigation_panel($output, 'quiz_review_nav_panel', $page, $showall); +$navbc = $attemptobj->get_navigation_panel($output, navigation_panel_review::class, $page, $showall); $regions = $PAGE->blocks->get_regions(); $PAGE->blocks->add_fake_block($navbc, reset($regions)); diff --git a/mod/quiz/upgrade.txt b/mod/quiz/upgrade.txt index cee3520cabc..3d868bb67b4 100644 --- a/mod/quiz/upgrade.txt +++ b/mod/quiz/upgrade.txt @@ -47,6 +47,7 @@ This files describes API changes in the quiz code. - quiz_nav_section_heading => mod_quiz\output\navigation_section_heading - quiz_nav_panel_base => mod_quiz\output\navigation_panel_base - quiz_attempt_nav_panel => mod_quiz\output\navigation_panel_attempt + - quiz_review_nav_panel => mod_quiz\output\navigation_panel_review * The following classes have been deprecated: - mod_quiz_overdue_attempt_updater - merged into mod_quiz\task\update_overdue_attempts From 79a29d243c3009eec77f7caf77335133304f8d54 Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Mon, 19 Dec 2022 20:38:16 +0000 Subject: [PATCH 23/30] MDL-76614 quiz: remove unneeded MOODLE_INTERNAL --- mod/quiz/classes/output/question_chooser.php | 1 - 1 file changed, 1 deletion(-) diff --git a/mod/quiz/classes/output/question_chooser.php b/mod/quiz/classes/output/question_chooser.php index f58ffe114e2..06e101ab0a5 100644 --- a/mod/quiz/classes/output/question_chooser.php +++ b/mod/quiz/classes/output/question_chooser.php @@ -23,7 +23,6 @@ */ namespace mod_quiz\output; -defined('MOODLE_INTERNAL') || die(); /** * The question_chooser renderable class. From 5682c8c70ee29511feab98074c3eac6f1d328bf9 Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Mon, 19 Dec 2022 21:14:51 +0000 Subject: [PATCH 24/30] MDL-76614 quiz: move class quiz_attempt => mod_quiz\quiz_attempt I thought about renaming the class to just plain attempt, but I acutally think quiz_attempt makes it clearer what this is. Also not changing the name massively reduces the pain for everyone updating their code (including me right now!) --- .../tool/recyclebin/tests/course_bin_test.php | 8 +- .../tests/restore_stepslib_date_test.php | 6 +- mod/quiz/accessrule/seb/rule.php | 1 + .../seb/tests/test_helper_trait.php | 5 +- mod/quiz/attempt.php | 1 + mod/quiz/attemptlib.php | 2160 +--------------- mod/quiz/classes/access_manager.php | 1 - mod/quiz/classes/external.php | 1 + .../local/reports/attempts_report_options.php | 2 +- .../local/reports/attempts_report_table.php | 2 +- .../output/links_to_other_attempts.php | 2 +- .../classes/output/navigation_panel_base.php | 3 +- mod/quiz/classes/output/renderer.php | 2 +- mod/quiz/classes/output/view_page.php | 3 +- mod/quiz/classes/privacy/provider.php | 3 +- .../classes/question/qubaids_for_quiz.php | 4 +- .../question/qubaids_for_users_attempts.php | 10 +- mod/quiz/classes/quiz_attempt.php | 2196 +++++++++++++++++ ...otify_attempt_manual_grading_completed.php | 2 +- .../classes/task/update_overdue_attempts.php | 2 +- mod/quiz/db/renamedclasses.php | 1 + mod/quiz/grade.php | 1 + mod/quiz/lib.php | 8 +- mod/quiz/locallib.php | 5 +- mod/quiz/processattempt.php | 2 + mod/quiz/report/grading/report.php | 1 + mod/quiz/report/overview/overview_table.php | 1 + mod/quiz/report/overview/report.php | 1 + .../report/overview/tests/report_test.php | 2 +- .../first_or_all_responses_table.php | 2 + .../report/responses/last_responses_table.php | 2 +- .../responses_from_steps_walkthrough_test.php | 2 +- .../statistics/classes/task/recalculate.php | 2 +- mod/quiz/report/statistics/statisticslib.php | 2 + mod/quiz/review.php | 1 + mod/quiz/startattempt.php | 2 + mod/quiz/tests/attempt_test.php | 1 - .../attempt_walkthrough_from_csv_test.php | 1 - mod/quiz/tests/attempt_walkthrough_test.php | 1 - mod/quiz/tests/behat/behat_mod_quiz.php | 1 + mod/quiz/tests/custom_completion_test.php | 1 - mod/quiz/tests/event/events_test.php | 2 +- mod/quiz/tests/external/external_test.php | 2 +- mod/quiz/tests/generator/lib.php | 2 + mod/quiz/tests/lib_test.php | 3 - mod/quiz/tests/locallib_test.php | 1 - mod/quiz/tests/privacy/provider_test.php | 15 +- ..._attempt_manual_grading_completed_test.php | 1 - .../tests/quiz_question_helper_test_trait.php | 1 + mod/quiz/tests/reportlib_test.php | 2 - mod/quiz/upgrade.txt | 1 + mod/quiz/view.php | 1 + .../bank/statistics/tests/helper_test.php | 2 +- question/bank/usage/tests/helper_test.php | 4 +- .../bank/usage/tests/question_usage_test.php | 4 +- 55 files changed, 2279 insertions(+), 2216 deletions(-) create mode 100644 mod/quiz/classes/quiz_attempt.php diff --git a/admin/tool/recyclebin/tests/course_bin_test.php b/admin/tool/recyclebin/tests/course_bin_test.php index 821ceedf749..10a2427c98d 100644 --- a/admin/tool/recyclebin/tests/course_bin_test.php +++ b/admin/tool/recyclebin/tests/course_bin_test.php @@ -16,6 +16,8 @@ namespace tool_recyclebin; +use mod_quiz\quiz_attempt; + /** * Recycle bin course tests. * @@ -237,7 +239,7 @@ class course_bin_test extends \advanced_testcase { $attempts = quiz_get_user_attempts($cm->instance, $student->id); $this->assertEquals(1, count($attempts)); $attempt = array_pop($attempts); - $attemptobj = \quiz_attempt::create($attempt->id); + $attemptobj = quiz_attempt::create($attempt->id); $this->assertEquals($student->id, $attemptobj->get_userid()); $this->assertEquals(true, $attemptobj->is_finished()); } @@ -307,10 +309,10 @@ class course_bin_test extends \advanced_testcase { $attempt = quiz_create_attempt($quizobj, 1, false, $timenow, false, $student->id); quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow); quiz_attempt_save_started($quizobj, $quba, $attempt); - $attemptobj = \quiz_attempt::create($attempt->id); + $attemptobj = quiz_attempt::create($attempt->id); $tosubmit = array(1 => array('answer' => '0')); $attemptobj->process_submitted_actions($timenow, false, $tosubmit); - $attemptobj = \quiz_attempt::create($attempt->id); + $attemptobj = quiz_attempt::create($attempt->id); $attemptobj->process_finish($timenow, false); } } diff --git a/backup/moodle2/tests/restore_stepslib_date_test.php b/backup/moodle2/tests/restore_stepslib_date_test.php index 8ff878c55d3..009f57d3626 100644 --- a/backup/moodle2/tests/restore_stepslib_date_test.php +++ b/backup/moodle2/tests/restore_stepslib_date_test.php @@ -16,6 +16,8 @@ namespace core_backup; +use mod_quiz\quiz_attempt; + defined('MOODLE_INTERNAL') || die(); global $CFG; @@ -393,7 +395,7 @@ class restore_stepslib_date_test extends \restore_date_testcase { quiz_attempt_save_started($quizobj, $quba, $attempt); // Process some responses from the student. - $attemptobj = \quiz_attempt::create($attempt->id); + $attemptobj = quiz_attempt::create($attempt->id); $prefix1 = $quba->get_field_prefix(1); $prefix2 = $quba->get_field_prefix(2); @@ -404,7 +406,7 @@ class restore_stepslib_date_test extends \restore_date_testcase { $attemptobj->process_submitted_actions($timenow, false, $tosubmit); // Finish the attempt. - $attemptobj = \quiz_attempt::create($attempt->id); + $attemptobj = quiz_attempt::create($attempt->id); $attemptobj->process_finish($timenow, false); $questionattemptstepdates = []; diff --git a/mod/quiz/accessrule/seb/rule.php b/mod/quiz/accessrule/seb/rule.php index 27cc12cfe5b..efd8eef516d 100644 --- a/mod/quiz/accessrule/seb/rule.php +++ b/mod/quiz/accessrule/seb/rule.php @@ -15,6 +15,7 @@ // along with Moodle. If not, see . use mod_quiz\local\access_rule_base; +use mod_quiz\quiz_attempt; use quizaccess_seb\access_manager; use quizaccess_seb\quiz_settings; use quizaccess_seb\settings_provider; diff --git a/mod/quiz/accessrule/seb/tests/test_helper_trait.php b/mod/quiz/accessrule/seb/tests/test_helper_trait.php index b4ee1137734..564f916bd5f 100644 --- a/mod/quiz/accessrule/seb/tests/test_helper_trait.php +++ b/mod/quiz/accessrule/seb/tests/test_helper_trait.php @@ -24,6 +24,7 @@ */ use mod_quiz\local\access_rule_base; +use mod_quiz\quiz_attempt; use quizaccess_seb\access_manager; use quizaccess_seb\settings_provider; @@ -201,7 +202,7 @@ trait quizaccess_seb_test_helper_trait { quiz_attempt_save_started($quizobj, $quba, $attempt); // Answer the questions. - $attemptobj = \quiz_attempt::create($attempt->id); + $attemptobj = quiz_attempt::create($attempt->id); $tosubmit = [ 1 => ['answer' => 'frog'], @@ -211,7 +212,7 @@ trait quizaccess_seb_test_helper_trait { $attemptobj->process_submitted_actions($starttime, false, $tosubmit); // Finish the attempt. - $attemptobj = \quiz_attempt::create($attempt->id); + $attemptobj = quiz_attempt::create($attempt->id); $attemptobj->process_finish($starttime, false); $this->setUser(); diff --git a/mod/quiz/attempt.php b/mod/quiz/attempt.php index 9cd4e0e286c..163b8277ca4 100644 --- a/mod/quiz/attempt.php +++ b/mod/quiz/attempt.php @@ -24,6 +24,7 @@ use mod_quiz\output\navigation_panel_attempt; use mod_quiz\output\renderer; +use mod_quiz\quiz_attempt; require_once(__DIR__ . '/../../config.php'); require_once($CFG->dirroot . '/mod/quiz/locallib.php'); diff --git a/mod/quiz/attemptlib.php b/mod/quiz/attemptlib.php index 9e9ae61f595..a5c61f52f04 100644 --- a/mod/quiz/attemptlib.php +++ b/mod/quiz/attemptlib.php @@ -33,6 +33,7 @@ use mod_quiz\output\navigation_panel_base; use mod_quiz\output\renderer; use mod_quiz\question\bank\qbank_helper; use mod_quiz\question\display_options; +use mod_quiz\quiz_attempt; /** * A class encapsulating a quiz and the questions it contains, and making the @@ -111,7 +112,7 @@ class quiz { } /** - * Create a {@link quiz_attempt} for an attempt at this quiz. + * Create a {@see quiz_attempt} for an attempt at this quiz. * * @param object $attemptdata row from the quiz_attempts table. * @return quiz_attempt the new quiz_attempt object. @@ -553,2160 +554,3 @@ class quiz { return $questiontypes; } } - - -/** - * This class extends the quiz class to hold data about the state of a particular attempt, - * in addition to the data about the quiz. - * - * @copyright 2008 Tim Hunt - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @since Moodle 2.0 - */ -class quiz_attempt { - - /** @var string to identify the in progress state. */ - const IN_PROGRESS = 'inprogress'; - /** @var string to identify the overdue state. */ - const OVERDUE = 'overdue'; - /** @var string to identify the finished state. */ - const FINISHED = 'finished'; - /** @var string to identify the abandoned state. */ - const ABANDONED = 'abandoned'; - - /** @var int maximum number of slots in the quiz for the review page to default to show all. */ - const MAX_SLOTS_FOR_DEFAULT_REVIEW_SHOW_ALL = 50; - - /** @var quiz object containing the quiz settings. */ - protected $quizobj; - - /** @var stdClass the quiz_attempts row. */ - protected $attempt; - - /** @var question_usage_by_activity the question usage for this quiz attempt. */ - protected $quba; - - /** - * @var array of slot information. These objects contain ->slot (int), - * ->requireprevious (bool), ->questionids (int) the original question for random questions, - * ->firstinsection (bool), ->section (stdClass from $this->sections). - * This does not contain page - get that from {@link get_question_page()} - - * or maxmark - get that from $this->quba. - */ - protected $slots; - - /** @var array of quiz_sections rows, with a ->lastslot field added. */ - protected $sections; - - /** @var array page no => array of slot numbers on the page in order. */ - protected $pagelayout; - - /** @var array slot => displayed question number for this slot. (E.g. 1, 2, 3 or 'i'.) */ - protected $questionnumbers; - - /** @var array slot => page number for this slot. */ - protected $questionpages; - - /** @var display_options cache for the appropriate review options. */ - protected $reviewoptions = null; - - // Constructor ============================================================= - /** - * Constructor assuming we already have the necessary data loaded. - * - * @param object $attempt the row of the quiz_attempts table. - * @param object $quiz the quiz object for this attempt and user. - * @param object $cm the course_module object for this quiz. - * @param object $course the row from the course table for the course we belong to. - * @param bool $loadquestions (optional) if true, the default, load all the details - * of the state of each question. Else just set up the basic details of the attempt. - */ - public function __construct($attempt, $quiz, $cm, $course, $loadquestions = true) { - $this->attempt = $attempt; - $this->quizobj = new quiz($quiz, $cm, $course); - - if ($loadquestions) { - $this->load_questions(); - } - } - - /** - * Used by {create()} and {create_from_usage_id()}. - * - * @param array $conditions passed to $DB->get_record('quiz_attempts', $conditions). - * @return quiz_attempt the desired instance of this class. - */ - protected static function create_helper($conditions) { - global $DB; - - $attempt = $DB->get_record('quiz_attempts', $conditions, '*', MUST_EXIST); - $quiz = access_manager::load_quiz_and_settings($attempt->quiz); - $course = $DB->get_record('course', array('id' => $quiz->course), '*', MUST_EXIST); - $cm = get_coursemodule_from_instance('quiz', $quiz->id, $course->id, false, MUST_EXIST); - - // Update quiz with override information. - $quiz = quiz_update_effective_access($quiz, $attempt->userid); - - return new quiz_attempt($attempt, $quiz, $cm, $course); - } - - /** - * Static function to create a new quiz_attempt object given an attemptid. - * - * @param int $attemptid the attempt id. - * @return quiz_attempt the new quiz_attempt object - */ - public static function create($attemptid) { - return self::create_helper(array('id' => $attemptid)); - } - - /** - * Static function to create a new quiz_attempt object given a usage id. - * - * @param int $usageid the attempt usage id. - * @return quiz_attempt the new quiz_attempt object - */ - public static function create_from_usage_id($usageid) { - return self::create_helper(array('uniqueid' => $usageid)); - } - - /** - * @param string $state one of the state constants like IN_PROGRESS. - * @return string the human-readable state name. - */ - public static function state_name($state) { - return quiz_attempt_state_name($state); - } - - /** - * This method can be called later if the object was constructed with $loadqusetions = false. - */ - public function load_questions() { - global $DB; - - if (isset($this->quba)) { - throw new coding_exception('This quiz attempt has already had the questions loaded.'); - } - - $this->quba = question_engine::load_questions_usage_by_activity($this->attempt->uniqueid); - $this->slots = $DB->get_records('quiz_slots', - ['quizid' => $this->get_quizid()], 'slot', 'slot, id, requireprevious, displaynumber'); - $this->sections = array_values($DB->get_records('quiz_sections', - ['quizid' => $this->get_quizid()], 'firstslot')); - - $this->link_sections_and_slots(); - $this->determine_layout(); - $this->number_questions(); - } - - /** - * Preload all attempt step users to show in Response history. - * - * @throws dml_exception - */ - public function preload_all_attempt_step_users(): void { - $this->quba->preload_all_step_users(); - } - - /** - * Let each slot know which section it is part of. - */ - protected function link_sections_and_slots() { - foreach ($this->sections as $i => $section) { - if (isset($this->sections[$i + 1])) { - $section->lastslot = $this->sections[$i + 1]->firstslot - 1; - } else { - $section->lastslot = count($this->slots); - } - for ($slot = $section->firstslot; $slot <= $section->lastslot; $slot += 1) { - $this->slots[$slot]->section = $section; - } - } - } - - /** - * Parse attempt->layout to populate the other arrays the represent the layout. - */ - protected function determine_layout() { - $this->pagelayout = array(); - - // Break up the layout string into pages. - $pagelayouts = explode(',0', $this->attempt->layout); - - // Strip off any empty last page (normally there is one). - if (end($pagelayouts) == '') { - array_pop($pagelayouts); - } - - // File the ids into the arrays. - // Tracking which is the first slot in each section in this attempt is - // trickier than you might guess, since the slots in this section - // may be shuffled, so $section->firstslot (the lowest numbered slot in - // the section) may not be the first one. - $unseensections = $this->sections; - $this->pagelayout = array(); - foreach ($pagelayouts as $page => $pagelayout) { - $pagelayout = trim($pagelayout, ','); - if ($pagelayout == '') { - continue; - } - $this->pagelayout[$page] = explode(',', $pagelayout); - foreach ($this->pagelayout[$page] as $slot) { - $sectionkey = array_search($this->slots[$slot]->section, $unseensections); - if ($sectionkey !== false) { - $this->slots[$slot]->firstinsection = true; - unset($unseensections[$sectionkey]); - } else { - $this->slots[$slot]->firstinsection = false; - } - } - } - } - - /** - * Work out the number to display for each question/slot. - */ - protected function number_questions() { - $number = 1; - foreach ($this->pagelayout as $page => $slots) { - foreach ($slots as $slot) { - if ($length = $this->is_real_question($slot)) { - // Whether question numbering is customised or is numeric and automatically incremented. - if (!empty($this->slots[$slot]->displaynumber) && !is_null($this->slots[$slot]->displaynumber)) { - $this->questionnumbers[$slot] = $this->slots[$slot]->displaynumber; - } else { - $this->questionnumbers[$slot] = $number; - } - $number += $length; - } else { - $this->questionnumbers[$slot] = get_string('infoshort', 'quiz'); - } - $this->questionpages[$slot] = $page; - } - } - } - - /** - * If the given page number is out of range (before the first page, or after - * the last page, chnage it to be within range). - * - * @param int $page the requested page number. - * @return int a safe page number to use. - */ - public function force_page_number_into_range($page) { - return min(max($page, 0), count($this->pagelayout) - 1); - } - - // Simple getters ========================================================== - public function get_quiz() { - return $this->quizobj->get_quiz(); - } - - public function get_quizobj() { - return $this->quizobj; - } - - /** @return int the course id. */ - public function get_courseid() { - return $this->quizobj->get_courseid(); - } - - /** - * Get the course settings object. - * - * @return stdClass the course settings object. - */ - public function get_course() { - return $this->quizobj->get_course(); - } - - /** @return int the quiz id. */ - public function get_quizid() { - return $this->quizobj->get_quizid(); - } - - /** @return string the name of this quiz. */ - public function get_quiz_name() { - return $this->quizobj->get_quiz_name(); - } - - /** @return int the quiz navigation method. */ - public function get_navigation_method() { - return $this->quizobj->get_navigation_method(); - } - - /** @return object the course_module object. */ - public function get_cm() { - return $this->quizobj->get_cm(); - } - - /** - * Get the course-module id. - * - * @return int the course_module id. - */ - public function get_cmid() { - return $this->quizobj->get_cmid(); - } - - /** - * @return bool whether the current user is someone who previews the quiz, - * rather than attempting it. - */ - public function is_preview_user() { - return $this->quizobj->is_preview_user(); - } - - /** @return int the number of attempts allowed at this quiz (0 = infinite). */ - public function get_num_attempts_allowed() { - return $this->quizobj->get_num_attempts_allowed(); - } - - /** @return int number fo pages in this quiz. */ - public function get_num_pages() { - return count($this->pagelayout); - } - - /** - * @param int $timenow the current time as a unix timestamp. - * @return access_manager and instance of the access_manager class - * for this quiz at this time. - */ - public function get_access_manager($timenow) { - return $this->quizobj->get_access_manager($timenow); - } - - /** @return int the attempt id. */ - public function get_attemptid() { - return $this->attempt->id; - } - - /** @return int the attempt unique id. */ - public function get_uniqueid() { - return $this->attempt->uniqueid; - } - - /** @return object the row from the quiz_attempts table. */ - public function get_attempt() { - return $this->attempt; - } - - /** @return int the number of this attemp (is it this user's first, second, ... attempt). */ - public function get_attempt_number() { - return $this->attempt->attempt; - } - - /** @return string one of the quiz_attempt::IN_PROGRESS, FINISHED, OVERDUE or ABANDONED constants. */ - public function get_state() { - return $this->attempt->state; - } - - /** @return int the id of the user this attempt belongs to. */ - public function get_userid() { - return $this->attempt->userid; - } - - /** @return int the current page of the attempt. */ - public function get_currentpage() { - return $this->attempt->currentpage; - } - - public function get_sum_marks() { - return $this->attempt->sumgrades; - } - - /** - * @return bool whether this attempt has been finished (true) or is still - * in progress (false). Be warned that this is not just state == self::FINISHED, - * it also includes self::ABANDONED. - */ - public function is_finished() { - return $this->attempt->state == self::FINISHED || $this->attempt->state == self::ABANDONED; - } - - /** @return bool whether this attempt is a preview attempt. */ - public function is_preview() { - return $this->attempt->preview; - } - - /** - * Is this someone dealing with their own attempt or preview? - * - * @return bool true => own attempt/preview. false => reviewing someone else's. - */ - public function is_own_attempt() { - global $USER; - return $this->attempt->userid == $USER->id; - } - - /** - * @return bool whether this attempt is a preview belonging to the current user. - */ - public function is_own_preview() { - return $this->is_own_attempt() && - $this->is_preview_user() && $this->attempt->preview; - } - - /** - * Is the current user allowed to review this attempt. This applies when - * {@link is_own_attempt()} returns false. - * - * @return bool whether the review should be allowed. - */ - public function is_review_allowed() { - if (!$this->has_capability('mod/quiz:viewreports')) { - return false; - } - - $cm = $this->get_cm(); - if ($this->has_capability('moodle/site:accessallgroups') || - groups_get_activity_groupmode($cm) != SEPARATEGROUPS) { - return true; - } - - // Check the users have at least one group in common. - $teachersgroups = groups_get_activity_allowed_groups($cm); - $studentsgroups = groups_get_all_groups( - $cm->course, $this->attempt->userid, $cm->groupingid); - return $teachersgroups && $studentsgroups && - array_intersect(array_keys($teachersgroups), array_keys($studentsgroups)); - } - - /** - * Has the student, in this attempt, engaged with the quiz in a non-trivial way? - * - * That is, is there any question worth a non-zero number of marks, where - * the student has made some response that we have saved? - * - * @return bool true if we have saved a response for at least one graded question. - */ - public function has_response_to_at_least_one_graded_question() { - foreach ($this->quba->get_attempt_iterator() as $qa) { - if ($qa->get_max_mark() == 0) { - continue; - } - if ($qa->get_num_steps() > 1) { - return true; - } - } - return false; - } - - /** - * Do any questions in this attempt need to be graded manually? - * - * @return bool True if we have at least one question still needs manual grading. - */ - public function requires_manual_grading(): bool { - return $this->quba->get_total_mark() === null; - } - - /** - * Get extra summary information about this attempt. - * - * Some behaviours may be able to provide interesting summary information - * about the attempt as a whole, and this method provides access to that data. - * To see how this works, try setting a quiz to one of the CBM behaviours, - * and then look at the extra information displayed at the top of the quiz - * review page once you have sumitted an attempt. - * - * In the return value, the array keys are identifiers of the form - * qbehaviour_behaviourname_meaningfullkey. For qbehaviour_deferredcbm_highsummary. - * The values are arrays with two items, title and content. Each of these - * will be either a string, or a renderable. - * - * @param question_display_options $options the display options for this quiz attempt at this time. - * @return array as described above. - */ - public function get_additional_summary_data(question_display_options $options) { - return $this->quba->get_summary_information($options); - } - - /** - * Get the overall feedback corresponding to a particular mark. - * - * @param number $grade a particular grade. - * @return string the feedback. - */ - public function get_overall_feedback($grade) { - return quiz_feedback_for_grade($grade, $this->get_quiz(), - $this->quizobj->get_context()); - } - - /** - * Wrapper round the has_capability funciton that automatically passes in the quiz context. - * - * @param string $capability the name of the capability to check. For example mod/forum:view. - * @param int|null $userid A user id. By default (null) checks the permissions of the current user. - * @param bool $doanything If false, ignore effect of admin role assignment. - * @return boolean true if the user has this capability. Otherwise false. - */ - public function has_capability($capability, $userid = null, $doanything = true) { - return $this->quizobj->has_capability($capability, $userid, $doanything); - } - - /** - * Wrapper round the require_capability function that automatically passes in the quiz context. - * - * @param string $capability the name of the capability to check. For example mod/forum:view. - * @param int|null $userid A user id. By default (null) checks the permissions of the current user. - * @param bool $doanything If false, ignore effect of admin role assignment. - */ - public function require_capability($capability, $userid = null, $doanything = true) { - $this->quizobj->require_capability($capability, $userid, $doanything); - } - - /** - * Check the appropriate capability to see whether this user may review their own attempt. - * If not, prints an error. - */ - public function check_review_capability() { - if ($this->get_attempt_state() == display_options::IMMEDIATELY_AFTER) { - $capability = 'mod/quiz:attempt'; - } else { - $capability = 'mod/quiz:reviewmyattempts'; - } - - // These next tests are in a slighly funny order. The point is that the - // common and most performance-critical case is students attempting a quiz - // so we want to check that permisison first. - - if ($this->has_capability($capability)) { - // User has the permission that lets you do the quiz as a student. Fine. - return; - } - - if ($this->has_capability('mod/quiz:viewreports') || - $this->has_capability('mod/quiz:preview')) { - // User has the permission that lets teachers review. Fine. - return; - } - - // They should not be here. Trigger the standard no-permission error - // but using the name of the student capability. - // We know this will fail. We just want the stadard exception thown. - $this->require_capability($capability); - } - - /** - * Checks whether a user may navigate to a particular slot. - * - * @param int $slot the target slot (currently does not affect the answer). - * @return bool true if the navigation should be allowed. - */ - public function can_navigate_to($slot) { - if ($this->attempt->state == self::OVERDUE) { - // When the attempt is overdue, students can only see the - // attempt summary page and cannot navigate anywhere else. - return false; - } - - switch ($this->get_navigation_method()) { - case QUIZ_NAVMETHOD_FREE: - return true; - break; - case QUIZ_NAVMETHOD_SEQ: - return false; - break; - } - return true; - } - - /** - * @return int one of the display_options::DURING, - * IMMEDIATELY_AFTER, LATER_WHILE_OPEN or AFTER_CLOSE constants. - */ - public function get_attempt_state() { - return quiz_attempt_state($this->get_quiz(), $this->attempt); - } - - /** - * Wrapper that the correct display_options for this quiz at the - * moment. - * - * @param bool $reviewing true for options when reviewing, false for when attempting. - * @return question_display_options the render options for this user on this attempt. - */ - public function get_display_options($reviewing) { - if ($reviewing) { - if (is_null($this->reviewoptions)) { - $this->reviewoptions = quiz_get_review_options($this->get_quiz(), - $this->attempt, $this->quizobj->get_context()); - if ($this->is_own_preview()) { - // It should always be possible for a teacher to review their - // own preview irrespective of the review options settings. - $this->reviewoptions->attempt = true; - } - } - return $this->reviewoptions; - - } else { - $options = display_options::make_from_quiz($this->get_quiz(), - display_options::DURING); - $options->flags = quiz_get_flag_option($this->attempt, $this->quizobj->get_context()); - return $options; - } - } - - /** - * Wrapper that the correct display_options for this quiz at the - * moment. - * - * @param bool $reviewing true for review page, else attempt page. - * @param int $slot which question is being displayed. - * @param moodle_url $thispageurl to return to after the editing form is - * submitted or cancelled. If null, no edit link will be generated. - * - * @return question_display_options the render options for this user on this - * attempt, with extra info to generate an edit link, if applicable. - */ - public function get_display_options_with_edit_link($reviewing, $slot, $thispageurl) { - $options = clone($this->get_display_options($reviewing)); - - if (!$thispageurl) { - return $options; - } - - if (!($reviewing || $this->is_preview())) { - return $options; - } - - $question = $this->quba->get_question($slot, false); - if (!question_has_capability_on($question, 'edit', $question->category)) { - return $options; - } - - $options->editquestionparams['cmid'] = $this->get_cmid(); - $options->editquestionparams['returnurl'] = $thispageurl; - - return $options; - } - - /** - * @param int $page page number - * @return bool true if this is the last page of the quiz. - */ - public function is_last_page($page) { - return $page == count($this->pagelayout) - 1; - } - - /** - * Return the list of slot numbers for either a given page of the quiz, or for the - * whole quiz. - * - * @param mixed $page string 'all' or integer page number. - * @return array the requested list of slot numbers. - */ - public function get_slots($page = 'all') { - if ($page === 'all') { - $numbers = array(); - foreach ($this->pagelayout as $numbersonpage) { - $numbers = array_merge($numbers, $numbersonpage); - } - return $numbers; - } else { - return $this->pagelayout[$page]; - } - } - - /** - * Return the list of slot numbers for either a given page of the quiz, or for the - * whole quiz. - * - * @param mixed $page string 'all' or integer page number. - * @return array the requested list of slot numbers. - */ - public function get_active_slots($page = 'all') { - $activeslots = array(); - foreach ($this->get_slots($page) as $slot) { - if (!$this->is_blocked_by_previous_question($slot)) { - $activeslots[] = $slot; - } - } - return $activeslots; - } - - /** - * Helper method for unit tests. Get the underlying question usage object. - * - * @return question_usage_by_activity the usage. - */ - public function get_question_usage() { - if (!(PHPUNIT_TEST || defined('BEHAT_TEST'))) { - throw new coding_exception('get_question_usage is only for use in unit tests. ' . - 'For other operations, use the quiz_attempt api, or extend it properly.'); - } - return $this->quba; - } - - /** - * Get the question_attempt object for a particular question in this attempt. - * - * @param int $slot the number used to identify this question within this attempt. - * @return question_attempt the requested question_attempt. - */ - public function get_question_attempt($slot) { - return $this->quba->get_question_attempt($slot); - } - - /** - * Get all the question_attempt objects that have ever appeared in a given slot. - * - * This relates to the 'Try another question like this one' feature. - * - * @param int $slot the number used to identify this question within this attempt. - * @return question_attempt[] the attempts. - */ - public function all_question_attempts_originally_in_slot($slot) { - $qas = array(); - foreach ($this->quba->get_attempt_iterator() as $qa) { - if ($qa->get_metadata('originalslot') == $slot) { - $qas[] = $qa; - } - } - $qas[] = $this->quba->get_question_attempt($slot); - return $qas; - } - - /** - * Is a particular question in this attempt a real question, or something like a description. - * - * @param int $slot the number used to identify this question within this attempt. - * @return int whether that question is a real question. Actually returns the - * question length, which could theoretically be greater than one. - */ - public function is_real_question($slot) { - return $this->quba->get_question($slot, false)->length; - } - - /** - * Is a particular question in this attempt a real question, or something like a description. - * - * @param int $slot the number used to identify this question within this attempt. - * @return bool whether that question is a real question. - */ - public function is_question_flagged($slot) { - return $this->quba->get_question_attempt($slot)->is_flagged(); - } - - /** - * Checks whether the question in this slot requires the previous - * question to have been completed. - * - * @param int $slot the number used to identify this question within this attempt. - * @return bool whether the previous question must have been completed before - * this one can be seen. - */ - public function is_blocked_by_previous_question($slot) { - return $slot > 1 && isset($this->slots[$slot]) && $this->slots[$slot]->requireprevious && - !$this->slots[$slot]->section->shufflequestions && - !$this->slots[$slot - 1]->section->shufflequestions && - $this->get_navigation_method() != QUIZ_NAVMETHOD_SEQ && - !$this->get_question_state($slot - 1)->is_finished() && - $this->quba->can_question_finish_during_attempt($slot - 1); - } - - /** - * Is it possible for this question to be re-started within this attempt? - * - * @param int $slot the number used to identify this question within this attempt. - * @return bool whether the student should be given the option to restart this question now. - */ - public function can_question_be_redone_now($slot) { - return $this->get_quiz()->canredoquestions && !$this->is_finished() && - $this->get_question_state($slot)->is_finished(); - } - - /** - * Given a slot in this attempt, which may or not be a redone question, return the original slot. - * - * @param int $slot identifies a particular question in this attempt. - * @return int the slot where this question was originally. - */ - public function get_original_slot($slot) { - $originalslot = $this->quba->get_question_attempt_metadata($slot, 'originalslot'); - if ($originalslot) { - return $originalslot; - } else { - return $slot; - } - } - - /** - * Get the displayed question number for a slot. - * - * @param int $slot the number used to identify this question within this attempt. - * @return string the displayed question number for the question in this slot. - * For example '1', '2', '3' or 'i'. - */ - public function get_question_number($slot) { - return $this->questionnumbers[$slot]; - } - - /** - * If the section heading, if any, that should come just before this slot. - * - * @param int $slot identifies a particular question in this attempt. - * @return string the required heading, or null if there is not one here. - */ - public function get_heading_before_slot($slot) { - if ($this->slots[$slot]->firstinsection) { - return $this->slots[$slot]->section->heading; - } else { - return null; - } - } - - /** - * Return the page of the quiz where this question appears. - * - * @param int $slot the number used to identify this question within this attempt. - * @return int the page of the quiz this question appears on. - */ - public function get_question_page($slot) { - return $this->questionpages[$slot]; - } - - /** - * Return the grade obtained on a particular question, if the user is permitted - * to see it. You must previously have called load_question_states to load the - * state data about this question. - * - * @param int $slot the number used to identify this question within this attempt. - * @return string the formatted grade, to the number of decimal places specified - * by the quiz. - */ - public function get_question_name($slot) { - return $this->quba->get_question($slot, false)->name; - } - - /** - * Return the {@link question_state} that this question is in. - * - * @param int $slot the number used to identify this question within this attempt. - * @return question_state the state this question is in. - */ - public function get_question_state($slot) { - return $this->quba->get_question_state($slot); - } - - /** - * Return the grade obtained on a particular question, if the user is permitted - * to see it. You must previously have called load_question_states to load the - * state data about this question. - * - * @param int $slot the number used to identify this question within this attempt. - * @param bool $showcorrectness Whether right/partial/wrong states should - * be distinguished. - * @return string the formatted grade, to the number of decimal places specified - * by the quiz. - */ - public function get_question_status($slot, $showcorrectness) { - return $this->quba->get_question_state_string($slot, $showcorrectness); - } - - /** - * Return the grade obtained on a particular question, if the user is permitted - * to see it. You must previously have called load_question_states to load the - * state data about this question. - * - * @param int $slot the number used to identify this question within this attempt. - * @param bool $showcorrectness Whether right/partial/wrong states should - * be distinguished. - * @return string class name for this state. - */ - public function get_question_state_class($slot, $showcorrectness) { - return $this->quba->get_question_state_class($slot, $showcorrectness); - } - - /** - * Return the grade obtained on a particular question. - * - * You must previously have called load_question_states to load the state - * data about this question. - * - * @param int $slot the number used to identify this question within this attempt. - * @return string the formatted grade, to the number of decimal places specified by the quiz. - */ - public function get_question_mark($slot) { - return quiz_format_question_grade($this->get_quiz(), $this->quba->get_question_mark($slot)); - } - - /** - * Get the time of the most recent action performed on a question. - * - * @param int $slot the number used to identify this question within this usage. - * @return int timestamp. - */ - public function get_question_action_time($slot) { - return $this->quba->get_question_action_time($slot); - } - - /** - * Return the question type name for a given slot within the current attempt. - * - * @param int $slot the number used to identify this question within this attempt. - * @return string the question type name. - * @since Moodle 3.1 - */ - public function get_question_type_name($slot) { - return $this->quba->get_question($slot, false)->get_type_name(); - } - - /** - * Get the time remaining for an in-progress attempt, if the time is short - * enough that it would be worth showing a timer. - * - * @param int $timenow the time to consider as 'now'. - * @return int|false the number of seconds remaining for this attempt. - * False if there is no limit. - */ - public function get_time_left_display($timenow) { - if ($this->attempt->state != self::IN_PROGRESS) { - return false; - } - return $this->get_access_manager($timenow)->get_time_left_display($this->attempt, $timenow); - } - - - /** - * @return int the time when this attempt was submitted. 0 if it has not been - * submitted yet. - */ - public function get_submitted_date() { - return $this->attempt->timefinish; - } - - /** - * If the attempt is in an applicable state, work out the time by which the - * student should next do something. - * - * @return int timestamp by which the student needs to do something. - */ - public function get_due_date() { - $deadlines = array(); - if ($this->quizobj->get_quiz()->timelimit) { - $deadlines[] = $this->attempt->timestart + $this->quizobj->get_quiz()->timelimit; - } - if ($this->quizobj->get_quiz()->timeclose) { - $deadlines[] = $this->quizobj->get_quiz()->timeclose; - } - if ($deadlines) { - $duedate = min($deadlines); - } else { - return false; - } - - switch ($this->attempt->state) { - case self::IN_PROGRESS: - return $duedate; - - case self::OVERDUE: - return $duedate + $this->quizobj->get_quiz()->graceperiod; - - default: - throw new coding_exception('Unexpected state: ' . $this->attempt->state); - } - } - - // URLs related to this attempt ============================================ - /** - * @return string quiz view url. - */ - public function view_url() { - return $this->quizobj->view_url(); - } - - /** - * Get the URL to start or continue an attempt. - * - * @param int|null $slot which question in the attempt to go to after starting (optional). - * @param int $page which page in the attempt to go to after starting. - * @return string the URL of this quiz's edit page. Needs to be POSTed to with a cmid parameter. - */ - public function start_attempt_url($slot = null, $page = -1) { - if ($page == -1 && !is_null($slot)) { - $page = $this->get_question_page($slot); - } else { - $page = 0; - } - return $this->quizobj->start_attempt_url($page); - } - - /** - * Generates the title of the attempt page. - * - * @param int $page the page number (starting with 0) in the attempt. - * @return string attempt page title. - */ - public function attempt_page_title(int $page) : string { - if ($this->get_num_pages() > 1) { - $a = new stdClass(); - $a->name = $this->get_quiz_name(); - $a->currentpage = $page + 1; - $a->totalpages = $this->get_num_pages(); - $title = get_string('attempttitlepaged', 'quiz', $a); - } else { - $title = get_string('attempttitle', 'quiz', $this->get_quiz_name()); - } - - return $title; - } - - /** - * @param int|null $slot if specified, the slot number of a specific question to link to. - * @param int $page if specified, a particular page to link to. If not given deduced - * from $slot, or goes to the first page. - * @param int $thispage if not -1, the current page. Will cause links to other things on - * this page to be output as only a fragment. - * @return string the URL to continue this attempt. - */ - public function attempt_url($slot = null, $page = -1, $thispage = -1) { - return $this->page_and_question_url('attempt', $slot, $page, false, $thispage); - } - - /** - * Generates the title of the summary page. - * - * @return string summary page title. - */ - public function summary_page_title() : string { - return get_string('attemptsummarytitle', 'quiz', $this->get_quiz_name()); - } - - /** - * @return moodle_url the URL of this quiz's summary page. - */ - public function summary_url() { - return new moodle_url('/mod/quiz/summary.php', array('attempt' => $this->attempt->id, 'cmid' => $this->get_cmid())); - } - - /** - * @return moodle_url the URL of this quiz's summary page. - */ - public function processattempt_url() { - return new moodle_url('/mod/quiz/processattempt.php'); - } - - /** - * Generates the title of the review page. - * - * @param int $page the page number (starting with 0) in the attempt. - * @param bool $showall whether the review page contains the entire attempt on one page. - * @return string title of the review page. - */ - public function review_page_title(int $page, bool $showall = false) : string { - if (!$showall && $this->get_num_pages() > 1) { - $a = new stdClass(); - $a->name = $this->get_quiz_name(); - $a->currentpage = $page + 1; - $a->totalpages = $this->get_num_pages(); - $title = get_string('attemptreviewtitlepaged', 'quiz', $a); - } else { - $title = get_string('attemptreviewtitle', 'quiz', $this->get_quiz_name()); - } - - return $title; - } - - /** - * @param int|null $slot indicates which question to link to. - * @param int $page if specified, the URL of this particular page of the attempt, otherwise - * the URL will go to the first page. If -1, deduce $page from $slot. - * @param bool|null $showall if true, the URL will be to review the entire attempt on one page, - * and $page will be ignored. If null, a sensible default will be chosen. - * @param int $thispage if not -1, the current page. Will cause links to other things on - * this page to be output as only a fragment. - * @return string the URL to review this attempt. - */ - public function review_url($slot = null, $page = -1, $showall = null, $thispage = -1) { - return $this->page_and_question_url('review', $slot, $page, $showall, $thispage); - } - - /** - * By default, should this script show all questions on one page for this attempt? - * - * @param string $script the script name, e.g. 'attempt', 'summary', 'review'. - * @return bool whether show all on one page should be on by default. - */ - public function get_default_show_all($script) { - return $script === 'review' && count($this->questionpages) < self::MAX_SLOTS_FOR_DEFAULT_REVIEW_SHOW_ALL; - } - - // Bits of content ========================================================= - - /** - * If $reviewoptions->attempt is false, meaning that students can't review this - * attempt at the moment, return an appropriate string explaining why. - * - * @param bool $short if true, return a shorter string. - * @return string an appropriate message. - */ - public function cannot_review_message($short = false) { - return $this->quizobj->cannot_review_message( - $this->get_attempt_state(), $short); - } - - /** - * Initialise the JS etc. required all the questions on a page. - * - * @param int|string $page a page number, or 'all'. - * @param bool $showall if true forces page number to all. - * @return string HTML to output - mostly obsolete, will probably be an empty string. - */ - public function get_html_head_contributions($page = 'all', $showall = false) { - if ($showall) { - $page = 'all'; - } - $result = ''; - foreach ($this->get_slots($page) as $slot) { - $result .= $this->quba->render_question_head_html($slot); - } - $result .= question_engine::initialise_js(); - return $result; - } - - /** - * Initialise the JS etc. required by one question. - * - * @param int $slot the question slot number. - * @return string HTML to output - but this is mostly obsolete. Will probably be an empty string. - */ - public function get_question_html_head_contributions($slot) { - return $this->quba->render_question_head_html($slot) . - question_engine::initialise_js(); - } - - /** - * Print the HTML for the start new preview button, if the current user - * is allowed to see one. - * - * @return string HTML for the button. - */ - public function restart_preview_button() { - global $OUTPUT; - if ($this->is_preview() && $this->is_preview_user()) { - return $OUTPUT->single_button(new moodle_url( - $this->start_attempt_url(), array('forcenew' => true)), - get_string('startnewpreview', 'quiz')); - } else { - return ''; - } - } - - /** - * Generate the HTML that displayes the question in its current state, with - * the appropriate display options. - * - * @param int $slot identifies the question in the attempt. - * @param bool $reviewing is the being printed on an attempt or a review page. - * @param renderer $renderer the quiz renderer. - * @param moodle_url $thispageurl the URL of the page this question is being printed on. - * @return string HTML for the question in its current state. - */ - public function render_question($slot, $reviewing, renderer $renderer, $thispageurl = null) { - if ($this->is_blocked_by_previous_question($slot)) { - $placeholderqa = $this->make_blocked_question_placeholder($slot); - - $displayoptions = $this->get_display_options($reviewing); - $displayoptions->manualcomment = question_display_options::HIDDEN; - $displayoptions->history = question_display_options::HIDDEN; - $displayoptions->readonly = true; - - return html_writer::div($placeholderqa->render($displayoptions, - $this->get_question_number($this->get_original_slot($slot))), - 'mod_quiz-blocked_question_warning'); - } - - return $this->render_question_helper($slot, $reviewing, $thispageurl, $renderer, null); - } - - /** - * Helper used by {@link render_question()} and {@link render_question_at_step()}. - * - * @param int $slot identifies the question in the attempt. - * @param bool $reviewing is the being printed on an attempt or a review page. - * @param moodle_url $thispageurl the URL of the page this question is being printed on. - * @param renderer $renderer the quiz renderer. - * @param int|null $seq the seq number of the past state to display. - * @return string HTML fragment. - */ - protected function render_question_helper($slot, $reviewing, $thispageurl, - renderer $renderer, $seq) { - $originalslot = $this->get_original_slot($slot); - $number = $this->get_question_number($originalslot); - $displayoptions = $this->get_display_options_with_edit_link($reviewing, $slot, $thispageurl); - - if ($slot != $originalslot) { - $originalmaxmark = $this->get_question_attempt($slot)->get_max_mark(); - $this->get_question_attempt($slot)->set_max_mark($this->get_question_attempt($originalslot)->get_max_mark()); - } - - if ($this->can_question_be_redone_now($slot)) { - $displayoptions->extrainfocontent = $renderer->redo_question_button( - $slot, $displayoptions->readonly); - } - - if ($displayoptions->history && $displayoptions->questionreviewlink) { - $links = $this->links_to_other_redos($slot, $displayoptions->questionreviewlink); - if ($links) { - $displayoptions->extrahistorycontent = html_writer::tag('p', - get_string('redoesofthisquestion', 'quiz', $renderer->render($links))); - } - } - - if ($seq === null) { - $output = $this->quba->render_question($slot, $displayoptions, $number); - } else { - $output = $this->quba->render_question_at_step($slot, $seq, $displayoptions, $number); - } - - if ($slot != $originalslot) { - $this->get_question_attempt($slot)->set_max_mark($originalmaxmark); - } - - return $output; - } - - /** - * Create a fake question to be displayed in place of a question that is blocked - * until the previous question has been answered. - * - * @param int $slot int slot number of the question to replace. - * @return question_attempt the placeholder question attempt. - */ - protected function make_blocked_question_placeholder($slot) { - $replacedquestion = $this->get_question_attempt($slot)->get_question(false); - - question_bank::load_question_definition_classes('description'); - $question = new qtype_description_question(); - $question->id = $replacedquestion->id; - $question->category = null; - $question->parent = 0; - $question->qtype = question_bank::get_qtype('description'); - $question->name = ''; - $question->questiontext = get_string('questiondependsonprevious', 'quiz'); - $question->questiontextformat = FORMAT_HTML; - $question->generalfeedback = ''; - $question->defaultmark = $this->quba->get_question_max_mark($slot); - $question->length = $replacedquestion->length; - $question->penalty = 0; - $question->stamp = ''; - $question->status = \core_question\local\bank\question_version_status::QUESTION_STATUS_READY; - $question->timecreated = null; - $question->timemodified = null; - $question->createdby = null; - $question->modifiedby = null; - - $placeholderqa = new question_attempt($question, $this->quba->get_id(), - null, $this->quba->get_question_max_mark($slot)); - $placeholderqa->set_slot($slot); - $placeholderqa->start($this->get_quiz()->preferredbehaviour, 1); - $placeholderqa->set_flagged($this->is_question_flagged($slot)); - return $placeholderqa; - } - - /** - * Like {@link render_question()} but displays the question at the past step - * indicated by $seq, rather than showing the latest step. - * - * @param int $slot the slot number of a question in this quiz attempt. - * @param int $seq the seq number of the past state to display. - * @param bool $reviewing is the being printed on an attempt or a review page. - * @param renderer $renderer the quiz renderer. - * @param moodle_url $thispageurl the URL of the page this question is being printed on. - * @return string HTML for the question in its current state. - */ - public function render_question_at_step($slot, $seq, $reviewing, - renderer $renderer, $thispageurl = null) { - return $this->render_question_helper($slot, $reviewing, $thispageurl, $renderer, $seq); - } - - /** - * Wrapper round print_question from lib/questionlib.php. - * - * @param int $slot the id of a question in this quiz attempt. - * @return string HTML of the question. - */ - public function render_question_for_commenting($slot) { - $options = $this->get_display_options(true); - $options->generalfeedback = question_display_options::HIDDEN; - $options->manualcomment = question_display_options::EDITABLE; - return $this->quba->render_question($slot, $options, - $this->get_question_number($slot)); - } - - /** - * Check wheter access should be allowed to a particular file. - * - * @param int $slot the slot of a question in this quiz attempt. - * @param bool $reviewing is the being printed on an attempt or a review page. - * @param int $contextid the file context id from the request. - * @param string $component the file component from the request. - * @param string $filearea the file area from the request. - * @param array $args extra part components from the request. - * @param bool $forcedownload whether to force download. - * @return string HTML for the question in its current state. - */ - public function check_file_access($slot, $reviewing, $contextid, $component, - $filearea, $args, $forcedownload) { - $options = $this->get_display_options($reviewing); - - // Check permissions - warning there is similar code in review.php and - // reviewquestion.php. If you change on, change them all. - if ($reviewing && $this->is_own_attempt() && !$options->attempt) { - return false; - } - - if ($reviewing && !$this->is_own_attempt() && !$this->is_review_allowed()) { - return false; - } - - return $this->quba->check_file_access($slot, $options, - $component, $filearea, $args, $forcedownload); - } - - /** - * Get the navigation panel object for this attempt. - * - * @param renderer $output the quiz renderer to use to output things. - * @param string $panelclass The type of panel, navigation_panel_attempt::class or navigation_panel_review::class - * @param int $page the current page number. - * @param bool $showall whether we are showing the whole quiz on one page. (Used by review.php.) - * @return block_contents the requested object. - */ - public function get_navigation_panel(renderer $output, - $panelclass, $page, $showall = false) { - $panel = new $panelclass($this, $this->get_display_options(true), $page, $showall); - - $bc = new block_contents(); - $bc->attributes['id'] = 'mod_quiz_navblock'; - $bc->attributes['role'] = 'navigation'; - $bc->title = get_string('quiznavigation', 'quiz'); - $bc->content = $output->navigation_panel($panel); - return $bc; - } - - /** - * Return an array of variant URLs to other attempts at this quiz. - * - * The $url passed in must contain an attempt parameter. - * - * The {@see links_to_other_attempts} object returned contains an - * array with keys that are the attempt number, 1, 2, 3. - * The array values are either a {@link moodle_url} with the attempt parameter - * updated to point to the attempt id of the other attempt, or null corresponding - * to the current attempt number. - * - * @param moodle_url $url a URL. - * @return links_to_other_attempts|bool containing array int => null|moodle_url. - * False if none. - */ - public function links_to_other_attempts(moodle_url $url) { - $attempts = quiz_get_user_attempts($this->get_quiz()->id, $this->attempt->userid, 'all'); - if (count($attempts) <= 1) { - return false; - } - - $links = new links_to_other_attempts(); - foreach ($attempts as $at) { - if ($at->id == $this->attempt->id) { - $links->links[$at->attempt] = null; - } else { - $links->links[$at->attempt] = new moodle_url($url, array('attempt' => $at->id)); - } - } - return $links; - } - - /** - * Return an array of variant URLs to other redos of the question in a particular slot. - * - * The $url passed in must contain a slot parameter. - * - * The {@see links_to_other_attempts} object returned contains an - * array with keys that are the redo number, 1, 2, 3. - * The array values are either a {@see moodle_url} with the slot parameter - * updated to point to the slot that has that redo of this question; or null - * corresponding to the redo identified by $slot. - * - * @param int $slot identifies a question in this attempt. - * @param moodle_url $baseurl the base URL to modify to generate each link. - * @return links_to_other_attempts|null containing array int => null|moodle_url, - * or null if the question in this slot has not been redone. - */ - public function links_to_other_redos($slot, moodle_url $baseurl) { - $originalslot = $this->get_original_slot($slot); - - $qas = $this->all_question_attempts_originally_in_slot($originalslot); - if (count($qas) <= 1) { - return null; - } - - $links = new links_to_other_attempts(); - $index = 1; - foreach ($qas as $qa) { - if ($qa->get_slot() == $slot) { - $links->links[$index] = null; - } else { - $url = new moodle_url($baseurl, array('slot' => $qa->get_slot())); - $links->links[$index] = new action_link($url, $index, - new popup_action('click', $url, 'reviewquestion', - array('width' => 450, 'height' => 650)), - array('title' => get_string('reviewresponse', 'question'))); - } - $index++; - } - return $links; - } - - // Methods for processing ================================================== - - /** - * Check this attempt, to see if there are any state transitions that should - * happen automatically. This function will update the attempt checkstatetime. - * @param int $timestamp the timestamp that should be stored as the modified - * @param bool $studentisonline is the student currently interacting with Moodle? - */ - public function handle_if_time_expired($timestamp, $studentisonline) { - - $timeclose = $this->get_access_manager($timestamp)->get_end_time($this->attempt); - - if ($timeclose === false || $this->is_preview()) { - $this->update_timecheckstate(null); - return; // No time limit. - } - if ($timestamp < $timeclose) { - $this->update_timecheckstate($timeclose); - return; // Time has not yet expired. - } - - // If the attempt is already overdue, look to see if it should be abandoned ... - if ($this->attempt->state == self::OVERDUE) { - $timeoverdue = $timestamp - $timeclose; - $graceperiod = $this->quizobj->get_quiz()->graceperiod; - if ($timeoverdue >= $graceperiod) { - $this->process_abandon($timestamp, $studentisonline); - } else { - // Overdue time has not yet expired - $this->update_timecheckstate($timeclose + $graceperiod); - } - return; // ... and we are done. - } - - if ($this->attempt->state != self::IN_PROGRESS) { - $this->update_timecheckstate(null); - return; // Attempt is already in a final state. - } - - // Otherwise, we were in quiz_attempt::IN_PROGRESS, and time has now expired. - // Transition to the appropriate state. - switch ($this->quizobj->get_quiz()->overduehandling) { - case 'autosubmit': - $this->process_finish($timestamp, false, $studentisonline ? $timestamp : $timeclose, $studentisonline); - return; - - case 'graceperiod': - $this->process_going_overdue($timestamp, $studentisonline); - return; - - case 'autoabandon': - $this->process_abandon($timestamp, $studentisonline); - return; - } - - // This is an overdue attempt with no overdue handling defined, so just abandon. - $this->process_abandon($timestamp, $studentisonline); - return; - } - - /** - * Process all the actions that were submitted as part of the current request. - * - * @param int $timestamp the timestamp that should be stored as the modified. - * time in the database for these actions. If null, will use the current time. - * @param bool $becomingoverdue - * @param array|null $simulatedresponses If not null, then we are testing, and this is an array of simulated data. - * There are two formats supported here, for historical reasons. The newer approach is to pass an array created by - * {@link core_question_generator::get_simulated_post_data_for_questions_in_usage()}. - * the second is to pass an array slot no => contains arrays representing student - * responses which will be passed to {@link question_definition::prepare_simulated_post_data()}. - * This second method will probably get deprecated one day. - */ - public function process_submitted_actions($timestamp, $becomingoverdue = false, $simulatedresponses = null) { - global $DB; - - $transaction = $DB->start_delegated_transaction(); - - if ($simulatedresponses !== null) { - if (is_int(key($simulatedresponses))) { - // Legacy approach. Should be removed one day. - $simulatedpostdata = $this->quba->prepare_simulated_post_data($simulatedresponses); - } else { - $simulatedpostdata = $simulatedresponses; - } - } else { - $simulatedpostdata = null; - } - - $this->quba->process_all_actions($timestamp, $simulatedpostdata); - question_engine::save_questions_usage_by_activity($this->quba); - - $this->attempt->timemodified = $timestamp; - if ($this->attempt->state == self::FINISHED) { - $this->attempt->sumgrades = $this->quba->get_total_mark(); - } - if ($becomingoverdue) { - $this->process_going_overdue($timestamp, true); - } else { - $DB->update_record('quiz_attempts', $this->attempt); - } - - if (!$this->is_preview() && $this->attempt->state == self::FINISHED) { - quiz_save_best_grade($this->get_quiz(), $this->get_userid()); - } - - $transaction->allow_commit(); - } - - /** - * Replace a question in an attempt with a new attempt at the same question. - * - * Well, for randomised questions, it won't be the same question, it will be - * a different randomised selection. - * - * @param int $slot the question to restart. - * @param int $timestamp the timestamp to record for this action. - */ - public function process_redo_question($slot, $timestamp) { - global $DB; - - if (!$this->can_question_be_redone_now($slot)) { - throw new coding_exception('Attempt to restart the question in slot ' . $slot . - ' when it is not in a state to be restarted.'); - } - - $qubaids = new \mod_quiz\question\qubaids_for_users_attempts( - $this->get_quizid(), $this->get_userid(), 'all', true); - - $transaction = $DB->start_delegated_transaction(); - - // Add the question to the usage. It is important we do this before we choose a variant. - $newquestionid = qbank_helper::choose_question_for_redo($this->get_quizid(), - $this->get_quizobj()->get_context(), $this->slots[$slot]->id, $qubaids); - $newquestion = question_bank::load_question($newquestionid, $this->get_quiz()->shuffleanswers); - $newslot = $this->quba->add_question_in_place_of_other($slot, $newquestion); - - // Choose the variant. - if ($newquestion->get_num_variants() == 1) { - $variant = 1; - } else { - $variantstrategy = new core_question\engine\variants\least_used_strategy( - $this->quba, $qubaids); - $variant = $variantstrategy->choose_variant($newquestion->get_num_variants(), - $newquestion->get_variants_selection_seed()); - } - - // Start the question. - $this->quba->start_question($slot, $variant); - $this->quba->set_max_mark($newslot, 0); - $this->quba->set_question_attempt_metadata($newslot, 'originalslot', $slot); - question_engine::save_questions_usage_by_activity($this->quba); - $this->fire_attempt_question_restarted_event($slot, $newquestion->id); - - $transaction->allow_commit(); - } - - /** - * Process all the autosaved data that was part of the current request. - * - * @param int $timestamp the timestamp that should be stored as the modified. - * time in the database for these actions. If null, will use the current time. - */ - public function process_auto_save($timestamp) { - global $DB; - - $transaction = $DB->start_delegated_transaction(); - - $this->quba->process_all_autosaves($timestamp); - question_engine::save_questions_usage_by_activity($this->quba); - $this->fire_attempt_autosaved_event(); - - $transaction->allow_commit(); - } - - /** - * Update the flagged state for all question_attempts in this usage, if their - * flagged state was changed in the request. - */ - public function save_question_flags() { - global $DB; - - $transaction = $DB->start_delegated_transaction(); - $this->quba->update_question_flags(); - question_engine::save_questions_usage_by_activity($this->quba); - $transaction->allow_commit(); - } - - /** - * Submit the attempt. - * - * The separate $timefinish argument should be used when the quiz attempt - * is being processed asynchronously (for example when cron is submitting - * attempts where the time has expired). - * - * @param int $timestamp the time to record as last modified time. - * @param bool $processsubmitted if true, and question responses in the current - * POST request are stored to be graded, before the attempt is finished. - * @param ?int $timefinish if set, use this as the finish time for the attempt. - * (otherwise use $timestamp as the finish time as well). - * @param bool $studentisonline is the student currently interacting with Moodle? - */ - public function process_finish($timestamp, $processsubmitted, $timefinish = null, $studentisonline = false) { - global $DB; - - $transaction = $DB->start_delegated_transaction(); - - if ($processsubmitted) { - $this->quba->process_all_actions($timestamp); - } - $this->quba->finish_all_questions($timestamp); - - question_engine::save_questions_usage_by_activity($this->quba); - - $this->attempt->timemodified = $timestamp; - $this->attempt->timefinish = $timefinish ?? $timestamp; - $this->attempt->sumgrades = $this->quba->get_total_mark(); - $this->attempt->state = self::FINISHED; - $this->attempt->timecheckstate = null; - $this->attempt->gradednotificationsenttime = null; - - if (!$this->requires_manual_grading() || - !has_capability('mod/quiz:emailnotifyattemptgraded', $this->get_quizobj()->get_context(), - $this->get_userid())) { - $this->attempt->gradednotificationsenttime = $this->attempt->timefinish; - } - - $DB->update_record('quiz_attempts', $this->attempt); - - if (!$this->is_preview()) { - quiz_save_best_grade($this->get_quiz(), $this->attempt->userid); - - // Trigger event. - $this->fire_state_transition_event('\mod_quiz\event\attempt_submitted', $timestamp, $studentisonline); - - // Tell any access rules that care that the attempt is over. - $this->get_access_manager($timestamp)->current_attempt_finished(); - } - - $transaction->allow_commit(); - } - - /** - * Update this attempt timecheckstate if necessary. - * - * @param int|null $time the timestamp to set. - */ - public function update_timecheckstate($time) { - global $DB; - if ($this->attempt->timecheckstate !== $time) { - $this->attempt->timecheckstate = $time; - $DB->set_field('quiz_attempts', 'timecheckstate', $time, array('id' => $this->attempt->id)); - } - } - - /** - * Mark this attempt as now overdue. - * - * @param int $timestamp the time to deem as now. - * @param bool $studentisonline is the student currently interacting with Moodle? - */ - public function process_going_overdue($timestamp, $studentisonline) { - global $DB; - - $transaction = $DB->start_delegated_transaction(); - $this->attempt->timemodified = $timestamp; - $this->attempt->state = self::OVERDUE; - // If we knew the attempt close time, we could compute when the graceperiod ends. - // Instead we'll just fix it up through cron. - $this->attempt->timecheckstate = $timestamp; - $DB->update_record('quiz_attempts', $this->attempt); - - $this->fire_state_transition_event('\mod_quiz\event\attempt_becameoverdue', $timestamp, $studentisonline); - - $transaction->allow_commit(); - - quiz_send_overdue_message($this); - } - - /** - * Mark this attempt as abandoned. - * - * @param int $timestamp the time to deem as now. - * @param bool $studentisonline is the student currently interacting with Moodle? - */ - public function process_abandon($timestamp, $studentisonline) { - global $DB; - - $transaction = $DB->start_delegated_transaction(); - $this->attempt->timemodified = $timestamp; - $this->attempt->state = self::ABANDONED; - $this->attempt->timecheckstate = null; - $DB->update_record('quiz_attempts', $this->attempt); - - $this->fire_state_transition_event('\mod_quiz\event\attempt_abandoned', $timestamp, $studentisonline); - - $transaction->allow_commit(); - } - - /** - * Fire a state transition event. - * - * @param string $eventclass the event class name. - * @param int $timestamp the timestamp to include in the event. - * @param bool $studentisonline is the student currently interacting with Moodle? - */ - protected function fire_state_transition_event($eventclass, $timestamp, $studentisonline) { - global $USER; - $quizrecord = $this->get_quiz(); - $params = array( - 'context' => $this->get_quizobj()->get_context(), - 'courseid' => $this->get_courseid(), - 'objectid' => $this->attempt->id, - 'relateduserid' => $this->attempt->userid, - 'other' => array( - 'submitterid' => CLI_SCRIPT ? null : $USER->id, - 'quizid' => $quizrecord->id, - 'studentisonline' => $studentisonline - ) - ); - $event = $eventclass::create($params); - $event->add_record_snapshot('quiz', $this->get_quiz()); - $event->add_record_snapshot('quiz_attempts', $this->get_attempt()); - $event->trigger(); - } - - // Private methods ========================================================= - - /** - * Get a URL for a particular question on a particular page of the quiz. - * Used by {@link attempt_url()} and {@link review_url()}. - * - * @param string $script. Used in the URL like /mod/quiz/$script.php. - * @param int $slot identifies the specific question on the page to jump to. - * 0 to just use the $page parameter. - * @param int $page -1 to look up the page number from the slot, otherwise - * the page number to go to. - * @param bool|null $showall if true, return a URL with showall=1, and not page number. - * if null, then an intelligent default will be chosen. - * @param int $thispage the page we are currently on. Links to questions on this - * page will just be a fragment #q123. -1 to disable this. - * @return moodle_url The requested URL. - */ - protected function page_and_question_url($script, $slot, $page, $showall, $thispage) { - - $defaultshowall = $this->get_default_show_all($script); - if ($showall === null && ($page == 0 || $page == -1)) { - $showall = $defaultshowall; - } - - // Fix up $page. - if ($page == -1) { - if ($slot !== null && !$showall) { - $page = $this->get_question_page($slot); - } else { - $page = 0; - } - } - - if ($showall) { - $page = 0; - } - - // Add a fragment to scroll down to the question. - $fragment = ''; - if ($slot !== null) { - if ($slot == reset($this->pagelayout[$page]) && $thispage != $page) { - // Changing the page, go to top. - $fragment = '#'; - } else { - // Link to the question container. - $qa = $this->get_question_attempt($slot); - $fragment = '#' . $qa->get_outer_question_div_unique_id(); - } - } - - // Work out the correct start to the URL. - if ($thispage == $page) { - return new moodle_url($fragment); - - } else { - $url = new moodle_url('/mod/quiz/' . $script . '.php' . $fragment, - array('attempt' => $this->attempt->id, 'cmid' => $this->get_cmid())); - if ($page == 0 && $showall != $defaultshowall) { - $url->param('showall', (int) $showall); - } else if ($page > 0) { - $url->param('page', $page); - } - return $url; - } - } - - /** - * Process responses during an attempt at a quiz. - * - * @param int $timenow time when the processing started. - * @param bool $finishattempt whether to finish the attempt or not. - * @param bool $timeup true if form was submitted by timer. - * @param int $thispage current page number. - * @return string the attempt state once the data has been processed. - * @since Moodle 3.1 - */ - public function process_attempt($timenow, $finishattempt, $timeup, $thispage) { - global $DB; - - $transaction = $DB->start_delegated_transaction(); - - // Get key times. - $accessmanager = $this->get_access_manager($timenow); - $timeclose = $accessmanager->get_end_time($this->get_attempt()); - $graceperiodmin = get_config('quiz', 'graceperiodmin'); - - // Don't enforce timeclose for previews. - if ($this->is_preview()) { - $timeclose = false; - } - - // Check where we are in relation to the end time, if there is one. - $toolate = false; - if ($timeclose !== false) { - if ($timenow > $timeclose - QUIZ_MIN_TIME_TO_CONTINUE) { - // If there is only a very small amount of time left, there is no point trying - // to show the student another page of the quiz. Just finish now. - $timeup = true; - if ($timenow > $timeclose + $graceperiodmin) { - $toolate = true; - } - } else { - // If time is not close to expiring, then ignore the client-side timer's opinion - // about whether time has expired. This can happen if the time limit has changed - // since the student's previous interaction. - $timeup = false; - } - } - - // If time is running out, trigger the appropriate action. - $becomingoverdue = false; - $becomingabandoned = false; - if ($timeup) { - if ($this->get_quiz()->overduehandling === 'graceperiod') { - if ($timenow > $timeclose + $this->get_quiz()->graceperiod + $graceperiodmin) { - // Grace period has run out. - $finishattempt = true; - $becomingabandoned = true; - } else { - $becomingoverdue = true; - } - } else { - $finishattempt = true; - } - } - - if (!$finishattempt) { - // Just process the responses for this page and go to the next page. - if (!$toolate) { - try { - $this->process_submitted_actions($timenow, $becomingoverdue); - $this->fire_attempt_updated_event(); - } catch (question_out_of_sequence_exception $e) { - throw new moodle_exception('submissionoutofsequencefriendlymessage', 'question', - $this->attempt_url(null, $thispage)); - - } catch (Exception $e) { - // This sucks, if we display our own custom error message, there is no way - // to display the original stack trace. - $debuginfo = ''; - if (!empty($e->debuginfo)) { - $debuginfo = $e->debuginfo; - } - throw new moodle_exception('errorprocessingresponses', 'question', - $this->attempt_url(null, $thispage), $e->getMessage(), $debuginfo); - } - - if (!$becomingoverdue) { - foreach ($this->get_slots() as $slot) { - if (optional_param('redoslot' . $slot, false, PARAM_BOOL)) { - $this->process_redo_question($slot, $timenow); - } - } - } - - } else { - // The student is too late. - $this->process_going_overdue($timenow, true); - } - - $transaction->allow_commit(); - - return $becomingoverdue ? self::OVERDUE : self::IN_PROGRESS; - } - - // Update the quiz attempt record. - try { - if ($becomingabandoned) { - $this->process_abandon($timenow, true); - } else { - if (!$toolate || $this->get_quiz()->overduehandling === 'graceperiod') { - // Normally, we record the accurate finish time when the student is online. - $finishtime = $timenow; - } else { - // But, if there is no grade period, and the final responses were too - // late to be processed, record the close time, to reduce confusion. - $finishtime = $timeclose; - } - $this->process_finish($timenow, !$toolate, $finishtime, true); - } - - } catch (question_out_of_sequence_exception $e) { - throw new moodle_exception('submissionoutofsequencefriendlymessage', 'question', - $this->attempt_url(null, $thispage)); - - } catch (Exception $e) { - // This sucks, if we display our own custom error message, there is no way - // to display the original stack trace. - $debuginfo = ''; - if (!empty($e->debuginfo)) { - $debuginfo = $e->debuginfo; - } - throw new moodle_exception('errorprocessingresponses', 'question', - $this->attempt_url(null, $thispage), $e->getMessage(), $debuginfo); - } - - // Send the user to the review page. - $transaction->allow_commit(); - - return $becomingabandoned ? self::ABANDONED : self::FINISHED; - } - - /** - * Check a page read access to see if is an out of sequence access. - * - * If allownext is set then we also check whether access to the page - * after the current one should be permitted. - * - * @param int $page page number. - * @param bool $allownext in case of a sequential navigation, can we go to next page ? - * @return boolean false is an out of sequence access, true otherwise. - * @since Moodle 3.1 - */ - public function check_page_access(int $page, bool $allownext = true): bool { - if ($this->get_navigation_method() != QUIZ_NAVMETHOD_SEQ) { - return true; - } - // Sequential access: allow access to the summary, current page or next page. - // Or if the user review his/her attempt, see MDLQA-1523. - return $page == -1 - || $page == $this->get_currentpage() - || $allownext && ($page == $this->get_currentpage() + 1); - } - - /** - * Update attempt page. - * - * @param int $page page number. - * @return boolean true if everything was ok, false otherwise (out of sequence access). - * @since Moodle 3.1 - */ - public function set_currentpage($page) { - global $DB; - - if ($this->check_page_access($page)) { - $DB->set_field('quiz_attempts', 'currentpage', $page, array('id' => $this->get_attemptid())); - return true; - } - return false; - } - - /** - * Trigger the attempt_viewed event. - * - * @since Moodle 3.1 - */ - public function fire_attempt_viewed_event() { - $params = array( - 'objectid' => $this->get_attemptid(), - 'relateduserid' => $this->get_userid(), - 'courseid' => $this->get_courseid(), - 'context' => context_module::instance($this->get_cmid()), - 'other' => array( - 'quizid' => $this->get_quizid(), - 'page' => $this->get_currentpage() - ) - ); - $event = \mod_quiz\event\attempt_viewed::create($params); - $event->add_record_snapshot('quiz_attempts', $this->get_attempt()); - $event->trigger(); - } - - /** - * Trigger the attempt_updated event. - * - * @return void - */ - public function fire_attempt_updated_event(): void { - $params = [ - 'objectid' => $this->get_attemptid(), - 'relateduserid' => $this->get_userid(), - 'courseid' => $this->get_courseid(), - 'context' => context_module::instance($this->get_cmid()), - 'other' => [ - 'quizid' => $this->get_quizid(), - 'page' => $this->get_currentpage() - ] - ]; - $event = \mod_quiz\event\attempt_updated::create($params); - $event->add_record_snapshot('quiz_attempts', $this->get_attempt()); - $event->trigger(); - } - - /** - * Trigger the attempt_autosaved event. - * - * @return void - */ - public function fire_attempt_autosaved_event(): void { - $params = [ - 'objectid' => $this->get_attemptid(), - 'relateduserid' => $this->get_userid(), - 'courseid' => $this->get_courseid(), - 'context' => context_module::instance($this->get_cmid()), - 'other' => [ - 'quizid' => $this->get_quizid(), - 'page' => $this->get_currentpage() - ] - ]; - $event = \mod_quiz\event\attempt_autosaved::create($params); - $event->add_record_snapshot('quiz_attempts', $this->get_attempt()); - $event->trigger(); - } - - /** - * Trigger the attempt_question_restarted event. - * - * @param int $slot Slot number - * @param int $newquestionid New question id. - * @return void - */ - public function fire_attempt_question_restarted_event(int $slot, int $newquestionid): void { - $params = [ - 'objectid' => $this->get_attemptid(), - 'relateduserid' => $this->get_userid(), - 'courseid' => $this->get_courseid(), - 'context' => context_module::instance($this->get_cmid()), - 'other' => [ - 'quizid' => $this->get_quizid(), - 'page' => $this->get_currentpage(), - 'slot' => $slot, - 'newquestionid' => $newquestionid - ] - ]; - $event = \mod_quiz\event\attempt_question_restarted::create($params); - $event->add_record_snapshot('quiz_attempts', $this->get_attempt()); - $event->trigger(); - } - - /** - * Trigger the attempt_summary_viewed event. - * - * @since Moodle 3.1 - */ - public function fire_attempt_summary_viewed_event() { - - $params = array( - 'objectid' => $this->get_attemptid(), - 'relateduserid' => $this->get_userid(), - 'courseid' => $this->get_courseid(), - 'context' => context_module::instance($this->get_cmid()), - 'other' => array( - 'quizid' => $this->get_quizid() - ) - ); - $event = \mod_quiz\event\attempt_summary_viewed::create($params); - $event->add_record_snapshot('quiz_attempts', $this->get_attempt()); - $event->trigger(); - } - - /** - * Trigger the attempt_reviewed event. - * - * @since Moodle 3.1 - */ - public function fire_attempt_reviewed_event() { - - $params = array( - 'objectid' => $this->get_attemptid(), - 'relateduserid' => $this->get_userid(), - 'courseid' => $this->get_courseid(), - 'context' => context_module::instance($this->get_cmid()), - 'other' => array( - 'quizid' => $this->get_quizid() - ) - ); - $event = \mod_quiz\event\attempt_reviewed::create($params); - $event->add_record_snapshot('quiz_attempts', $this->get_attempt()); - $event->trigger(); - } - - /** - * Trigger the attempt manual grading completed event. - */ - public function fire_attempt_manual_grading_completed_event() { - $params = [ - 'objectid' => $this->get_attemptid(), - 'relateduserid' => $this->get_userid(), - 'courseid' => $this->get_courseid(), - 'context' => context_module::instance($this->get_cmid()), - 'other' => [ - 'quizid' => $this->get_quizid() - ] - ]; - - $event = \mod_quiz\event\attempt_manual_grading_completed::create($params); - $event->add_record_snapshot('quiz_attempts', $this->get_attempt()); - $event->trigger(); - } - - /** - * Update the timemodifiedoffline attempt field. - * - * This function should be used only when web services are being used. - * - * @param int $time time stamp. - * @return boolean false if the field is not updated because web services aren't being used. - * @since Moodle 3.2 - */ - public function set_offline_modified_time($time) { - // Update the timemodifiedoffline field only if web services are being used. - if (WS_SERVER) { - $this->attempt->timemodifiedoffline = $time; - return true; - } - return false; - } - - /** - * Get the total number of unanswered questions in the attempt. - * - * @return int - */ - public function get_number_of_unanswered_questions(): int { - $totalunanswered = 0; - foreach ($this->get_slots() as $slot) { - $questionstate = $this->get_question_state($slot); - if ($questionstate == question_state::$todo || $questionstate == question_state::$invalid) { - $totalunanswered++; - } - } - return $totalunanswered; - } -} diff --git a/mod/quiz/classes/access_manager.php b/mod/quiz/classes/access_manager.php index c17893cd13f..8ba515df7af 100644 --- a/mod/quiz/classes/access_manager.php +++ b/mod/quiz/classes/access_manager.php @@ -26,7 +26,6 @@ use moodle_page; use moodle_url; use MoodleQuickForm; use quiz; -use quiz_attempt; use stdClass; /** diff --git a/mod/quiz/classes/external.php b/mod/quiz/classes/external.php index 0b23ad3add2..da33c5da4f3 100644 --- a/mod/quiz/classes/external.php +++ b/mod/quiz/classes/external.php @@ -26,6 +26,7 @@ use core_course\external\helper_for_get_mods_by_courses; use mod_quiz\access_manager; +use mod_quiz\quiz_attempt; defined('MOODLE_INTERNAL') || die; diff --git a/mod/quiz/classes/local/reports/attempts_report_options.php b/mod/quiz/classes/local/reports/attempts_report_options.php index b66872845fe..5d8f92383d5 100644 --- a/mod/quiz/classes/local/reports/attempts_report_options.php +++ b/mod/quiz/classes/local/reports/attempts_report_options.php @@ -17,8 +17,8 @@ namespace mod_quiz\local\reports; use context_module; +use mod_quiz\quiz_attempt; use moodle_url; -use quiz_attempt; use stdClass; /** diff --git a/mod/quiz/classes/local/reports/attempts_report_table.php b/mod/quiz/classes/local/reports/attempts_report_table.php index 993e6385e3e..d55d0131272 100644 --- a/mod/quiz/classes/local/reports/attempts_report_table.php +++ b/mod/quiz/classes/local/reports/attempts_report_table.php @@ -23,6 +23,7 @@ require_once($CFG->libdir.'/tablelib.php'); use coding_exception; use context_module; use html_writer; +use mod_quiz\quiz_attempt; use moodle_url; use popup_action; use question_state; @@ -30,7 +31,6 @@ use qubaid_condition; use qubaid_join; use qubaid_list; use question_engine_data_mapper; -use quiz_attempt; use stdClass; /** diff --git a/mod/quiz/classes/output/links_to_other_attempts.php b/mod/quiz/classes/output/links_to_other_attempts.php index 7a94410281d..3fede712925 100644 --- a/mod/quiz/classes/output/links_to_other_attempts.php +++ b/mod/quiz/classes/output/links_to_other_attempts.php @@ -32,7 +32,7 @@ class links_to_other_attempts implements renderable { * - null if this is the current attempt, and so should not be linked. (Just the number is output.) * - moodle_url if this is a different attempt. (Output as a link to the URL with the number as link text.) * - a renderable, in which case the results of rendering the renderable is output. - * (This is used by {@see quiz_attempt::links_to_other_redos()}.) + * (The third option is used by {@see quiz_attempt::links_to_other_redos()}.) */ public $links = []; } diff --git a/mod/quiz/classes/output/navigation_panel_base.php b/mod/quiz/classes/output/navigation_panel_base.php index 057729f9415..6de96a68b1a 100644 --- a/mod/quiz/classes/output/navigation_panel_base.php +++ b/mod/quiz/classes/output/navigation_panel_base.php @@ -16,12 +16,11 @@ namespace mod_quiz\output; -use moodle_page; +use mod_quiz\quiz_attempt; use moodle_url; use question_attempt; use question_display_options; use question_state; -use quiz_attempt; use renderable; use user_picture; diff --git a/mod/quiz/classes/output/renderer.php b/mod/quiz/classes/output/renderer.php index 9c691d899d7..0f62095b2fa 100644 --- a/mod/quiz/classes/output/renderer.php +++ b/mod/quiz/classes/output/renderer.php @@ -26,12 +26,12 @@ use html_writer; use mod_quiz\access_manager; use mod_quiz\form\preflight_check_form; use mod_quiz\question\display_options; +use mod_quiz\quiz_attempt; use moodle_url; use plugin_renderer_base; use popup_action; use question_display_options; use quiz; -use quiz_attempt; use renderable; use single_button; use stdClass; diff --git a/mod/quiz/classes/output/view_page.php b/mod/quiz/classes/output/view_page.php index 6475a31867d..ec437bfafa7 100644 --- a/mod/quiz/classes/output/view_page.php +++ b/mod/quiz/classes/output/view_page.php @@ -18,6 +18,7 @@ namespace mod_quiz\output; use mod_quiz\access_manager; use mod_quiz\form\preflight_check_form; +use mod_quiz\quiz_attempt; use moodle_url; /** @@ -36,7 +37,7 @@ class view_page { public $infomessages; /** @var array $attempts contains all the user's attempts at this quiz. */ public $attempts; - /** @var array $attemptobjs quiz_attempt objects corresponding to $attempts. */ + /** @var quiz_attempt[] $attemptobjs objects corresponding to $attempts. */ public $attemptobjs; /** @var access_manager $accessmanager contains various access rules. */ public $accessmanager; diff --git a/mod/quiz/classes/privacy/provider.php b/mod/quiz/classes/privacy/provider.php index 82b4e9e58f8..94cbc33e3a2 100644 --- a/mod/quiz/classes/privacy/provider.php +++ b/mod/quiz/classes/privacy/provider.php @@ -34,6 +34,7 @@ use core_privacy\local\metadata\collection; use core_privacy\local\request\userlist; use core_privacy\local\request\writer; use core_privacy\manager; +use mod_quiz\quiz_attempt; defined('MOODLE_INTERNAL') || die(); @@ -526,7 +527,7 @@ class provider implements // Store the quiz attempt data. $data = (object) [ - 'state' => \quiz_attempt::state_name($attempt->state), + 'state' => quiz_attempt::state_name($attempt->state), ]; if (!empty($attempt->timestart)) { diff --git a/mod/quiz/classes/question/qubaids_for_quiz.php b/mod/quiz/classes/question/qubaids_for_quiz.php index e42a650a838..5583349d061 100644 --- a/mod/quiz/classes/question/qubaids_for_quiz.php +++ b/mod/quiz/classes/question/qubaids_for_quiz.php @@ -16,6 +16,8 @@ namespace mod_quiz\question; +use mod_quiz\quiz_attempt; + defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/question/engine/datalib.php'); @@ -47,7 +49,7 @@ class qubaids_for_quiz extends \qubaid_join { if ($onlyfinished) { $where .= ' AND state = :statefinished'; - $params['statefinished'] = \quiz_attempt::FINISHED; + $params['statefinished'] = quiz_attempt::FINISHED; } parent::__construct('{quiz_attempts} quiza', 'quiza.uniqueid', $where, $params); diff --git a/mod/quiz/classes/question/qubaids_for_users_attempts.php b/mod/quiz/classes/question/qubaids_for_users_attempts.php index b3c102ca835..2f4c7ba1abf 100644 --- a/mod/quiz/classes/question/qubaids_for_users_attempts.php +++ b/mod/quiz/classes/question/qubaids_for_users_attempts.php @@ -16,6 +16,8 @@ namespace mod_quiz\question; +use mod_quiz\quiz_attempt; + defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/question/engine/datalib.php'); @@ -54,14 +56,14 @@ class qubaids_for_users_attempts extends \qubaid_join { case 'finished': $where .= ' AND state IN (:state1, :state2)'; - $params['state1'] = \quiz_attempt::FINISHED; - $params['state2'] = \quiz_attempt::ABANDONED; + $params['state1'] = quiz_attempt::FINISHED; + $params['state2'] = quiz_attempt::ABANDONED; break; case 'unfinished': $where .= ' AND state IN (:state1, :state2)'; - $params['state1'] = \quiz_attempt::IN_PROGRESS; - $params['state2'] = \quiz_attempt::OVERDUE; + $params['state1'] = quiz_attempt::IN_PROGRESS; + $params['state2'] = quiz_attempt::OVERDUE; break; } diff --git a/mod/quiz/classes/quiz_attempt.php b/mod/quiz/classes/quiz_attempt.php new file mode 100644 index 00000000000..df2897499bf --- /dev/null +++ b/mod/quiz/classes/quiz_attempt.php @@ -0,0 +1,2196 @@ +. + +namespace mod_quiz; + +use action_link; +use block_contents; +use coding_exception; +use context_module; +use dml_exception; +use html_writer; +use mod_quiz\output\links_to_other_attempts; +use mod_quiz\output\renderer; +use mod_quiz\question\bank\qbank_helper; +use mod_quiz\question\display_options; +use moodle_exception; +use moodle_url; +use popup_action; +use qtype_description_question; +use question_attempt; +use question_bank; +use question_display_options; +use question_engine; +use question_out_of_sequence_exception; +use question_state; +use question_usage_by_activity; +use quiz; +use stdClass; + +/** + * This class represents one user's attept at a particular quiz. + * + * @package mod_quiz + * @copyright 2008 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class quiz_attempt { + + /** @var string to identify the in progress state. */ + const IN_PROGRESS = 'inprogress'; + /** @var string to identify the overdue state. */ + const OVERDUE = 'overdue'; + /** @var string to identify the finished state. */ + const FINISHED = 'finished'; + /** @var string to identify the abandoned state. */ + const ABANDONED = 'abandoned'; + + /** @var int maximum number of slots in the quiz for the review page to default to show all. */ + const MAX_SLOTS_FOR_DEFAULT_REVIEW_SHOW_ALL = 50; + + /** @var quiz object containing the quiz settings. */ + protected $quizobj; + + /** @var stdClass the quiz_attempts row. */ + protected $attempt; + + /** @var question_usage_by_activity the question usage for this quiz attempt. */ + protected $quba; + + /** + * @var array of slot information. These objects contain ->slot (int), + * ->requireprevious (bool), ->questionids (int) the original question for random questions, + * ->firstinsection (bool), ->section (stdClass from $this->sections). + * This does not contain page - get that from {@link get_question_page()} - + * or maxmark - get that from $this->quba. + */ + protected $slots; + + /** @var array of quiz_sections rows, with a ->lastslot field added. */ + protected $sections; + + /** @var array page no => array of slot numbers on the page in order. */ + protected $pagelayout; + + /** @var array slot => displayed question number for this slot. (E.g. 1, 2, 3 or 'i'.) */ + protected $questionnumbers; + + /** @var array slot => page number for this slot. */ + protected $questionpages; + + /** @var display_options cache for the appropriate review options. */ + protected $reviewoptions = null; + + // Constructor ============================================================= + /** + * Constructor assuming we already have the necessary data loaded. + * + * @param object $attempt the row of the quiz_attempts table. + * @param object $quiz the quiz object for this attempt and user. + * @param object $cm the course_module object for this quiz. + * @param object $course the row from the course table for the course we belong to. + * @param bool $loadquestions (optional) if true, the default, load all the details + * of the state of each question. Else just set up the basic details of the attempt. + */ + public function __construct($attempt, $quiz, $cm, $course, $loadquestions = true) { + $this->attempt = $attempt; + $this->quizobj = new quiz($quiz, $cm, $course); + + if ($loadquestions) { + $this->load_questions(); + } + } + + /** + * Used by {create()} and {create_from_usage_id()}. + * + * @param array $conditions passed to $DB->get_record('quiz_attempts', $conditions). + * @return quiz_attempt the desired instance of this class. + */ + protected static function create_helper($conditions) { + global $DB; + + $attempt = $DB->get_record('quiz_attempts', $conditions, '*', MUST_EXIST); + $quiz = access_manager::load_quiz_and_settings($attempt->quiz); + $course = $DB->get_record('course', array('id' => $quiz->course), '*', MUST_EXIST); + $cm = get_coursemodule_from_instance('quiz', $quiz->id, $course->id, false, MUST_EXIST); + + // Update quiz with override information. + $quiz = quiz_update_effective_access($quiz, $attempt->userid); + + return new quiz_attempt($attempt, $quiz, $cm, $course); + } + + /** + * Static function to create a new quiz_attempt object given an attemptid. + * + * @param int $attemptid the attempt id. + * @return quiz_attempt the new quiz_attempt object + */ + public static function create($attemptid) { + return self::create_helper(array('id' => $attemptid)); + } + + /** + * Static function to create a new quiz_attempt object given a usage id. + * + * @param int $usageid the attempt usage id. + * @return quiz_attempt the new quiz_attempt object + */ + public static function create_from_usage_id($usageid) { + return self::create_helper(array('uniqueid' => $usageid)); + } + + /** + * @param string $state one of the state constants like IN_PROGRESS. + * @return string the human-readable state name. + */ + public static function state_name($state) { + return quiz_attempt_state_name($state); + } + + /** + * This method can be called later if the object was constructed with $loadqusetions = false. + */ + public function load_questions() { + global $DB; + + if (isset($this->quba)) { + throw new coding_exception('This quiz attempt has already had the questions loaded.'); + } + + $this->quba = question_engine::load_questions_usage_by_activity($this->attempt->uniqueid); + $this->slots = $DB->get_records('quiz_slots', + ['quizid' => $this->get_quizid()], 'slot', 'slot, id, requireprevious, displaynumber'); + $this->sections = array_values($DB->get_records('quiz_sections', + ['quizid' => $this->get_quizid()], 'firstslot')); + + $this->link_sections_and_slots(); + $this->determine_layout(); + $this->number_questions(); + } + + /** + * Preload all attempt step users to show in Response history. + * + * @throws dml_exception + */ + public function preload_all_attempt_step_users(): void { + $this->quba->preload_all_step_users(); + } + + /** + * Let each slot know which section it is part of. + */ + protected function link_sections_and_slots() { + foreach ($this->sections as $i => $section) { + if (isset($this->sections[$i + 1])) { + $section->lastslot = $this->sections[$i + 1]->firstslot - 1; + } else { + $section->lastslot = count($this->slots); + } + for ($slot = $section->firstslot; $slot <= $section->lastslot; $slot += 1) { + $this->slots[$slot]->section = $section; + } + } + } + + /** + * Parse attempt->layout to populate the other arrays the represent the layout. + */ + protected function determine_layout() { + $this->pagelayout = array(); + + // Break up the layout string into pages. + $pagelayouts = explode(',0', $this->attempt->layout); + + // Strip off any empty last page (normally there is one). + if (end($pagelayouts) == '') { + array_pop($pagelayouts); + } + + // File the ids into the arrays. + // Tracking which is the first slot in each section in this attempt is + // trickier than you might guess, since the slots in this section + // may be shuffled, so $section->firstslot (the lowest numbered slot in + // the section) may not be the first one. + $unseensections = $this->sections; + $this->pagelayout = array(); + foreach ($pagelayouts as $page => $pagelayout) { + $pagelayout = trim($pagelayout, ','); + if ($pagelayout == '') { + continue; + } + $this->pagelayout[$page] = explode(',', $pagelayout); + foreach ($this->pagelayout[$page] as $slot) { + $sectionkey = array_search($this->slots[$slot]->section, $unseensections); + if ($sectionkey !== false) { + $this->slots[$slot]->firstinsection = true; + unset($unseensections[$sectionkey]); + } else { + $this->slots[$slot]->firstinsection = false; + } + } + } + } + + /** + * Work out the number to display for each question/slot. + */ + protected function number_questions() { + $number = 1; + foreach ($this->pagelayout as $page => $slots) { + foreach ($slots as $slot) { + if ($length = $this->is_real_question($slot)) { + // Whether question numbering is customised or is numeric and automatically incremented. + if (!empty($this->slots[$slot]->displaynumber) && !is_null($this->slots[$slot]->displaynumber)) { + $this->questionnumbers[$slot] = $this->slots[$slot]->displaynumber; + } else { + $this->questionnumbers[$slot] = $number; + } + $number += $length; + } else { + $this->questionnumbers[$slot] = get_string('infoshort', 'quiz'); + } + $this->questionpages[$slot] = $page; + } + } + } + + /** + * If the given page number is out of range (before the first page, or after + * the last page, chnage it to be within range). + * + * @param int $page the requested page number. + * @return int a safe page number to use. + */ + public function force_page_number_into_range($page) { + return min(max($page, 0), count($this->pagelayout) - 1); + } + + // Simple getters ========================================================== + public function get_quiz() { + return $this->quizobj->get_quiz(); + } + + public function get_quizobj() { + return $this->quizobj; + } + + /** @return int the course id. */ + public function get_courseid() { + return $this->quizobj->get_courseid(); + } + + /** + * Get the course settings object. + * + * @return stdClass the course settings object. + */ + public function get_course() { + return $this->quizobj->get_course(); + } + + /** @return int the quiz id. */ + public function get_quizid() { + return $this->quizobj->get_quizid(); + } + + /** @return string the name of this quiz. */ + public function get_quiz_name() { + return $this->quizobj->get_quiz_name(); + } + + /** @return int the quiz navigation method. */ + public function get_navigation_method() { + return $this->quizobj->get_navigation_method(); + } + + /** @return object the course_module object. */ + public function get_cm() { + return $this->quizobj->get_cm(); + } + + /** + * Get the course-module id. + * + * @return int the course_module id. + */ + public function get_cmid() { + return $this->quizobj->get_cmid(); + } + + /** + * @return bool whether the current user is someone who previews the quiz, + * rather than attempting it. + */ + public function is_preview_user() { + return $this->quizobj->is_preview_user(); + } + + /** @return int the number of attempts allowed at this quiz (0 = infinite). */ + public function get_num_attempts_allowed() { + return $this->quizobj->get_num_attempts_allowed(); + } + + /** @return int number fo pages in this quiz. */ + public function get_num_pages() { + return count($this->pagelayout); + } + + /** + * @param int $timenow the current time as a unix timestamp. + * @return access_manager and instance of the access_manager class + * for this quiz at this time. + */ + public function get_access_manager($timenow) { + return $this->quizobj->get_access_manager($timenow); + } + + /** @return int the attempt id. */ + public function get_attemptid() { + return $this->attempt->id; + } + + /** @return int the attempt unique id. */ + public function get_uniqueid() { + return $this->attempt->uniqueid; + } + + /** @return object the row from the quiz_attempts table. */ + public function get_attempt() { + return $this->attempt; + } + + /** @return int the number of this attemp (is it this user's first, second, ... attempt). */ + public function get_attempt_number() { + return $this->attempt->attempt; + } + + /** @return string one of the quiz_attempt::IN_PROGRESS, FINISHED, OVERDUE or ABANDONED constants. */ + public function get_state() { + return $this->attempt->state; + } + + /** @return int the id of the user this attempt belongs to. */ + public function get_userid() { + return $this->attempt->userid; + } + + /** @return int the current page of the attempt. */ + public function get_currentpage() { + return $this->attempt->currentpage; + } + + public function get_sum_marks() { + return $this->attempt->sumgrades; + } + + /** + * @return bool whether this attempt has been finished (true) or is still + * in progress (false). Be warned that this is not just state == self::FINISHED, + * it also includes self::ABANDONED. + */ + public function is_finished() { + return $this->attempt->state == self::FINISHED || $this->attempt->state == self::ABANDONED; + } + + /** @return bool whether this attempt is a preview attempt. */ + public function is_preview() { + return $this->attempt->preview; + } + + /** + * Is this someone dealing with their own attempt or preview? + * + * @return bool true => own attempt/preview. false => reviewing someone else's. + */ + public function is_own_attempt() { + global $USER; + return $this->attempt->userid == $USER->id; + } + + /** + * @return bool whether this attempt is a preview belonging to the current user. + */ + public function is_own_preview() { + return $this->is_own_attempt() && + $this->is_preview_user() && $this->attempt->preview; + } + + /** + * Is the current user allowed to review this attempt. This applies when + * {@link is_own_attempt()} returns false. + * + * @return bool whether the review should be allowed. + */ + public function is_review_allowed() { + if (!$this->has_capability('mod/quiz:viewreports')) { + return false; + } + + $cm = $this->get_cm(); + if ($this->has_capability('moodle/site:accessallgroups') || + groups_get_activity_groupmode($cm) != SEPARATEGROUPS) { + return true; + } + + // Check the users have at least one group in common. + $teachersgroups = groups_get_activity_allowed_groups($cm); + $studentsgroups = groups_get_all_groups( + $cm->course, $this->attempt->userid, $cm->groupingid); + return $teachersgroups && $studentsgroups && + array_intersect(array_keys($teachersgroups), array_keys($studentsgroups)); + } + + /** + * Has the student, in this attempt, engaged with the quiz in a non-trivial way? + * + * That is, is there any question worth a non-zero number of marks, where + * the student has made some response that we have saved? + * + * @return bool true if we have saved a response for at least one graded question. + */ + public function has_response_to_at_least_one_graded_question() { + foreach ($this->quba->get_attempt_iterator() as $qa) { + if ($qa->get_max_mark() == 0) { + continue; + } + if ($qa->get_num_steps() > 1) { + return true; + } + } + return false; + } + + /** + * Do any questions in this attempt need to be graded manually? + * + * @return bool True if we have at least one question still needs manual grading. + */ + public function requires_manual_grading(): bool { + return $this->quba->get_total_mark() === null; + } + + /** + * Get extra summary information about this attempt. + * + * Some behaviours may be able to provide interesting summary information + * about the attempt as a whole, and this method provides access to that data. + * To see how this works, try setting a quiz to one of the CBM behaviours, + * and then look at the extra information displayed at the top of the quiz + * review page once you have sumitted an attempt. + * + * In the return value, the array keys are identifiers of the form + * qbehaviour_behaviourname_meaningfullkey. For qbehaviour_deferredcbm_highsummary. + * The values are arrays with two items, title and content. Each of these + * will be either a string, or a renderable. + * + * @param question_display_options $options the display options for this quiz attempt at this time. + * @return array as described above. + */ + public function get_additional_summary_data(question_display_options $options) { + return $this->quba->get_summary_information($options); + } + + /** + * Get the overall feedback corresponding to a particular mark. + * + * @param number $grade a particular grade. + * @return string the feedback. + */ + public function get_overall_feedback($grade) { + return quiz_feedback_for_grade($grade, $this->get_quiz(), + $this->quizobj->get_context()); + } + + /** + * Wrapper round the has_capability funciton that automatically passes in the quiz context. + * + * @param string $capability the name of the capability to check. For example mod/forum:view. + * @param int|null $userid A user id. By default (null) checks the permissions of the current user. + * @param bool $doanything If false, ignore effect of admin role assignment. + * @return boolean true if the user has this capability. Otherwise false. + */ + public function has_capability($capability, $userid = null, $doanything = true) { + return $this->quizobj->has_capability($capability, $userid, $doanything); + } + + /** + * Wrapper round the require_capability function that automatically passes in the quiz context. + * + * @param string $capability the name of the capability to check. For example mod/forum:view. + * @param int|null $userid A user id. By default (null) checks the permissions of the current user. + * @param bool $doanything If false, ignore effect of admin role assignment. + */ + public function require_capability($capability, $userid = null, $doanything = true) { + $this->quizobj->require_capability($capability, $userid, $doanything); + } + + /** + * Check the appropriate capability to see whether this user may review their own attempt. + * If not, prints an error. + */ + public function check_review_capability() { + if ($this->get_attempt_state() == display_options::IMMEDIATELY_AFTER) { + $capability = 'mod/quiz:attempt'; + } else { + $capability = 'mod/quiz:reviewmyattempts'; + } + + // These next tests are in a slighly funny order. The point is that the + // common and most performance-critical case is students attempting a quiz + // so we want to check that permisison first. + + if ($this->has_capability($capability)) { + // User has the permission that lets you do the quiz as a student. Fine. + return; + } + + if ($this->has_capability('mod/quiz:viewreports') || + $this->has_capability('mod/quiz:preview')) { + // User has the permission that lets teachers review. Fine. + return; + } + + // They should not be here. Trigger the standard no-permission error + // but using the name of the student capability. + // We know this will fail. We just want the stadard exception thown. + $this->require_capability($capability); + } + + /** + * Checks whether a user may navigate to a particular slot. + * + * @param int $slot the target slot (currently does not affect the answer). + * @return bool true if the navigation should be allowed. + */ + public function can_navigate_to($slot) { + if ($this->attempt->state == self::OVERDUE) { + // When the attempt is overdue, students can only see the + // attempt summary page and cannot navigate anywhere else. + return false; + } + + switch ($this->get_navigation_method()) { + case QUIZ_NAVMETHOD_FREE: + return true; + break; + case QUIZ_NAVMETHOD_SEQ: + return false; + break; + } + return true; + } + + /** + * @return int one of the display_options::DURING, + * IMMEDIATELY_AFTER, LATER_WHILE_OPEN or AFTER_CLOSE constants. + */ + public function get_attempt_state() { + return quiz_attempt_state($this->get_quiz(), $this->attempt); + } + + /** + * Wrapper that the correct display_options for this quiz at the + * moment. + * + * @param bool $reviewing true for options when reviewing, false for when attempting. + * @return question_display_options the render options for this user on this attempt. + */ + public function get_display_options($reviewing) { + if ($reviewing) { + if (is_null($this->reviewoptions)) { + $this->reviewoptions = quiz_get_review_options($this->get_quiz(), + $this->attempt, $this->quizobj->get_context()); + if ($this->is_own_preview()) { + // It should always be possible for a teacher to review their + // own preview irrespective of the review options settings. + $this->reviewoptions->attempt = true; + } + } + return $this->reviewoptions; + + } else { + $options = display_options::make_from_quiz($this->get_quiz(), + display_options::DURING); + $options->flags = quiz_get_flag_option($this->attempt, $this->quizobj->get_context()); + return $options; + } + } + + /** + * Wrapper that the correct display_options for this quiz at the + * moment. + * + * @param bool $reviewing true for review page, else attempt page. + * @param int $slot which question is being displayed. + * @param moodle_url $thispageurl to return to after the editing form is + * submitted or cancelled. If null, no edit link will be generated. + * + * @return question_display_options the render options for this user on this + * attempt, with extra info to generate an edit link, if applicable. + */ + public function get_display_options_with_edit_link($reviewing, $slot, $thispageurl) { + $options = clone($this->get_display_options($reviewing)); + + if (!$thispageurl) { + return $options; + } + + if (!($reviewing || $this->is_preview())) { + return $options; + } + + $question = $this->quba->get_question($slot, false); + if (!question_has_capability_on($question, 'edit', $question->category)) { + return $options; + } + + $options->editquestionparams['cmid'] = $this->get_cmid(); + $options->editquestionparams['returnurl'] = $thispageurl; + + return $options; + } + + /** + * @param int $page page number + * @return bool true if this is the last page of the quiz. + */ + public function is_last_page($page) { + return $page == count($this->pagelayout) - 1; + } + + /** + * Return the list of slot numbers for either a given page of the quiz, or for the + * whole quiz. + * + * @param mixed $page string 'all' or integer page number. + * @return array the requested list of slot numbers. + */ + public function get_slots($page = 'all') { + if ($page === 'all') { + $numbers = array(); + foreach ($this->pagelayout as $numbersonpage) { + $numbers = array_merge($numbers, $numbersonpage); + } + return $numbers; + } else { + return $this->pagelayout[$page]; + } + } + + /** + * Return the list of slot numbers for either a given page of the quiz, or for the + * whole quiz. + * + * @param mixed $page string 'all' or integer page number. + * @return array the requested list of slot numbers. + */ + public function get_active_slots($page = 'all') { + $activeslots = array(); + foreach ($this->get_slots($page) as $slot) { + if (!$this->is_blocked_by_previous_question($slot)) { + $activeslots[] = $slot; + } + } + return $activeslots; + } + + /** + * Helper method for unit tests. Get the underlying question usage object. + * + * @return question_usage_by_activity the usage. + */ + public function get_question_usage() { + if (!(PHPUNIT_TEST || defined('BEHAT_TEST'))) { + throw new coding_exception('get_question_usage is only for use in unit tests. ' . + 'For other operations, use the quiz_attempt api, or extend it properly.'); + } + return $this->quba; + } + + /** + * Get the question_attempt object for a particular question in this attempt. + * + * @param int $slot the number used to identify this question within this attempt. + * @return question_attempt the requested question_attempt. + */ + public function get_question_attempt($slot) { + return $this->quba->get_question_attempt($slot); + } + + /** + * Get all the question_attempt objects that have ever appeared in a given slot. + * + * This relates to the 'Try another question like this one' feature. + * + * @param int $slot the number used to identify this question within this attempt. + * @return question_attempt[] the attempts. + */ + public function all_question_attempts_originally_in_slot($slot) { + $qas = array(); + foreach ($this->quba->get_attempt_iterator() as $qa) { + if ($qa->get_metadata('originalslot') == $slot) { + $qas[] = $qa; + } + } + $qas[] = $this->quba->get_question_attempt($slot); + return $qas; + } + + /** + * Is a particular question in this attempt a real question, or something like a description. + * + * @param int $slot the number used to identify this question within this attempt. + * @return int whether that question is a real question. Actually returns the + * question length, which could theoretically be greater than one. + */ + public function is_real_question($slot) { + return $this->quba->get_question($slot, false)->length; + } + + /** + * Is a particular question in this attempt a real question, or something like a description. + * + * @param int $slot the number used to identify this question within this attempt. + * @return bool whether that question is a real question. + */ + public function is_question_flagged($slot) { + return $this->quba->get_question_attempt($slot)->is_flagged(); + } + + /** + * Checks whether the question in this slot requires the previous + * question to have been completed. + * + * @param int $slot the number used to identify this question within this attempt. + * @return bool whether the previous question must have been completed before + * this one can be seen. + */ + public function is_blocked_by_previous_question($slot) { + return $slot > 1 && isset($this->slots[$slot]) && $this->slots[$slot]->requireprevious && + !$this->slots[$slot]->section->shufflequestions && + !$this->slots[$slot - 1]->section->shufflequestions && + $this->get_navigation_method() != QUIZ_NAVMETHOD_SEQ && + !$this->get_question_state($slot - 1)->is_finished() && + $this->quba->can_question_finish_during_attempt($slot - 1); + } + + /** + * Is it possible for this question to be re-started within this attempt? + * + * @param int $slot the number used to identify this question within this attempt. + * @return bool whether the student should be given the option to restart this question now. + */ + public function can_question_be_redone_now($slot) { + return $this->get_quiz()->canredoquestions && !$this->is_finished() && + $this->get_question_state($slot)->is_finished(); + } + + /** + * Given a slot in this attempt, which may or not be a redone question, return the original slot. + * + * @param int $slot identifies a particular question in this attempt. + * @return int the slot where this question was originally. + */ + public function get_original_slot($slot) { + $originalslot = $this->quba->get_question_attempt_metadata($slot, 'originalslot'); + if ($originalslot) { + return $originalslot; + } else { + return $slot; + } + } + + /** + * Get the displayed question number for a slot. + * + * @param int $slot the number used to identify this question within this attempt. + * @return string the displayed question number for the question in this slot. + * For example '1', '2', '3' or 'i'. + */ + public function get_question_number($slot) { + return $this->questionnumbers[$slot]; + } + + /** + * If the section heading, if any, that should come just before this slot. + * + * @param int $slot identifies a particular question in this attempt. + * @return string the required heading, or null if there is not one here. + */ + public function get_heading_before_slot($slot) { + if ($this->slots[$slot]->firstinsection) { + return $this->slots[$slot]->section->heading; + } else { + return null; + } + } + + /** + * Return the page of the quiz where this question appears. + * + * @param int $slot the number used to identify this question within this attempt. + * @return int the page of the quiz this question appears on. + */ + public function get_question_page($slot) { + return $this->questionpages[$slot]; + } + + /** + * Return the grade obtained on a particular question, if the user is permitted + * to see it. You must previously have called load_question_states to load the + * state data about this question. + * + * @param int $slot the number used to identify this question within this attempt. + * @return string the formatted grade, to the number of decimal places specified + * by the quiz. + */ + public function get_question_name($slot) { + return $this->quba->get_question($slot, false)->name; + } + + /** + * Return the {@link question_state} that this question is in. + * + * @param int $slot the number used to identify this question within this attempt. + * @return question_state the state this question is in. + */ + public function get_question_state($slot) { + return $this->quba->get_question_state($slot); + } + + /** + * Return the grade obtained on a particular question, if the user is permitted + * to see it. You must previously have called load_question_states to load the + * state data about this question. + * + * @param int $slot the number used to identify this question within this attempt. + * @param bool $showcorrectness Whether right/partial/wrong states should + * be distinguished. + * @return string the formatted grade, to the number of decimal places specified + * by the quiz. + */ + public function get_question_status($slot, $showcorrectness) { + return $this->quba->get_question_state_string($slot, $showcorrectness); + } + + /** + * Return the grade obtained on a particular question, if the user is permitted + * to see it. You must previously have called load_question_states to load the + * state data about this question. + * + * @param int $slot the number used to identify this question within this attempt. + * @param bool $showcorrectness Whether right/partial/wrong states should + * be distinguished. + * @return string class name for this state. + */ + public function get_question_state_class($slot, $showcorrectness) { + return $this->quba->get_question_state_class($slot, $showcorrectness); + } + + /** + * Return the grade obtained on a particular question. + * + * You must previously have called load_question_states to load the state + * data about this question. + * + * @param int $slot the number used to identify this question within this attempt. + * @return string the formatted grade, to the number of decimal places specified by the quiz. + */ + public function get_question_mark($slot) { + return quiz_format_question_grade($this->get_quiz(), $this->quba->get_question_mark($slot)); + } + + /** + * Get the time of the most recent action performed on a question. + * + * @param int $slot the number used to identify this question within this usage. + * @return int timestamp. + */ + public function get_question_action_time($slot) { + return $this->quba->get_question_action_time($slot); + } + + /** + * Return the question type name for a given slot within the current attempt. + * + * @param int $slot the number used to identify this question within this attempt. + * @return string the question type name. + * @since Moodle 3.1 + */ + public function get_question_type_name($slot) { + return $this->quba->get_question($slot, false)->get_type_name(); + } + + /** + * Get the time remaining for an in-progress attempt, if the time is short + * enough that it would be worth showing a timer. + * + * @param int $timenow the time to consider as 'now'. + * @return int|false the number of seconds remaining for this attempt. + * False if there is no limit. + */ + public function get_time_left_display($timenow) { + if ($this->attempt->state != self::IN_PROGRESS) { + return false; + } + return $this->get_access_manager($timenow)->get_time_left_display($this->attempt, $timenow); + } + + + /** + * @return int the time when this attempt was submitted. 0 if it has not been + * submitted yet. + */ + public function get_submitted_date() { + return $this->attempt->timefinish; + } + + /** + * If the attempt is in an applicable state, work out the time by which the + * student should next do something. + * + * @return int timestamp by which the student needs to do something. + */ + public function get_due_date() { + $deadlines = array(); + if ($this->quizobj->get_quiz()->timelimit) { + $deadlines[] = $this->attempt->timestart + $this->quizobj->get_quiz()->timelimit; + } + if ($this->quizobj->get_quiz()->timeclose) { + $deadlines[] = $this->quizobj->get_quiz()->timeclose; + } + if ($deadlines) { + $duedate = min($deadlines); + } else { + return false; + } + + switch ($this->attempt->state) { + case self::IN_PROGRESS: + return $duedate; + + case self::OVERDUE: + return $duedate + $this->quizobj->get_quiz()->graceperiod; + + default: + throw new coding_exception('Unexpected state: ' . $this->attempt->state); + } + } + + // URLs related to this attempt ============================================ + /** + * @return string quiz view url. + */ + public function view_url() { + return $this->quizobj->view_url(); + } + + /** + * Get the URL to start or continue an attempt. + * + * @param int|null $slot which question in the attempt to go to after starting (optional). + * @param int $page which page in the attempt to go to after starting. + * @return string the URL of this quiz's edit page. Needs to be POSTed to with a cmid parameter. + */ + public function start_attempt_url($slot = null, $page = -1) { + if ($page == -1 && !is_null($slot)) { + $page = $this->get_question_page($slot); + } else { + $page = 0; + } + return $this->quizobj->start_attempt_url($page); + } + + /** + * Generates the title of the attempt page. + * + * @param int $page the page number (starting with 0) in the attempt. + * @return string attempt page title. + */ + public function attempt_page_title(int $page) : string { + if ($this->get_num_pages() > 1) { + $a = new stdClass(); + $a->name = $this->get_quiz_name(); + $a->currentpage = $page + 1; + $a->totalpages = $this->get_num_pages(); + $title = get_string('attempttitlepaged', 'quiz', $a); + } else { + $title = get_string('attempttitle', 'quiz', $this->get_quiz_name()); + } + + return $title; + } + + /** + * @param int|null $slot if specified, the slot number of a specific question to link to. + * @param int $page if specified, a particular page to link to. If not given deduced + * from $slot, or goes to the first page. + * @param int $thispage if not -1, the current page. Will cause links to other things on + * this page to be output as only a fragment. + * @return string the URL to continue this attempt. + */ + public function attempt_url($slot = null, $page = -1, $thispage = -1) { + return $this->page_and_question_url('attempt', $slot, $page, false, $thispage); + } + + /** + * Generates the title of the summary page. + * + * @return string summary page title. + */ + public function summary_page_title() : string { + return get_string('attemptsummarytitle', 'quiz', $this->get_quiz_name()); + } + + /** + * @return moodle_url the URL of this quiz's summary page. + */ + public function summary_url() { + return new moodle_url('/mod/quiz/summary.php', array('attempt' => $this->attempt->id, 'cmid' => $this->get_cmid())); + } + + /** + * @return moodle_url the URL of this quiz's summary page. + */ + public function processattempt_url() { + return new moodle_url('/mod/quiz/processattempt.php'); + } + + /** + * Generates the title of the review page. + * + * @param int $page the page number (starting with 0) in the attempt. + * @param bool $showall whether the review page contains the entire attempt on one page. + * @return string title of the review page. + */ + public function review_page_title(int $page, bool $showall = false) : string { + if (!$showall && $this->get_num_pages() > 1) { + $a = new stdClass(); + $a->name = $this->get_quiz_name(); + $a->currentpage = $page + 1; + $a->totalpages = $this->get_num_pages(); + $title = get_string('attemptreviewtitlepaged', 'quiz', $a); + } else { + $title = get_string('attemptreviewtitle', 'quiz', $this->get_quiz_name()); + } + + return $title; + } + + /** + * @param int|null $slot indicates which question to link to. + * @param int $page if specified, the URL of this particular page of the attempt, otherwise + * the URL will go to the first page. If -1, deduce $page from $slot. + * @param bool|null $showall if true, the URL will be to review the entire attempt on one page, + * and $page will be ignored. If null, a sensible default will be chosen. + * @param int $thispage if not -1, the current page. Will cause links to other things on + * this page to be output as only a fragment. + * @return string the URL to review this attempt. + */ + public function review_url($slot = null, $page = -1, $showall = null, $thispage = -1) { + return $this->page_and_question_url('review', $slot, $page, $showall, $thispage); + } + + /** + * By default, should this script show all questions on one page for this attempt? + * + * @param string $script the script name, e.g. 'attempt', 'summary', 'review'. + * @return bool whether show all on one page should be on by default. + */ + public function get_default_show_all($script) { + return $script === 'review' && count($this->questionpages) < self::MAX_SLOTS_FOR_DEFAULT_REVIEW_SHOW_ALL; + } + + // Bits of content ========================================================= + + /** + * If $reviewoptions->attempt is false, meaning that students can't review this + * attempt at the moment, return an appropriate string explaining why. + * + * @param bool $short if true, return a shorter string. + * @return string an appropriate message. + */ + public function cannot_review_message($short = false) { + return $this->quizobj->cannot_review_message( + $this->get_attempt_state(), $short); + } + + /** + * Initialise the JS etc. required all the questions on a page. + * + * @param int|string $page a page number, or 'all'. + * @param bool $showall if true forces page number to all. + * @return string HTML to output - mostly obsolete, will probably be an empty string. + */ + public function get_html_head_contributions($page = 'all', $showall = false) { + if ($showall) { + $page = 'all'; + } + $result = ''; + foreach ($this->get_slots($page) as $slot) { + $result .= $this->quba->render_question_head_html($slot); + } + $result .= question_engine::initialise_js(); + return $result; + } + + /** + * Initialise the JS etc. required by one question. + * + * @param int $slot the question slot number. + * @return string HTML to output - but this is mostly obsolete. Will probably be an empty string. + */ + public function get_question_html_head_contributions($slot) { + return $this->quba->render_question_head_html($slot) . + question_engine::initialise_js(); + } + + /** + * Print the HTML for the start new preview button, if the current user + * is allowed to see one. + * + * @return string HTML for the button. + */ + public function restart_preview_button() { + global $OUTPUT; + if ($this->is_preview() && $this->is_preview_user()) { + return $OUTPUT->single_button(new moodle_url( + $this->start_attempt_url(), array('forcenew' => true)), + get_string('startnewpreview', 'quiz')); + } else { + return ''; + } + } + + /** + * Generate the HTML that displayes the question in its current state, with + * the appropriate display options. + * + * @param int $slot identifies the question in the attempt. + * @param bool $reviewing is the being printed on an attempt or a review page. + * @param renderer $renderer the quiz renderer. + * @param moodle_url $thispageurl the URL of the page this question is being printed on. + * @return string HTML for the question in its current state. + */ + public function render_question($slot, $reviewing, renderer $renderer, $thispageurl = null) { + if ($this->is_blocked_by_previous_question($slot)) { + $placeholderqa = $this->make_blocked_question_placeholder($slot); + + $displayoptions = $this->get_display_options($reviewing); + $displayoptions->manualcomment = question_display_options::HIDDEN; + $displayoptions->history = question_display_options::HIDDEN; + $displayoptions->readonly = true; + + return html_writer::div($placeholderqa->render($displayoptions, + $this->get_question_number($this->get_original_slot($slot))), + 'mod_quiz-blocked_question_warning'); + } + + return $this->render_question_helper($slot, $reviewing, $thispageurl, $renderer, null); + } + + /** + * Helper used by {@link render_question()} and {@link render_question_at_step()}. + * + * @param int $slot identifies the question in the attempt. + * @param bool $reviewing is the being printed on an attempt or a review page. + * @param moodle_url $thispageurl the URL of the page this question is being printed on. + * @param renderer $renderer the quiz renderer. + * @param int|null $seq the seq number of the past state to display. + * @return string HTML fragment. + */ + protected function render_question_helper($slot, $reviewing, $thispageurl, + renderer $renderer, $seq) { + $originalslot = $this->get_original_slot($slot); + $number = $this->get_question_number($originalslot); + $displayoptions = $this->get_display_options_with_edit_link($reviewing, $slot, $thispageurl); + + if ($slot != $originalslot) { + $originalmaxmark = $this->get_question_attempt($slot)->get_max_mark(); + $this->get_question_attempt($slot)->set_max_mark($this->get_question_attempt($originalslot)->get_max_mark()); + } + + if ($this->can_question_be_redone_now($slot)) { + $displayoptions->extrainfocontent = $renderer->redo_question_button( + $slot, $displayoptions->readonly); + } + + if ($displayoptions->history && $displayoptions->questionreviewlink) { + $links = $this->links_to_other_redos($slot, $displayoptions->questionreviewlink); + if ($links) { + $displayoptions->extrahistorycontent = html_writer::tag('p', + get_string('redoesofthisquestion', 'quiz', $renderer->render($links))); + } + } + + if ($seq === null) { + $output = $this->quba->render_question($slot, $displayoptions, $number); + } else { + $output = $this->quba->render_question_at_step($slot, $seq, $displayoptions, $number); + } + + if ($slot != $originalslot) { + $this->get_question_attempt($slot)->set_max_mark($originalmaxmark); + } + + return $output; + } + + /** + * Create a fake question to be displayed in place of a question that is blocked + * until the previous question has been answered. + * + * @param int $slot int slot number of the question to replace. + * @return question_attempt the placeholder question attempt. + */ + protected function make_blocked_question_placeholder($slot) { + $replacedquestion = $this->get_question_attempt($slot)->get_question(false); + + question_bank::load_question_definition_classes('description'); + $question = new qtype_description_question(); + $question->id = $replacedquestion->id; + $question->category = null; + $question->parent = 0; + $question->qtype = question_bank::get_qtype('description'); + $question->name = ''; + $question->questiontext = get_string('questiondependsonprevious', 'quiz'); + $question->questiontextformat = FORMAT_HTML; + $question->generalfeedback = ''; + $question->defaultmark = $this->quba->get_question_max_mark($slot); + $question->length = $replacedquestion->length; + $question->penalty = 0; + $question->stamp = ''; + $question->status = \core_question\local\bank\question_version_status::QUESTION_STATUS_READY; + $question->timecreated = null; + $question->timemodified = null; + $question->createdby = null; + $question->modifiedby = null; + + $placeholderqa = new question_attempt($question, $this->quba->get_id(), + null, $this->quba->get_question_max_mark($slot)); + $placeholderqa->set_slot($slot); + $placeholderqa->start($this->get_quiz()->preferredbehaviour, 1); + $placeholderqa->set_flagged($this->is_question_flagged($slot)); + return $placeholderqa; + } + + /** + * Like {@link render_question()} but displays the question at the past step + * indicated by $seq, rather than showing the latest step. + * + * @param int $slot the slot number of a question in this quiz attempt. + * @param int $seq the seq number of the past state to display. + * @param bool $reviewing is the being printed on an attempt or a review page. + * @param renderer $renderer the quiz renderer. + * @param moodle_url $thispageurl the URL of the page this question is being printed on. + * @return string HTML for the question in its current state. + */ + public function render_question_at_step($slot, $seq, $reviewing, + renderer $renderer, $thispageurl = null) { + return $this->render_question_helper($slot, $reviewing, $thispageurl, $renderer, $seq); + } + + /** + * Wrapper round print_question from lib/questionlib.php. + * + * @param int $slot the id of a question in this quiz attempt. + * @return string HTML of the question. + */ + public function render_question_for_commenting($slot) { + $options = $this->get_display_options(true); + $options->generalfeedback = question_display_options::HIDDEN; + $options->manualcomment = question_display_options::EDITABLE; + return $this->quba->render_question($slot, $options, + $this->get_question_number($slot)); + } + + /** + * Check wheter access should be allowed to a particular file. + * + * @param int $slot the slot of a question in this quiz attempt. + * @param bool $reviewing is the being printed on an attempt or a review page. + * @param int $contextid the file context id from the request. + * @param string $component the file component from the request. + * @param string $filearea the file area from the request. + * @param array $args extra part components from the request. + * @param bool $forcedownload whether to force download. + * @return string HTML for the question in its current state. + */ + public function check_file_access($slot, $reviewing, $contextid, $component, + $filearea, $args, $forcedownload) { + $options = $this->get_display_options($reviewing); + + // Check permissions - warning there is similar code in review.php and + // reviewquestion.php. If you change on, change them all. + if ($reviewing && $this->is_own_attempt() && !$options->attempt) { + return false; + } + + if ($reviewing && !$this->is_own_attempt() && !$this->is_review_allowed()) { + return false; + } + + return $this->quba->check_file_access($slot, $options, + $component, $filearea, $args, $forcedownload); + } + + /** + * Get the navigation panel object for this attempt. + * + * @param renderer $output the quiz renderer to use to output things. + * @param string $panelclass The type of panel, navigation_panel_attempt::class or navigation_panel_review::class + * @param int $page the current page number. + * @param bool $showall whether we are showing the whole quiz on one page. (Used by review.php.) + * @return block_contents the requested object. + */ + public function get_navigation_panel(renderer $output, + $panelclass, $page, $showall = false) { + $panel = new $panelclass($this, $this->get_display_options(true), $page, $showall); + + $bc = new block_contents(); + $bc->attributes['id'] = 'mod_quiz_navblock'; + $bc->attributes['role'] = 'navigation'; + $bc->title = get_string('quiznavigation', 'quiz'); + $bc->content = $output->navigation_panel($panel); + return $bc; + } + + /** + * Return an array of variant URLs to other attempts at this quiz. + * + * The $url passed in must contain an attempt parameter. + * + * The {@see links_to_other_attempts} object returned contains an + * array with keys that are the attempt number, 1, 2, 3. + * The array values are either a {@link moodle_url} with the attempt parameter + * updated to point to the attempt id of the other attempt, or null corresponding + * to the current attempt number. + * + * @param moodle_url $url a URL. + * @return links_to_other_attempts|bool containing array int => null|moodle_url. + * False if none. + */ + public function links_to_other_attempts(moodle_url $url) { + $attempts = quiz_get_user_attempts($this->get_quiz()->id, $this->attempt->userid, 'all'); + if (count($attempts) <= 1) { + return false; + } + + $links = new links_to_other_attempts(); + foreach ($attempts as $at) { + if ($at->id == $this->attempt->id) { + $links->links[$at->attempt] = null; + } else { + $links->links[$at->attempt] = new moodle_url($url, array('attempt' => $at->id)); + } + } + return $links; + } + + /** + * Return an array of variant URLs to other redos of the question in a particular slot. + * + * The $url passed in must contain a slot parameter. + * + * The {@see links_to_other_attempts} object returned contains an + * array with keys that are the redo number, 1, 2, 3. + * The array values are either a {@see moodle_url} with the slot parameter + * updated to point to the slot that has that redo of this question; or null + * corresponding to the redo identified by $slot. + * + * @param int $slot identifies a question in this attempt. + * @param moodle_url $baseurl the base URL to modify to generate each link. + * @return links_to_other_attempts|null containing array int => null|moodle_url, + * or null if the question in this slot has not been redone. + */ + public function links_to_other_redos($slot, moodle_url $baseurl) { + $originalslot = $this->get_original_slot($slot); + + $qas = $this->all_question_attempts_originally_in_slot($originalslot); + if (count($qas) <= 1) { + return null; + } + + $links = new links_to_other_attempts(); + $index = 1; + foreach ($qas as $qa) { + if ($qa->get_slot() == $slot) { + $links->links[$index] = null; + } else { + $url = new moodle_url($baseurl, array('slot' => $qa->get_slot())); + $links->links[$index] = new action_link($url, $index, + new popup_action('click', $url, 'reviewquestion', + array('width' => 450, 'height' => 650)), + array('title' => get_string('reviewresponse', 'question'))); + } + $index++; + } + return $links; + } + + // Methods for processing ================================================== + + /** + * Check this attempt, to see if there are any state transitions that should + * happen automatically. This function will update the attempt checkstatetime. + * @param int $timestamp the timestamp that should be stored as the modified + * @param bool $studentisonline is the student currently interacting with Moodle? + */ + public function handle_if_time_expired($timestamp, $studentisonline) { + + $timeclose = $this->get_access_manager($timestamp)->get_end_time($this->attempt); + + if ($timeclose === false || $this->is_preview()) { + $this->update_timecheckstate(null); + return; // No time limit. + } + if ($timestamp < $timeclose) { + $this->update_timecheckstate($timeclose); + return; // Time has not yet expired. + } + + // If the attempt is already overdue, look to see if it should be abandoned ... + if ($this->attempt->state == self::OVERDUE) { + $timeoverdue = $timestamp - $timeclose; + $graceperiod = $this->quizobj->get_quiz()->graceperiod; + if ($timeoverdue >= $graceperiod) { + $this->process_abandon($timestamp, $studentisonline); + } else { + // Overdue time has not yet expired + $this->update_timecheckstate($timeclose + $graceperiod); + } + return; // ... and we are done. + } + + if ($this->attempt->state != self::IN_PROGRESS) { + $this->update_timecheckstate(null); + return; // Attempt is already in a final state. + } + + // Otherwise, we were in quiz_attempt::IN_PROGRESS, and time has now expired. + // Transition to the appropriate state. + switch ($this->quizobj->get_quiz()->overduehandling) { + case 'autosubmit': + $this->process_finish($timestamp, false, $studentisonline ? $timestamp : $timeclose, $studentisonline); + return; + + case 'graceperiod': + $this->process_going_overdue($timestamp, $studentisonline); + return; + + case 'autoabandon': + $this->process_abandon($timestamp, $studentisonline); + return; + } + + // This is an overdue attempt with no overdue handling defined, so just abandon. + $this->process_abandon($timestamp, $studentisonline); + return; + } + + /** + * Process all the actions that were submitted as part of the current request. + * + * @param int $timestamp the timestamp that should be stored as the modified. + * time in the database for these actions. If null, will use the current time. + * @param bool $becomingoverdue + * @param array|null $simulatedresponses If not null, then we are testing, and this is an array of simulated data. + * There are two formats supported here, for historical reasons. The newer approach is to pass an array created by + * {@link core_question_generator::get_simulated_post_data_for_questions_in_usage()}. + * the second is to pass an array slot no => contains arrays representing student + * responses which will be passed to {@link question_definition::prepare_simulated_post_data()}. + * This second method will probably get deprecated one day. + */ + public function process_submitted_actions($timestamp, $becomingoverdue = false, $simulatedresponses = null) { + global $DB; + + $transaction = $DB->start_delegated_transaction(); + + if ($simulatedresponses !== null) { + if (is_int(key($simulatedresponses))) { + // Legacy approach. Should be removed one day. + $simulatedpostdata = $this->quba->prepare_simulated_post_data($simulatedresponses); + } else { + $simulatedpostdata = $simulatedresponses; + } + } else { + $simulatedpostdata = null; + } + + $this->quba->process_all_actions($timestamp, $simulatedpostdata); + question_engine::save_questions_usage_by_activity($this->quba); + + $this->attempt->timemodified = $timestamp; + if ($this->attempt->state == self::FINISHED) { + $this->attempt->sumgrades = $this->quba->get_total_mark(); + } + if ($becomingoverdue) { + $this->process_going_overdue($timestamp, true); + } else { + $DB->update_record('quiz_attempts', $this->attempt); + } + + if (!$this->is_preview() && $this->attempt->state == self::FINISHED) { + quiz_save_best_grade($this->get_quiz(), $this->get_userid()); + } + + $transaction->allow_commit(); + } + + /** + * Replace a question in an attempt with a new attempt at the same question. + * + * Well, for randomised questions, it won't be the same question, it will be + * a different randomised selection. + * + * @param int $slot the question to restart. + * @param int $timestamp the timestamp to record for this action. + */ + public function process_redo_question($slot, $timestamp) { + global $DB; + + if (!$this->can_question_be_redone_now($slot)) { + throw new coding_exception('Attempt to restart the question in slot ' . $slot . + ' when it is not in a state to be restarted.'); + } + + $qubaids = new \mod_quiz\question\qubaids_for_users_attempts( + $this->get_quizid(), $this->get_userid(), 'all', true); + + $transaction = $DB->start_delegated_transaction(); + + // Add the question to the usage. It is important we do this before we choose a variant. + $newquestionid = qbank_helper::choose_question_for_redo($this->get_quizid(), + $this->get_quizobj()->get_context(), $this->slots[$slot]->id, $qubaids); + $newquestion = question_bank::load_question($newquestionid, $this->get_quiz()->shuffleanswers); + $newslot = $this->quba->add_question_in_place_of_other($slot, $newquestion); + + // Choose the variant. + if ($newquestion->get_num_variants() == 1) { + $variant = 1; + } else { + $variantstrategy = new core_question\engine\variants\least_used_strategy( + $this->quba, $qubaids); + $variant = $variantstrategy->choose_variant($newquestion->get_num_variants(), + $newquestion->get_variants_selection_seed()); + } + + // Start the question. + $this->quba->start_question($slot, $variant); + $this->quba->set_max_mark($newslot, 0); + $this->quba->set_question_attempt_metadata($newslot, 'originalslot', $slot); + question_engine::save_questions_usage_by_activity($this->quba); + $this->fire_attempt_question_restarted_event($slot, $newquestion->id); + + $transaction->allow_commit(); + } + + /** + * Process all the autosaved data that was part of the current request. + * + * @param int $timestamp the timestamp that should be stored as the modified. + * time in the database for these actions. If null, will use the current time. + */ + public function process_auto_save($timestamp) { + global $DB; + + $transaction = $DB->start_delegated_transaction(); + + $this->quba->process_all_autosaves($timestamp); + question_engine::save_questions_usage_by_activity($this->quba); + $this->fire_attempt_autosaved_event(); + + $transaction->allow_commit(); + } + + /** + * Update the flagged state for all question_attempts in this usage, if their + * flagged state was changed in the request. + */ + public function save_question_flags() { + global $DB; + + $transaction = $DB->start_delegated_transaction(); + $this->quba->update_question_flags(); + question_engine::save_questions_usage_by_activity($this->quba); + $transaction->allow_commit(); + } + + /** + * Submit the attempt. + * + * The separate $timefinish argument should be used when the quiz attempt + * is being processed asynchronously (for example when cron is submitting + * attempts where the time has expired). + * + * @param int $timestamp the time to record as last modified time. + * @param bool $processsubmitted if true, and question responses in the current + * POST request are stored to be graded, before the attempt is finished. + * @param ?int $timefinish if set, use this as the finish time for the attempt. + * (otherwise use $timestamp as the finish time as well). + * @param bool $studentisonline is the student currently interacting with Moodle? + */ + public function process_finish($timestamp, $processsubmitted, $timefinish = null, $studentisonline = false) { + global $DB; + + $transaction = $DB->start_delegated_transaction(); + + if ($processsubmitted) { + $this->quba->process_all_actions($timestamp); + } + $this->quba->finish_all_questions($timestamp); + + question_engine::save_questions_usage_by_activity($this->quba); + + $this->attempt->timemodified = $timestamp; + $this->attempt->timefinish = $timefinish ?? $timestamp; + $this->attempt->sumgrades = $this->quba->get_total_mark(); + $this->attempt->state = self::FINISHED; + $this->attempt->timecheckstate = null; + $this->attempt->gradednotificationsenttime = null; + + if (!$this->requires_manual_grading() || + !has_capability('mod/quiz:emailnotifyattemptgraded', $this->get_quizobj()->get_context(), + $this->get_userid())) { + $this->attempt->gradednotificationsenttime = $this->attempt->timefinish; + } + + $DB->update_record('quiz_attempts', $this->attempt); + + if (!$this->is_preview()) { + quiz_save_best_grade($this->get_quiz(), $this->attempt->userid); + + // Trigger event. + $this->fire_state_transition_event('\mod_quiz\event\attempt_submitted', $timestamp, $studentisonline); + + // Tell any access rules that care that the attempt is over. + $this->get_access_manager($timestamp)->current_attempt_finished(); + } + + $transaction->allow_commit(); + } + + /** + * Update this attempt timecheckstate if necessary. + * + * @param int|null $time the timestamp to set. + */ + public function update_timecheckstate($time) { + global $DB; + if ($this->attempt->timecheckstate !== $time) { + $this->attempt->timecheckstate = $time; + $DB->set_field('quiz_attempts', 'timecheckstate', $time, array('id' => $this->attempt->id)); + } + } + + /** + * Mark this attempt as now overdue. + * + * @param int $timestamp the time to deem as now. + * @param bool $studentisonline is the student currently interacting with Moodle? + */ + public function process_going_overdue($timestamp, $studentisonline) { + global $DB; + + $transaction = $DB->start_delegated_transaction(); + $this->attempt->timemodified = $timestamp; + $this->attempt->state = self::OVERDUE; + // If we knew the attempt close time, we could compute when the graceperiod ends. + // Instead we'll just fix it up through cron. + $this->attempt->timecheckstate = $timestamp; + $DB->update_record('quiz_attempts', $this->attempt); + + $this->fire_state_transition_event('\mod_quiz\event\attempt_becameoverdue', $timestamp, $studentisonline); + + $transaction->allow_commit(); + + quiz_send_overdue_message($this); + } + + /** + * Mark this attempt as abandoned. + * + * @param int $timestamp the time to deem as now. + * @param bool $studentisonline is the student currently interacting with Moodle? + */ + public function process_abandon($timestamp, $studentisonline) { + global $DB; + + $transaction = $DB->start_delegated_transaction(); + $this->attempt->timemodified = $timestamp; + $this->attempt->state = self::ABANDONED; + $this->attempt->timecheckstate = null; + $DB->update_record('quiz_attempts', $this->attempt); + + $this->fire_state_transition_event('\mod_quiz\event\attempt_abandoned', $timestamp, $studentisonline); + + $transaction->allow_commit(); + } + + /** + * Fire a state transition event. + * + * @param string $eventclass the event class name. + * @param int $timestamp the timestamp to include in the event. + * @param bool $studentisonline is the student currently interacting with Moodle? + */ + protected function fire_state_transition_event($eventclass, $timestamp, $studentisonline) { + global $USER; + $quizrecord = $this->get_quiz(); + $params = array( + 'context' => $this->get_quizobj()->get_context(), + 'courseid' => $this->get_courseid(), + 'objectid' => $this->attempt->id, + 'relateduserid' => $this->attempt->userid, + 'other' => array( + 'submitterid' => CLI_SCRIPT ? null : $USER->id, + 'quizid' => $quizrecord->id, + 'studentisonline' => $studentisonline + ) + ); + $event = $eventclass::create($params); + $event->add_record_snapshot('quiz', $this->get_quiz()); + $event->add_record_snapshot('quiz_attempts', $this->get_attempt()); + $event->trigger(); + } + + // Private methods ========================================================= + + /** + * Get a URL for a particular question on a particular page of the quiz. + * Used by {@link attempt_url()} and {@link review_url()}. + * + * @param string $script. Used in the URL like /mod/quiz/$script.php. + * @param int $slot identifies the specific question on the page to jump to. + * 0 to just use the $page parameter. + * @param int $page -1 to look up the page number from the slot, otherwise + * the page number to go to. + * @param bool|null $showall if true, return a URL with showall=1, and not page number. + * if null, then an intelligent default will be chosen. + * @param int $thispage the page we are currently on. Links to questions on this + * page will just be a fragment #q123. -1 to disable this. + * @return moodle_url The requested URL. + */ + protected function page_and_question_url($script, $slot, $page, $showall, $thispage) { + + $defaultshowall = $this->get_default_show_all($script); + if ($showall === null && ($page == 0 || $page == -1)) { + $showall = $defaultshowall; + } + + // Fix up $page. + if ($page == -1) { + if ($slot !== null && !$showall) { + $page = $this->get_question_page($slot); + } else { + $page = 0; + } + } + + if ($showall) { + $page = 0; + } + + // Add a fragment to scroll down to the question. + $fragment = ''; + if ($slot !== null) { + if ($slot == reset($this->pagelayout[$page]) && $thispage != $page) { + // Changing the page, go to top. + $fragment = '#'; + } else { + // Link to the question container. + $qa = $this->get_question_attempt($slot); + $fragment = '#' . $qa->get_outer_question_div_unique_id(); + } + } + + // Work out the correct start to the URL. + if ($thispage == $page) { + return new moodle_url($fragment); + + } else { + $url = new moodle_url('/mod/quiz/' . $script . '.php' . $fragment, + array('attempt' => $this->attempt->id, 'cmid' => $this->get_cmid())); + if ($page == 0 && $showall != $defaultshowall) { + $url->param('showall', (int) $showall); + } else if ($page > 0) { + $url->param('page', $page); + } + return $url; + } + } + + /** + * Process responses during an attempt at a quiz. + * + * @param int $timenow time when the processing started. + * @param bool $finishattempt whether to finish the attempt or not. + * @param bool $timeup true if form was submitted by timer. + * @param int $thispage current page number. + * @return string the attempt state once the data has been processed. + * @since Moodle 3.1 + */ + public function process_attempt($timenow, $finishattempt, $timeup, $thispage) { + global $DB; + + $transaction = $DB->start_delegated_transaction(); + + // Get key times. + $accessmanager = $this->get_access_manager($timenow); + $timeclose = $accessmanager->get_end_time($this->get_attempt()); + $graceperiodmin = get_config('quiz', 'graceperiodmin'); + + // Don't enforce timeclose for previews. + if ($this->is_preview()) { + $timeclose = false; + } + + // Check where we are in relation to the end time, if there is one. + $toolate = false; + if ($timeclose !== false) { + if ($timenow > $timeclose - QUIZ_MIN_TIME_TO_CONTINUE) { + // If there is only a very small amount of time left, there is no point trying + // to show the student another page of the quiz. Just finish now. + $timeup = true; + if ($timenow > $timeclose + $graceperiodmin) { + $toolate = true; + } + } else { + // If time is not close to expiring, then ignore the client-side timer's opinion + // about whether time has expired. This can happen if the time limit has changed + // since the student's previous interaction. + $timeup = false; + } + } + + // If time is running out, trigger the appropriate action. + $becomingoverdue = false; + $becomingabandoned = false; + if ($timeup) { + if ($this->get_quiz()->overduehandling === 'graceperiod') { + if ($timenow > $timeclose + $this->get_quiz()->graceperiod + $graceperiodmin) { + // Grace period has run out. + $finishattempt = true; + $becomingabandoned = true; + } else { + $becomingoverdue = true; + } + } else { + $finishattempt = true; + } + } + + if (!$finishattempt) { + // Just process the responses for this page and go to the next page. + if (!$toolate) { + try { + $this->process_submitted_actions($timenow, $becomingoverdue); + $this->fire_attempt_updated_event(); + } catch (question_out_of_sequence_exception $e) { + throw new moodle_exception('submissionoutofsequencefriendlymessage', 'question', + $this->attempt_url(null, $thispage)); + + } catch (Exception $e) { + // This sucks, if we display our own custom error message, there is no way + // to display the original stack trace. + $debuginfo = ''; + if (!empty($e->debuginfo)) { + $debuginfo = $e->debuginfo; + } + throw new moodle_exception('errorprocessingresponses', 'question', + $this->attempt_url(null, $thispage), $e->getMessage(), $debuginfo); + } + + if (!$becomingoverdue) { + foreach ($this->get_slots() as $slot) { + if (optional_param('redoslot' . $slot, false, PARAM_BOOL)) { + $this->process_redo_question($slot, $timenow); + } + } + } + + } else { + // The student is too late. + $this->process_going_overdue($timenow, true); + } + + $transaction->allow_commit(); + + return $becomingoverdue ? self::OVERDUE : self::IN_PROGRESS; + } + + // Update the quiz attempt record. + try { + if ($becomingabandoned) { + $this->process_abandon($timenow, true); + } else { + if (!$toolate || $this->get_quiz()->overduehandling === 'graceperiod') { + // Normally, we record the accurate finish time when the student is online. + $finishtime = $timenow; + } else { + // But, if there is no grade period, and the final responses were too + // late to be processed, record the close time, to reduce confusion. + $finishtime = $timeclose; + } + $this->process_finish($timenow, !$toolate, $finishtime, true); + } + + } catch (question_out_of_sequence_exception $e) { + throw new moodle_exception('submissionoutofsequencefriendlymessage', 'question', + $this->attempt_url(null, $thispage)); + + } catch (Exception $e) { + // This sucks, if we display our own custom error message, there is no way + // to display the original stack trace. + $debuginfo = ''; + if (!empty($e->debuginfo)) { + $debuginfo = $e->debuginfo; + } + throw new moodle_exception('errorprocessingresponses', 'question', + $this->attempt_url(null, $thispage), $e->getMessage(), $debuginfo); + } + + // Send the user to the review page. + $transaction->allow_commit(); + + return $becomingabandoned ? self::ABANDONED : self::FINISHED; + } + + /** + * Check a page read access to see if is an out of sequence access. + * + * If allownext is set then we also check whether access to the page + * after the current one should be permitted. + * + * @param int $page page number. + * @param bool $allownext in case of a sequential navigation, can we go to next page ? + * @return boolean false is an out of sequence access, true otherwise. + * @since Moodle 3.1 + */ + public function check_page_access(int $page, bool $allownext = true): bool { + if ($this->get_navigation_method() != QUIZ_NAVMETHOD_SEQ) { + return true; + } + // Sequential access: allow access to the summary, current page or next page. + // Or if the user review his/her attempt, see MDLQA-1523. + return $page == -1 + || $page == $this->get_currentpage() + || $allownext && ($page == $this->get_currentpage() + 1); + } + + /** + * Update attempt page. + * + * @param int $page page number. + * @return boolean true if everything was ok, false otherwise (out of sequence access). + * @since Moodle 3.1 + */ + public function set_currentpage($page) { + global $DB; + + if ($this->check_page_access($page)) { + $DB->set_field('quiz_attempts', 'currentpage', $page, array('id' => $this->get_attemptid())); + return true; + } + return false; + } + + /** + * Trigger the attempt_viewed event. + * + * @since Moodle 3.1 + */ + public function fire_attempt_viewed_event() { + $params = array( + 'objectid' => $this->get_attemptid(), + 'relateduserid' => $this->get_userid(), + 'courseid' => $this->get_courseid(), + 'context' => context_module::instance($this->get_cmid()), + 'other' => array( + 'quizid' => $this->get_quizid(), + 'page' => $this->get_currentpage() + ) + ); + $event = \mod_quiz\event\attempt_viewed::create($params); + $event->add_record_snapshot('quiz_attempts', $this->get_attempt()); + $event->trigger(); + } + + /** + * Trigger the attempt_updated event. + * + * @return void + */ + public function fire_attempt_updated_event(): void { + $params = [ + 'objectid' => $this->get_attemptid(), + 'relateduserid' => $this->get_userid(), + 'courseid' => $this->get_courseid(), + 'context' => context_module::instance($this->get_cmid()), + 'other' => [ + 'quizid' => $this->get_quizid(), + 'page' => $this->get_currentpage() + ] + ]; + $event = \mod_quiz\event\attempt_updated::create($params); + $event->add_record_snapshot('quiz_attempts', $this->get_attempt()); + $event->trigger(); + } + + /** + * Trigger the attempt_autosaved event. + * + * @return void + */ + public function fire_attempt_autosaved_event(): void { + $params = [ + 'objectid' => $this->get_attemptid(), + 'relateduserid' => $this->get_userid(), + 'courseid' => $this->get_courseid(), + 'context' => context_module::instance($this->get_cmid()), + 'other' => [ + 'quizid' => $this->get_quizid(), + 'page' => $this->get_currentpage() + ] + ]; + $event = \mod_quiz\event\attempt_autosaved::create($params); + $event->add_record_snapshot('quiz_attempts', $this->get_attempt()); + $event->trigger(); + } + + /** + * Trigger the attempt_question_restarted event. + * + * @param int $slot Slot number + * @param int $newquestionid New question id. + * @return void + */ + public function fire_attempt_question_restarted_event(int $slot, int $newquestionid): void { + $params = [ + 'objectid' => $this->get_attemptid(), + 'relateduserid' => $this->get_userid(), + 'courseid' => $this->get_courseid(), + 'context' => context_module::instance($this->get_cmid()), + 'other' => [ + 'quizid' => $this->get_quizid(), + 'page' => $this->get_currentpage(), + 'slot' => $slot, + 'newquestionid' => $newquestionid + ] + ]; + $event = \mod_quiz\event\attempt_question_restarted::create($params); + $event->add_record_snapshot('quiz_attempts', $this->get_attempt()); + $event->trigger(); + } + + /** + * Trigger the attempt_summary_viewed event. + * + * @since Moodle 3.1 + */ + public function fire_attempt_summary_viewed_event() { + + $params = array( + 'objectid' => $this->get_attemptid(), + 'relateduserid' => $this->get_userid(), + 'courseid' => $this->get_courseid(), + 'context' => context_module::instance($this->get_cmid()), + 'other' => array( + 'quizid' => $this->get_quizid() + ) + ); + $event = \mod_quiz\event\attempt_summary_viewed::create($params); + $event->add_record_snapshot('quiz_attempts', $this->get_attempt()); + $event->trigger(); + } + + /** + * Trigger the attempt_reviewed event. + * + * @since Moodle 3.1 + */ + public function fire_attempt_reviewed_event() { + + $params = array( + 'objectid' => $this->get_attemptid(), + 'relateduserid' => $this->get_userid(), + 'courseid' => $this->get_courseid(), + 'context' => context_module::instance($this->get_cmid()), + 'other' => array( + 'quizid' => $this->get_quizid() + ) + ); + $event = \mod_quiz\event\attempt_reviewed::create($params); + $event->add_record_snapshot('quiz_attempts', $this->get_attempt()); + $event->trigger(); + } + + /** + * Trigger the attempt manual grading completed event. + */ + public function fire_attempt_manual_grading_completed_event() { + $params = [ + 'objectid' => $this->get_attemptid(), + 'relateduserid' => $this->get_userid(), + 'courseid' => $this->get_courseid(), + 'context' => context_module::instance($this->get_cmid()), + 'other' => [ + 'quizid' => $this->get_quizid() + ] + ]; + + $event = \mod_quiz\event\attempt_manual_grading_completed::create($params); + $event->add_record_snapshot('quiz_attempts', $this->get_attempt()); + $event->trigger(); + } + + /** + * Update the timemodifiedoffline attempt field. + * + * This function should be used only when web services are being used. + * + * @param int $time time stamp. + * @return boolean false if the field is not updated because web services aren't being used. + * @since Moodle 3.2 + */ + public function set_offline_modified_time($time) { + // Update the timemodifiedoffline field only if web services are being used. + if (WS_SERVER) { + $this->attempt->timemodifiedoffline = $time; + return true; + } + return false; + } + + /** + * Get the total number of unanswered questions in the attempt. + * + * @return int + */ + public function get_number_of_unanswered_questions(): int { + $totalunanswered = 0; + foreach ($this->get_slots() as $slot) { + $questionstate = $this->get_question_state($slot); + if ($questionstate == question_state::$todo || $questionstate == question_state::$invalid) { + $totalunanswered++; + } + } + return $totalunanswered; + } +} diff --git a/mod/quiz/classes/task/quiz_notify_attempt_manual_grading_completed.php b/mod/quiz/classes/task/quiz_notify_attempt_manual_grading_completed.php index 445cdd102b9..62531792b45 100644 --- a/mod/quiz/classes/task/quiz_notify_attempt_manual_grading_completed.php +++ b/mod/quiz/classes/task/quiz_notify_attempt_manual_grading_completed.php @@ -20,10 +20,10 @@ defined('MOODLE_INTERNAL') || die(); use context_course; use core_user; +use mod_quiz\quiz_attempt; use moodle_recordset; use question_display_options; use mod_quiz\question\display_options; -use quiz_attempt; require_once($CFG->dirroot . '/mod/quiz/locallib.php'); diff --git a/mod/quiz/classes/task/update_overdue_attempts.php b/mod/quiz/classes/task/update_overdue_attempts.php index 8241243466d..e83c08f344e 100644 --- a/mod/quiz/classes/task/update_overdue_attempts.php +++ b/mod/quiz/classes/task/update_overdue_attempts.php @@ -24,9 +24,9 @@ */ namespace mod_quiz\task; +use mod_quiz\quiz_attempt; use moodle_exception; use moodle_recordset; -use quiz_attempt; defined('MOODLE_INTERNAL') || die(); diff --git a/mod/quiz/db/renamedclasses.php b/mod/quiz/db/renamedclasses.php index 7fac769cd1b..059eadccb9f 100644 --- a/mod/quiz/db/renamedclasses.php +++ b/mod/quiz/db/renamedclasses.php @@ -64,4 +64,5 @@ $renamedclasses = [ 'quiz_nav_panel_base' => 'mod_quiz\output\navigation_panel_base', 'quiz_attempt_nav_panel' => 'mod_quiz\output\navigation_panel_attempt', 'quiz_review_nav_panel' => 'mod_quiz\output\navigation_panel_review', + 'quiz_attempt' => 'mod_quiz\quiz_attempt', ]; diff --git a/mod/quiz/grade.php b/mod/quiz/grade.php index 970aa6f5543..c7889735261 100644 --- a/mod/quiz/grade.php +++ b/mod/quiz/grade.php @@ -24,6 +24,7 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +use mod_quiz\quiz_attempt; require_once(__DIR__ . '/../../config.php'); require_once($CFG->dirroot . '/mod/quiz/locallib.php'); diff --git a/mod/quiz/lib.php b/mod/quiz/lib.php index e90cd1fed3d..dc1c46fdb3a 100644 --- a/mod/quiz/lib.php +++ b/mod/quiz/lib.php @@ -35,6 +35,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\quiz_attempt; require_once($CFG->dirroot . '/calendar/lib.php'); require_once($CFG->dirroot . '/mod/quiz/attemptlib.php'); @@ -583,12 +584,7 @@ function quiz_user_complete($course, $user, $mod, $quiz) { * array if there are none. */ function quiz_get_user_attempts($quizids, $userid, $status = 'finished', $includepreviews = false) { - global $DB, $CFG; - // TODO MDL-33071 it is very annoying to have to included all of locallib.php - // just to get the quiz_attempt::FINISHED constants, but I will try to sort - // that out properly for Moodle 2.4. For now, I will just do a quick fix for - // MDL-33048. - require_once($CFG->dirroot . '/mod/quiz/locallib.php'); + global $DB; $params = array(); switch ($status) { diff --git a/mod/quiz/locallib.php b/mod/quiz/locallib.php index ec9adb793b5..a31e243b389 100644 --- a/mod/quiz/locallib.php +++ b/mod/quiz/locallib.php @@ -39,6 +39,7 @@ require_once($CFG->libdir . '/questionlib.php'); use mod_quiz\access_manager; use mod_quiz\question\bank\qbank_helper; use mod_quiz\question\display_options; +use mod_quiz\quiz_attempt; /** * @var int We show the countdown timer if there is less than this amount of time left before the @@ -80,7 +81,7 @@ define('QUIZ_SHOWIMAGE_LARGE', 2); * * @param object $quizobj the quiz object to create an attempt for. * @param int $attemptnumber the sequence number for the attempt. - * @param stdClass|null $lastattempt the previous attempt by this user, if any. Only needed + * @param stdClass|false $lastattempt the previous attempt by this user, if any. Only needed * if $attemptnumber > 1 and $quiz->attemptonlast is true. * @param int $timenow the time the attempt was started at. * @param bool $ispreview whether this new attempt is a preview. @@ -1353,7 +1354,7 @@ function quiz_questions_per_page_options() { /** * Get the human-readable name for a quiz attempt state. - * @param string $state one of the state constants like {@link quiz_attempt::IN_PROGRESS}. + * @param string $state one of the state constants like {@see quiz_attempt::IN_PROGRESS}. * @return string The lang string to describe that state. */ function quiz_attempt_state_name($state) { diff --git a/mod/quiz/processattempt.php b/mod/quiz/processattempt.php index 8e0d594550b..4a871af17ce 100644 --- a/mod/quiz/processattempt.php +++ b/mod/quiz/processattempt.php @@ -28,6 +28,8 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +use mod_quiz\quiz_attempt; + require_once(__DIR__ . '/../../config.php'); require_once($CFG->dirroot . '/mod/quiz/locallib.php'); diff --git a/mod/quiz/report/grading/report.php b/mod/quiz/report/grading/report.php index c43532be94d..9192a8179e5 100644 --- a/mod/quiz/report/grading/report.php +++ b/mod/quiz/report/grading/report.php @@ -15,6 +15,7 @@ // along with Moodle. If not, see . use mod_quiz\local\reports\report_base; +use mod_quiz\quiz_attempt; defined('MOODLE_INTERNAL') || die(); diff --git a/mod/quiz/report/overview/overview_table.php b/mod/quiz/report/overview/overview_table.php index 681ee0f4ce4..f1ab1da042b 100644 --- a/mod/quiz/report/overview/overview_table.php +++ b/mod/quiz/report/overview/overview_table.php @@ -15,6 +15,7 @@ // along with Moodle. If not, see . use mod_quiz\local\reports\attempts_report_table; +use mod_quiz\quiz_attempt; /** * This is a table subclass for displaying the quiz grades report. diff --git a/mod/quiz/report/overview/report.php b/mod/quiz/report/overview/report.php index 4469aa77c4b..83c41a4dda6 100644 --- a/mod/quiz/report/overview/report.php +++ b/mod/quiz/report/overview/report.php @@ -24,6 +24,7 @@ use mod_quiz\local\reports\attempts_report; use mod_quiz\question\bank\qbank_helper; +use mod_quiz\quiz_attempt; defined('MOODLE_INTERNAL') || die(); diff --git a/mod/quiz/report/overview/tests/report_test.php b/mod/quiz/report/overview/tests/report_test.php index 309a78d430a..6d4fc89384c 100644 --- a/mod/quiz/report/overview/tests/report_test.php +++ b/mod/quiz/report/overview/tests/report_test.php @@ -18,9 +18,9 @@ namespace quiz_overview; use core_question\local\bank\question_version_status; use mod_quiz\external\submit_question_version; +use mod_quiz\quiz_attempt; use question_engine; use quiz; -use quiz_attempt; use mod_quiz\local\reports\attempts_report; use quiz_overview_options; use quiz_overview_report; diff --git a/mod/quiz/report/responses/first_or_all_responses_table.php b/mod/quiz/report/responses/first_or_all_responses_table.php index 0f96e7b0826..b9787cf8af3 100644 --- a/mod/quiz/report/responses/first_or_all_responses_table.php +++ b/mod/quiz/report/responses/first_or_all_responses_table.php @@ -23,6 +23,8 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +use mod_quiz\quiz_attempt; + defined('MOODLE_INTERNAL') || die(); /** diff --git a/mod/quiz/report/responses/last_responses_table.php b/mod/quiz/report/responses/last_responses_table.php index 92eb5ba828e..033de2a9311 100644 --- a/mod/quiz/report/responses/last_responses_table.php +++ b/mod/quiz/report/responses/last_responses_table.php @@ -15,7 +15,7 @@ // along with Moodle. If not, see . use mod_quiz\local\reports\attempts_report_table; - +use mod_quiz\quiz_attempt; /** * This is a table subclass for displaying the quiz responses report. diff --git a/mod/quiz/report/responses/tests/responses_from_steps_walkthrough_test.php b/mod/quiz/report/responses/tests/responses_from_steps_walkthrough_test.php index cb16d9ad759..909c706241c 100644 --- a/mod/quiz/report/responses/tests/responses_from_steps_walkthrough_test.php +++ b/mod/quiz/report/responses/tests/responses_from_steps_walkthrough_test.php @@ -16,8 +16,8 @@ namespace quiz_responses; +use mod_quiz\quiz_attempt; use question_bank; -use quiz_attempt; defined('MOODLE_INTERNAL') || die(); diff --git a/mod/quiz/report/statistics/classes/task/recalculate.php b/mod/quiz/report/statistics/classes/task/recalculate.php index 6d5906be865..425925f0eba 100644 --- a/mod/quiz/report/statistics/classes/task/recalculate.php +++ b/mod/quiz/report/statistics/classes/task/recalculate.php @@ -16,7 +16,7 @@ namespace quiz_statistics\task; -use quiz_attempt; +use mod_quiz\quiz_attempt; use quiz; use quiz_statistics_report; diff --git a/mod/quiz/report/statistics/statisticslib.php b/mod/quiz/report/statistics/statisticslib.php index 832e14be64b..383044ab643 100644 --- a/mod/quiz/report/statistics/statisticslib.php +++ b/mod/quiz/report/statistics/statisticslib.php @@ -23,6 +23,8 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +use mod_quiz\quiz_attempt; + defined('MOODLE_INTERNAL') || die; /** diff --git a/mod/quiz/review.php b/mod/quiz/review.php index ddb9d25fe00..0c907c1f421 100644 --- a/mod/quiz/review.php +++ b/mod/quiz/review.php @@ -27,6 +27,7 @@ use mod_quiz\output\navigation_panel_review; use mod_quiz\output\renderer; +use mod_quiz\quiz_attempt; require_once(__DIR__ . '/../../config.php'); require_once($CFG->dirroot . '/mod/quiz/locallib.php'); diff --git a/mod/quiz/startattempt.php b/mod/quiz/startattempt.php index b71771ab0c8..f1dea6bce10 100644 --- a/mod/quiz/startattempt.php +++ b/mod/quiz/startattempt.php @@ -26,6 +26,8 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +use mod_quiz\quiz_attempt; + require_once(__DIR__ . '/../../config.php'); require_once($CFG->dirroot . '/mod/quiz/locallib.php'); diff --git a/mod/quiz/tests/attempt_test.php b/mod/quiz/tests/attempt_test.php index 0b78eae2cba..8faefc11d4a 100644 --- a/mod/quiz/tests/attempt_test.php +++ b/mod/quiz/tests/attempt_test.php @@ -18,7 +18,6 @@ namespace mod_quiz; use question_engine; use quiz; -use quiz_attempt; defined('MOODLE_INTERNAL') || die(); diff --git a/mod/quiz/tests/attempt_walkthrough_from_csv_test.php b/mod/quiz/tests/attempt_walkthrough_from_csv_test.php index 788b3f577f9..4798e9036da 100644 --- a/mod/quiz/tests/attempt_walkthrough_from_csv_test.php +++ b/mod/quiz/tests/attempt_walkthrough_from_csv_test.php @@ -18,7 +18,6 @@ namespace mod_quiz; use question_engine; use quiz; -use quiz_attempt; defined('MOODLE_INTERNAL') || die(); diff --git a/mod/quiz/tests/attempt_walkthrough_test.php b/mod/quiz/tests/attempt_walkthrough_test.php index cbeb26db77b..8d3a80d753a 100644 --- a/mod/quiz/tests/attempt_walkthrough_test.php +++ b/mod/quiz/tests/attempt_walkthrough_test.php @@ -19,7 +19,6 @@ namespace mod_quiz; use question_bank; use question_engine; use quiz; -use quiz_attempt; defined('MOODLE_INTERNAL') || die(); diff --git a/mod/quiz/tests/behat/behat_mod_quiz.php b/mod/quiz/tests/behat/behat_mod_quiz.php index cc0b35eea7f..3f573c32712 100644 --- a/mod/quiz/tests/behat/behat_mod_quiz.php +++ b/mod/quiz/tests/behat/behat_mod_quiz.php @@ -31,6 +31,7 @@ require_once(__DIR__ . '/../../../../question/tests/behat/behat_question_base.ph use Behat\Gherkin\Node\TableNode as TableNode; use Behat\Mink\Exception\ExpectationException as ExpectationException; +use mod_quiz\quiz_attempt; /** * Steps definitions related to mod_quiz. diff --git a/mod/quiz/tests/custom_completion_test.php b/mod/quiz/tests/custom_completion_test.php index 0420ffad606..7ba00e1ddaa 100644 --- a/mod/quiz/tests/custom_completion_test.php +++ b/mod/quiz/tests/custom_completion_test.php @@ -25,7 +25,6 @@ use grade_item; use mod_quiz\completion\custom_completion; use question_engine; use quiz; -use quiz_attempt; defined('MOODLE_INTERNAL') || die(); diff --git a/mod/quiz/tests/event/events_test.php b/mod/quiz/tests/event/events_test.php index 5229e87b96b..78f6fe40e6c 100644 --- a/mod/quiz/tests/event/events_test.php +++ b/mod/quiz/tests/event/events_test.php @@ -25,8 +25,8 @@ namespace mod_quiz\event; +use mod_quiz\quiz_attempt; use quiz; -use quiz_attempt; use context_module; defined('MOODLE_INTERNAL') || die(); diff --git a/mod/quiz/tests/external/external_test.php b/mod/quiz/tests/external/external_test.php index 58b085df251..66f32c13e3b 100644 --- a/mod/quiz/tests/external/external_test.php +++ b/mod/quiz/tests/external/external_test.php @@ -27,11 +27,11 @@ namespace mod_quiz\external; use externallib_advanced_testcase; +use mod_quiz\quiz_attempt; use mod_quiz_external; use mod_quiz\question\display_options; use moodle_exception; use quiz; -use quiz_attempt; defined('MOODLE_INTERNAL') || die(); diff --git a/mod/quiz/tests/generator/lib.php b/mod/quiz/tests/generator/lib.php index cccdaf2f9df..9e728ce1f38 100644 --- a/mod/quiz/tests/generator/lib.php +++ b/mod/quiz/tests/generator/lib.php @@ -14,6 +14,8 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +use mod_quiz\quiz_attempt; + defined('MOODLE_INTERNAL') || die(); /** diff --git a/mod/quiz/tests/lib_test.php b/mod/quiz/tests/lib_test.php index a9ed6e1d0b1..5515b4a338e 100644 --- a/mod/quiz/tests/lib_test.php +++ b/mod/quiz/tests/lib_test.php @@ -24,10 +24,7 @@ */ namespace mod_quiz; -use mod_quiz\external\submit_question_version; -use mod_quiz\question\bank\qbank_helper; use quiz; -use quiz_attempt; defined('MOODLE_INTERNAL') || die(); diff --git a/mod/quiz/tests/locallib_test.php b/mod/quiz/tests/locallib_test.php index 79a93f7a5f4..b73248fc984 100644 --- a/mod/quiz/tests/locallib_test.php +++ b/mod/quiz/tests/locallib_test.php @@ -25,7 +25,6 @@ namespace mod_quiz; use mod_quiz\output\renderer; -use quiz_attempt; use mod_quiz\question\display_options; defined('MOODLE_INTERNAL') || die(); diff --git a/mod/quiz/tests/privacy/provider_test.php b/mod/quiz/tests/privacy/provider_test.php index a7292e7170e..88fa323d2f3 100644 --- a/mod/quiz/tests/privacy/provider_test.php +++ b/mod/quiz/tests/privacy/provider_test.php @@ -28,6 +28,7 @@ use core_privacy\local\request\deletion_criteria; use core_privacy\local\request\writer; use mod_quiz\privacy\provider; use mod_quiz\privacy\helper; +use mod_quiz\quiz_attempt; defined('MOODLE_INTERNAL') || die(); @@ -188,7 +189,7 @@ class provider_test extends \core_privacy\tests\provider_testcase { $attempt = $attemptobj->get_attempt(); $this->assertTrue(isset($attemptdata->state)); - $this->assertEquals(\quiz_attempt::state_name($attemptobj->get_state()), $attemptdata->state); + $this->assertEquals(quiz_attempt::state_name($attemptobj->get_state()), $attemptdata->state); $this->assertTrue(isset($attemptdata->timestart)); $this->assertTrue(isset($attemptdata->timefinish)); $this->assertTrue(isset($attemptdata->timemodified)); @@ -212,7 +213,7 @@ class provider_test extends \core_privacy\tests\provider_testcase { $this->setUser(); provider::delete_data_for_user($approvedcontextlist); $this->expectException(\dml_missing_record_exception::class); - \quiz_attempt::create($attemptobj->get_quizid()); + quiz_attempt::create($attemptobj->get_quizid()); } /** @@ -258,7 +259,7 @@ class provider_test extends \core_privacy\tests\provider_testcase { quiz_attempt_save_started($quizobj, $quba, $attempt); // Answer the questions. - $attemptobj = \quiz_attempt::create($attempt->id); + $attemptobj = quiz_attempt::create($attempt->id); $tosubmit = [ 1 => ['answer' => 'frog'], @@ -268,7 +269,7 @@ class provider_test extends \core_privacy\tests\provider_testcase { $attemptobj->process_submitted_actions($starttime, false, $tosubmit); // Finish the attempt. - $attemptobj = \quiz_attempt::create($attempt->id); + $attemptobj = quiz_attempt::create($attempt->id); $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question()); $attemptobj->process_finish($starttime, false); @@ -392,7 +393,7 @@ class provider_test extends \core_privacy\tests\provider_testcase { * Create a test quiz for the specified course. * * @param \stdClass $course - * @return array + * @return \stdClass */ protected function create_test_quiz($course) { global $DB; @@ -441,7 +442,7 @@ class provider_test extends \core_privacy\tests\provider_testcase { quiz_attempt_save_started($quizobj, $quba, $attempt); // Answer the questions. - $attemptobj = \quiz_attempt::create($attempt->id); + $attemptobj = quiz_attempt::create($attempt->id); $tosubmit = [ 1 => ['answer' => 'frog'], @@ -451,7 +452,7 @@ class provider_test extends \core_privacy\tests\provider_testcase { $attemptobj->process_submitted_actions($starttime, false, $tosubmit); // Finish the attempt. - $attemptobj = \quiz_attempt::create($attempt->id); + $attemptobj = quiz_attempt::create($attempt->id); $attemptobj->process_finish($starttime, false); $this->setUser(); diff --git a/mod/quiz/tests/quiz_notify_attempt_manual_grading_completed_test.php b/mod/quiz/tests/quiz_notify_attempt_manual_grading_completed_test.php index 44c189dfb9e..11968dfd905 100644 --- a/mod/quiz/tests/quiz_notify_attempt_manual_grading_completed_test.php +++ b/mod/quiz/tests/quiz_notify_attempt_manual_grading_completed_test.php @@ -30,7 +30,6 @@ use context_module; use mod_quiz\task\quiz_notify_attempt_manual_grading_completed; use question_engine; use quiz; -use quiz_attempt; use stdClass; defined('MOODLE_INTERNAL') || die(); diff --git a/mod/quiz/tests/quiz_question_helper_test_trait.php b/mod/quiz/tests/quiz_question_helper_test_trait.php index 1d315d1d25c..b41722a57ec 100644 --- a/mod/quiz/tests/quiz_question_helper_test_trait.php +++ b/mod/quiz/tests/quiz_question_helper_test_trait.php @@ -13,6 +13,7 @@ // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +use mod_quiz\quiz_attempt; /** * Helper trait for quiz question unit tests. diff --git a/mod/quiz/tests/reportlib_test.php b/mod/quiz/tests/reportlib_test.php index 2c177f7def1..bcd41c1ae50 100644 --- a/mod/quiz/tests/reportlib_test.php +++ b/mod/quiz/tests/reportlib_test.php @@ -16,8 +16,6 @@ namespace mod_quiz; -use quiz_attempt; - defined('MOODLE_INTERNAL') || die(); global $CFG; diff --git a/mod/quiz/upgrade.txt b/mod/quiz/upgrade.txt index 3d868bb67b4..68671836b1f 100644 --- a/mod/quiz/upgrade.txt +++ b/mod/quiz/upgrade.txt @@ -48,6 +48,7 @@ This files describes API changes in the quiz code. - quiz_nav_panel_base => mod_quiz\output\navigation_panel_base - quiz_attempt_nav_panel => mod_quiz\output\navigation_panel_attempt - quiz_review_nav_panel => mod_quiz\output\navigation_panel_review + - quiz_attempt => mod_quiz\quiz_attempt * The following classes have been deprecated: - mod_quiz_overdue_attempt_updater - merged into mod_quiz\task\update_overdue_attempts diff --git a/mod/quiz/view.php b/mod/quiz/view.php index 67a27968e2d..eb1ac271364 100644 --- a/mod/quiz/view.php +++ b/mod/quiz/view.php @@ -26,6 +26,7 @@ use mod_quiz\access_manager; use mod_quiz\output\renderer; use mod_quiz\output\view_page; +use mod_quiz\quiz_attempt; require_once(__DIR__ . '/../../config.php'); require_once($CFG->libdir.'/gradelib.php'); diff --git a/question/bank/statistics/tests/helper_test.php b/question/bank/statistics/tests/helper_test.php index f80084e91d9..777cb79a4df 100644 --- a/question/bank/statistics/tests/helper_test.php +++ b/question/bank/statistics/tests/helper_test.php @@ -17,9 +17,9 @@ namespace qbank_statistics; use core_question\statistics\questions\all_calculated_for_qubaid_condition; +use mod_quiz\quiz_attempt; use quiz; use question_engine; -use quiz_attempt; /** * Tests for question statistics. diff --git a/question/bank/usage/tests/helper_test.php b/question/bank/usage/tests/helper_test.php index f337fc31caa..3f50a0a4966 100644 --- a/question/bank/usage/tests/helper_test.php +++ b/question/bank/usage/tests/helper_test.php @@ -16,6 +16,8 @@ namespace qbank_usage; +use mod_quiz\quiz_attempt; + /** * Helper test. * @@ -75,7 +77,7 @@ class helper_test extends \advanced_testcase { $attempt = quiz_create_attempt($quizobj, 1, false, $timenow, false, $user->id); quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow); quiz_attempt_save_started($quizobj, $quba, $attempt); - \quiz_attempt::create($attempt->id); + quiz_attempt::create($attempt->id); } /** diff --git a/question/bank/usage/tests/question_usage_test.php b/question/bank/usage/tests/question_usage_test.php index 3994c82d224..377246a6cb2 100644 --- a/question/bank/usage/tests/question_usage_test.php +++ b/question/bank/usage/tests/question_usage_test.php @@ -16,6 +16,8 @@ namespace qbank_usage; +use mod_quiz\quiz_attempt; + /** * Tests for the data of question usage from differnet areas like helper or usage table. * @@ -68,7 +70,7 @@ class question_usage_test extends \advanced_testcase { $attempt = quiz_create_attempt($quizobj, 1, false, $timenow, false, $user->id); quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow); quiz_attempt_save_started($quizobj, $quba, $attempt); - $attemptdata = \quiz_attempt::create($attempt->id); + $attemptdata = quiz_attempt::create($attempt->id); $this->setAdminUser(); $PAGE->set_url(new \moodle_url('/')); From ae6a1c0391f4b19b2073d626ecd51eb4fc787904 Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Mon, 19 Dec 2022 23:07:38 +0000 Subject: [PATCH 25/30] MDL-76614 quiz: clean up the quiz_attempt class --- mod/quiz/classes/quiz_attempt.php | 336 +++++++++++++++++++----------- mod/quiz/lib.php | 4 +- question/type/questionbase.php | 3 +- 3 files changed, 215 insertions(+), 128 deletions(-) diff --git a/mod/quiz/classes/quiz_attempt.php b/mod/quiz/classes/quiz_attempt.php index df2897499bf..b105e2c7f72 100644 --- a/mod/quiz/classes/quiz_attempt.php +++ b/mod/quiz/classes/quiz_attempt.php @@ -18,9 +18,10 @@ namespace mod_quiz; use action_link; use block_contents; +use cm_info; use coding_exception; use context_module; -use dml_exception; +use Exception; use html_writer; use mod_quiz\output\links_to_other_attempts; use mod_quiz\output\renderer; @@ -41,7 +42,7 @@ use quiz; use stdClass; /** - * This class represents one user's attept at a particular quiz. + * This class represents one user's attempt at a particular quiz. * * @package mod_quiz * @copyright 2008 Tim Hunt @@ -74,7 +75,7 @@ class quiz_attempt { * @var array of slot information. These objects contain ->slot (int), * ->requireprevious (bool), ->questionids (int) the original question for random questions, * ->firstinsection (bool), ->section (stdClass from $this->sections). - * This does not contain page - get that from {@link get_question_page()} - + * This does not contain page - get that from {@see get_question_page()} - * or maxmark - get that from $this->quba. */ protected $slots; @@ -94,14 +95,14 @@ class quiz_attempt { /** @var display_options cache for the appropriate review options. */ protected $reviewoptions = null; - // Constructor ============================================================= + // Constructor =============================================================. /** * Constructor assuming we already have the necessary data loaded. * - * @param object $attempt the row of the quiz_attempts table. - * @param object $quiz the quiz object for this attempt and user. - * @param object $cm the course_module object for this quiz. - * @param object $course the row from the course table for the course we belong to. + * @param stdClass $attempt the row of the quiz_attempts table. + * @param stdClass $quiz the quiz object for this attempt and user. + * @param stdClass|cm_info $cm the course_module object for this quiz. + * @param stdClass $course the row from the course table for the course we belong to. * @param bool $loadquestions (optional) if true, the default, load all the details * of the state of each question. Else just set up the basic details of the attempt. */ @@ -125,7 +126,7 @@ class quiz_attempt { $attempt = $DB->get_record('quiz_attempts', $conditions, '*', MUST_EXIST); $quiz = access_manager::load_quiz_and_settings($attempt->quiz); - $course = $DB->get_record('course', array('id' => $quiz->course), '*', MUST_EXIST); + $course = $DB->get_record('course', ['id' => $quiz->course], '*', MUST_EXIST); $cm = get_coursemodule_from_instance('quiz', $quiz->id, $course->id, false, MUST_EXIST); // Update quiz with override information. @@ -141,7 +142,7 @@ class quiz_attempt { * @return quiz_attempt the new quiz_attempt object */ public static function create($attemptid) { - return self::create_helper(array('id' => $attemptid)); + return self::create_helper(['id' => $attemptid]); } /** @@ -151,10 +152,12 @@ class quiz_attempt { * @return quiz_attempt the new quiz_attempt object */ public static function create_from_usage_id($usageid) { - return self::create_helper(array('uniqueid' => $usageid)); + return self::create_helper(['uniqueid' => $usageid]); } /** + * Get a human-readable name for one of the quiz attempt states. + * * @param string $state one of the state constants like IN_PROGRESS. * @return string the human-readable state name. */ @@ -163,7 +166,7 @@ class quiz_attempt { } /** - * This method can be called later if the object was constructed with $loadqusetions = false. + * This method can be called later if the object was constructed with $loadquestions = false. */ public function load_questions() { global $DB; @@ -185,8 +188,6 @@ class quiz_attempt { /** * Preload all attempt step users to show in Response history. - * - * @throws dml_exception */ public function preload_all_attempt_step_users(): void { $this->quba->preload_all_step_users(); @@ -209,10 +210,9 @@ class quiz_attempt { } /** - * Parse attempt->layout to populate the other arrays the represent the layout. + * Parse attempt->layout to populate the other arrays that represent the layout. */ protected function determine_layout() { - $this->pagelayout = array(); // Break up the layout string into pages. $pagelayouts = explode(',0', $this->attempt->layout); @@ -228,7 +228,7 @@ class quiz_attempt { // may be shuffled, so $section->firstslot (the lowest numbered slot in // the section) may not be the first one. $unseensections = $this->sections; - $this->pagelayout = array(); + $this->pagelayout = []; foreach ($pagelayouts as $page => $pagelayout) { $pagelayout = trim($pagelayout, ','); if ($pagelayout == '') { @@ -272,7 +272,7 @@ class quiz_attempt { /** * If the given page number is out of range (before the first page, or after - * the last page, chnage it to be within range). + * the last page, change it to be within range). * * @param int $page the requested page number. * @return int a safe page number to use. @@ -281,16 +281,31 @@ class quiz_attempt { return min(max($page, 0), count($this->pagelayout) - 1); } - // Simple getters ========================================================== + // Simple getters ==========================================================. + + /** + * Get the raw quiz settings object. + * + * @return stdClass + */ public function get_quiz() { return $this->quizobj->get_quiz(); } + /** + * Get the {@see quiz} object for this quiz. + * + * @return quiz + */ public function get_quizobj() { return $this->quizobj; } - /** @return int the course id. */ + /** + * Git the id of the course this quiz belongs to. + * + * @return int the course id. + */ public function get_courseid() { return $this->quizobj->get_courseid(); } @@ -304,22 +319,38 @@ class quiz_attempt { return $this->quizobj->get_course(); } - /** @return int the quiz id. */ + /** + * Get the quiz id. + * + * @return int the quiz id. + */ public function get_quizid() { return $this->quizobj->get_quizid(); } - /** @return string the name of this quiz. */ + /** + * Get the name of this quiz. + * + * @return string Quiz name, directly from the database (format_string must be called before output). + */ public function get_quiz_name() { return $this->quizobj->get_quiz_name(); } - /** @return int the quiz navigation method. */ + /** + * Get the quiz navigation method. + * + * @return int QUIZ_NAVMETHOD_FREE or QUIZ_NAVMETHOD_SEQ. + */ public function get_navigation_method() { return $this->quizobj->get_navigation_method(); } - /** @return object the course_module object. */ + /** + * Get the course_module for this quiz. + * + * @return stdClass|cm_info the course_module object. + */ public function get_cm() { return $this->quizobj->get_cm(); } @@ -334,24 +365,35 @@ class quiz_attempt { } /** - * @return bool whether the current user is someone who previews the quiz, - * rather than attempting it. + * Is the current user is someone who previews the quiz, rather than attempting it? + * + * @return bool true user is a preview user. False, if they can do real attempts. */ public function is_preview_user() { return $this->quizobj->is_preview_user(); } - /** @return int the number of attempts allowed at this quiz (0 = infinite). */ + /** + * Get the number of attempts the user is allowed at this quiz. + * + * @return int the number of attempts allowed at this quiz (0 = infinite). + */ public function get_num_attempts_allowed() { return $this->quizobj->get_num_attempts_allowed(); } - /** @return int number fo pages in this quiz. */ + /** + * Get the number of quizzes in the quiz attempt. + * + * @return int number pages. + */ public function get_num_pages() { return count($this->pagelayout); } /** + * Get the access_manager for this quiz attempt. + * * @param int $timenow the current time as a unix timestamp. * @return access_manager and instance of the access_manager class * for this quiz at this time. @@ -360,61 +402,99 @@ class quiz_attempt { return $this->quizobj->get_access_manager($timenow); } - /** @return int the attempt id. */ + /** + * Get the id of this attempt. + * + * @return int the attempt id. + */ public function get_attemptid() { return $this->attempt->id; } - /** @return int the attempt unique id. */ + /** + * Get the question-usage id corresponding to this quiz attempt. + * + * @return int the attempt unique id. + */ public function get_uniqueid() { return $this->attempt->uniqueid; } - /** @return object the row from the quiz_attempts table. */ + /** + * Get the raw quiz attempt object. + * + * @return stdClass the row from the quiz_attempts table. + */ public function get_attempt() { return $this->attempt; } - /** @return int the number of this attemp (is it this user's first, second, ... attempt). */ + /** + * Get the attempt number. + * + * @return int the number of this attempt (is it this user's first, second, ... attempt). + */ public function get_attempt_number() { return $this->attempt->attempt; } - /** @return string one of the quiz_attempt::IN_PROGRESS, FINISHED, OVERDUE or ABANDONED constants. */ + /** + * Get the state of this attempt. + * + * @return string {@see IN_PROGRESS}, {@see FINISHED}, {@see OVERDUE} or {@see ABANDONED}. + */ public function get_state() { return $this->attempt->state; } - /** @return int the id of the user this attempt belongs to. */ + /** + * Get the id of the user this attempt belongs to. + * @return int user id. + */ public function get_userid() { return $this->attempt->userid; } - /** @return int the current page of the attempt. */ + /** + * Get the current page of the attempt + * @return int page number. + */ public function get_currentpage() { return $this->attempt->currentpage; } + /** + * Get the total number of marks that the user had scored on all the questions. + * + * @return float + */ public function get_sum_marks() { return $this->attempt->sumgrades; } /** - * @return bool whether this attempt has been finished (true) or is still - * in progress (false). Be warned that this is not just state == self::FINISHED, - * it also includes self::ABANDONED. + * Has this attempt been finished? + * + * States {@see FINISHED} and {@see ABANDONED} are both considered finished in this state. + * Other states are not. + * + * @return bool */ public function is_finished() { return $this->attempt->state == self::FINISHED || $this->attempt->state == self::ABANDONED; } - /** @return bool whether this attempt is a preview attempt. */ + /** + * Is this attempt a preview? + * + * @return bool true if it is. + */ public function is_preview() { return $this->attempt->preview; } /** - * Is this someone dealing with their own attempt or preview? + * Does this attempt belong to the current user? * * @return bool true => own attempt/preview. false => reviewing someone else's. */ @@ -424,7 +504,9 @@ class quiz_attempt { } /** - * @return bool whether this attempt is a preview belonging to the current user. + * Is this attempt is a preview belonging to the current user. + * + * @return bool true if it is. */ public function is_own_preview() { return $this->is_own_attempt() && @@ -433,7 +515,7 @@ class quiz_attempt { /** * Is the current user allowed to review this attempt. This applies when - * {@link is_own_attempt()} returns false. + * {@see is_own_attempt()} returns false. * * @return bool whether the review should be allowed. */ @@ -492,7 +574,7 @@ class quiz_attempt { * about the attempt as a whole, and this method provides access to that data. * To see how this works, try setting a quiz to one of the CBM behaviours, * and then look at the extra information displayed at the top of the quiz - * review page once you have sumitted an attempt. + * review page once you have submitted an attempt. * * In the return value, the array keys are identifiers of the form * qbehaviour_behaviourname_meaningfullkey. For qbehaviour_deferredcbm_highsummary. @@ -518,12 +600,12 @@ class quiz_attempt { } /** - * Wrapper round the has_capability funciton that automatically passes in the quiz context. + * Wrapper round the has_capability function that automatically passes in the quiz context. * * @param string $capability the name of the capability to check. For example mod/forum:view. - * @param int|null $userid A user id. By default (null) checks the permissions of the current user. + * @param int|null $userid A user id. If null checks the permissions of the current user. * @param bool $doanything If false, ignore effect of admin role assignment. - * @return boolean true if the user has this capability. Otherwise false. + * @return boolean true if the user has this capability, otherwise false. */ public function has_capability($capability, $userid = null, $doanything = true) { return $this->quizobj->has_capability($capability, $userid, $doanything); @@ -533,7 +615,7 @@ class quiz_attempt { * Wrapper round the require_capability function that automatically passes in the quiz context. * * @param string $capability the name of the capability to check. For example mod/forum:view. - * @param int|null $userid A user id. By default (null) checks the permissions of the current user. + * @param int|null $userid A user id. If null checks the permissions of the current user. * @param bool $doanything If false, ignore effect of admin role assignment. */ public function require_capability($capability, $userid = null, $doanything = true) { @@ -551,9 +633,9 @@ class quiz_attempt { $capability = 'mod/quiz:reviewmyattempts'; } - // These next tests are in a slighly funny order. The point is that the - // common and most performance-critical case is students attempting a quiz - // so we want to check that permisison first. + // These next tests are in a slightly funny order. The point is that the + // common and most performance-critical case is students attempting a quiz, + // so we want to check that permission first. if ($this->has_capability($capability)) { // User has the permission that lets you do the quiz as a student. Fine. @@ -568,7 +650,7 @@ class quiz_attempt { // They should not be here. Trigger the standard no-permission error // but using the name of the student capability. - // We know this will fail. We just want the stadard exception thown. + // We know this will fail. We just want the standard exception thrown. $this->require_capability($capability); } @@ -585,20 +667,14 @@ class quiz_attempt { return false; } - switch ($this->get_navigation_method()) { - case QUIZ_NAVMETHOD_FREE: - return true; - break; - case QUIZ_NAVMETHOD_SEQ: - return false; - break; - } - return true; + return $this->get_navigation_method() == QUIZ_NAVMETHOD_FREE; } /** - * @return int one of the display_options::DURING, - * IMMEDIATELY_AFTER, LATER_WHILE_OPEN or AFTER_CLOSE constants. + * Get where we are time-wise in relation to this attempt and the quiz settings. + * + * @return int one of {@see display_options::DURING}, {@see display_options::IMMEDIATELY_AFTER}, + * {@see display_options::LATER_WHILE_OPEN} or {@see display_options::AFTER_CLOSE}. */ public function get_attempt_state() { return quiz_attempt_state($this->get_quiz(), $this->attempt); @@ -667,8 +743,10 @@ class quiz_attempt { } /** - * @param int $page page number - * @return bool true if this is the last page of the quiz. + * Is a particular page the last one in the quiz? + * + * @param int $page a page number + * @return bool true if that is the last page of the quiz. */ public function is_last_page($page) { return $page == count($this->pagelayout) - 1; @@ -683,7 +761,7 @@ class quiz_attempt { */ public function get_slots($page = 'all') { if ($page === 'all') { - $numbers = array(); + $numbers = []; foreach ($this->pagelayout as $numbersonpage) { $numbers = array_merge($numbers, $numbersonpage); } @@ -701,7 +779,7 @@ class quiz_attempt { * @return array the requested list of slot numbers. */ public function get_active_slots($page = 'all') { - $activeslots = array(); + $activeslots = []; foreach ($this->get_slots($page) as $slot) { if (!$this->is_blocked_by_previous_question($slot)) { $activeslots[] = $slot; @@ -742,7 +820,7 @@ class quiz_attempt { * @return question_attempt[] the attempts. */ public function all_question_attempts_originally_in_slot($slot) { - $qas = array(); + $qas = []; foreach ($this->quba->get_attempt_iterator() as $qa) { if ($qa->get_metadata('originalslot') == $slot) { $qas[] = $qa; @@ -831,7 +909,7 @@ class quiz_attempt { * If the section heading, if any, that should come just before this slot. * * @param int $slot identifies a particular question in this attempt. - * @return string the required heading, or null if there is not one here. + * @return string|null the required heading, or null if there is not one here. */ public function get_heading_before_slot($slot) { if ($this->slots[$slot]->firstinsection) { @@ -865,7 +943,7 @@ class quiz_attempt { } /** - * Return the {@link question_state} that this question is in. + * Return the {@see question_state} that this question is in. * * @param int $slot the number used to identify this question within this attempt. * @return question_state the state this question is in. @@ -931,7 +1009,6 @@ class quiz_attempt { * * @param int $slot the number used to identify this question within this attempt. * @return string the question type name. - * @since Moodle 3.1 */ public function get_question_type_name($slot) { return $this->quba->get_question($slot, false)->get_type_name(); @@ -954,8 +1031,9 @@ class quiz_attempt { /** - * @return int the time when this attempt was submitted. 0 if it has not been - * submitted yet. + * Get the time when this attempt was submitted. + * + * @return int timestamp, or 0 if it has not been submitted yet. */ public function get_submitted_date() { return $this->attempt->timefinish; @@ -968,7 +1046,7 @@ class quiz_attempt { * @return int timestamp by which the student needs to do something. */ public function get_due_date() { - $deadlines = array(); + $deadlines = []; if ($this->quizobj->get_quiz()->timelimit) { $deadlines[] = $this->attempt->timestart + $this->quizobj->get_quiz()->timelimit; } @@ -993,12 +1071,15 @@ class quiz_attempt { } } - // URLs related to this attempt ============================================ + // URLs related to this attempt ============================================. + /** - * @return string quiz view url. + * Get the URL of this quiz's view.php page. + * + * @return moodle_url quiz view url. */ public function view_url() { - return $this->quizobj->view_url(); + return new moodle_url($this->quizobj->view_url()); } /** @@ -1006,7 +1087,7 @@ class quiz_attempt { * * @param int|null $slot which question in the attempt to go to after starting (optional). * @param int $page which page in the attempt to go to after starting. - * @return string the URL of this quiz's edit page. Needs to be POSTed to with a cmid parameter. + * @return moodle_url the URL of this quiz's edit page. Needs to be POSTed to with a cmid parameter. */ public function start_attempt_url($slot = null, $page = -1) { if ($page == -1 && !is_null($slot)) { @@ -1038,12 +1119,14 @@ class quiz_attempt { } /** + * Get the URL of a particular page within this attempt. + * * @param int|null $slot if specified, the slot number of a specific question to link to. * @param int $page if specified, a particular page to link to. If not given deduced * from $slot, or goes to the first page. * @param int $thispage if not -1, the current page. Will cause links to other things on - * this page to be output as only a fragment. - * @return string the URL to continue this attempt. + * this page to be output as only a fragment. + * @return moodle_url the URL to continue this attempt. */ public function attempt_url($slot = null, $page = -1, $thispage = -1) { return $this->page_and_question_url('attempt', $slot, $page, false, $thispage); @@ -1059,13 +1142,17 @@ class quiz_attempt { } /** + * Get the URL of the summary page of this attempt. + * * @return moodle_url the URL of this quiz's summary page. */ public function summary_url() { - return new moodle_url('/mod/quiz/summary.php', array('attempt' => $this->attempt->id, 'cmid' => $this->get_cmid())); + return new moodle_url('/mod/quiz/summary.php', ['attempt' => $this->attempt->id, 'cmid' => $this->get_cmid()]); } /** + * Get the URL to which the attempt data should be submitted. + * * @return moodle_url the URL of this quiz's summary page. */ public function processattempt_url() { @@ -1094,6 +1181,8 @@ class quiz_attempt { } /** + * Get the URL of a particular page in the review of this attempt. + * * @param int|null $slot indicates which question to link to. * @param int $page if specified, the URL of this particular page of the attempt, otherwise * the URL will go to the first page. If -1, deduce $page from $slot. @@ -1101,7 +1190,7 @@ class quiz_attempt { * and $page will be ignored. If null, a sensible default will be chosen. * @param int $thispage if not -1, the current page. Will cause links to other things on * this page to be output as only a fragment. - * @return string the URL to review this attempt. + * @return moodle_url the URL to review this attempt. */ public function review_url($slot = null, $page = -1, $showall = null, $thispage = -1) { return $this->page_and_question_url('review', $slot, $page, $showall, $thispage); @@ -1117,7 +1206,7 @@ class quiz_attempt { return $script === 'review' && count($this->questionpages) < self::MAX_SLOTS_FOR_DEFAULT_REVIEW_SHOW_ALL; } - // Bits of content ========================================================= + // Bits of content =========================================================. /** * If $reviewoptions->attempt is false, meaning that students can't review this @@ -1135,7 +1224,7 @@ class quiz_attempt { * Initialise the JS etc. required all the questions on a page. * * @param int|string $page a page number, or 'all'. - * @param bool $showall if true forces page number to all. + * @param bool $showall if true, forces page number to all. * @return string HTML to output - mostly obsolete, will probably be an empty string. */ public function get_html_head_contributions($page = 'all', $showall = false) { @@ -1171,7 +1260,7 @@ class quiz_attempt { global $OUTPUT; if ($this->is_preview() && $this->is_preview_user()) { return $OUTPUT->single_button(new moodle_url( - $this->start_attempt_url(), array('forcenew' => true)), + $this->start_attempt_url(), ['forcenew' => true]), get_string('startnewpreview', 'quiz')); } else { return ''; @@ -1179,7 +1268,7 @@ class quiz_attempt { } /** - * Generate the HTML that displayes the question in its current state, with + * Generate the HTML that displays the question in its current state, with * the appropriate display options. * * @param int $slot identifies the question in the attempt. @@ -1206,7 +1295,7 @@ class quiz_attempt { } /** - * Helper used by {@link render_question()} and {@link render_question_at_step()}. + * Helper used by {@see render_question()} and {@see render_question_at_step()}. * * @param int $slot identifies the question in the attempt. * @param bool $reviewing is the being printed on an attempt or a review page. @@ -1291,7 +1380,7 @@ class quiz_attempt { } /** - * Like {@link render_question()} but displays the question at the past step + * Like {@see render_question()} but displays the question at the past step * indicated by $seq, rather than showing the latest step. * * @param int $slot the slot number of a question in this quiz attempt. @@ -1321,7 +1410,7 @@ class quiz_attempt { } /** - * Check wheter access should be allowed to a particular file. + * Check whether access should be allowed to a particular file. * * @param int $slot the slot of a question in this quiz attempt. * @param bool $reviewing is the being printed on an attempt or a review page. @@ -1330,7 +1419,7 @@ class quiz_attempt { * @param string $filearea the file area from the request. * @param array $args extra part components from the request. * @param bool $forcedownload whether to force download. - * @return string HTML for the question in its current state. + * @return bool true if the file can be accessed. */ public function check_file_access($slot, $reviewing, $contextid, $component, $filearea, $args, $forcedownload) { @@ -1378,7 +1467,7 @@ class quiz_attempt { * * The {@see links_to_other_attempts} object returned contains an * array with keys that are the attempt number, 1, 2, 3. - * The array values are either a {@link moodle_url} with the attempt parameter + * The array values are either a {@see moodle_url} with the attempt parameter * updated to point to the attempt id of the other attempt, or null corresponding * to the current attempt number. * @@ -1397,7 +1486,7 @@ class quiz_attempt { if ($at->id == $this->attempt->id) { $links->links[$at->attempt] = null; } else { - $links->links[$at->attempt] = new moodle_url($url, array('attempt' => $at->id)); + $links->links[$at->attempt] = new moodle_url($url, ['attempt' => $at->id]); } } return $links; @@ -1433,18 +1522,18 @@ class quiz_attempt { if ($qa->get_slot() == $slot) { $links->links[$index] = null; } else { - $url = new moodle_url($baseurl, array('slot' => $qa->get_slot())); + $url = new moodle_url($baseurl, ['slot' => $qa->get_slot()]); $links->links[$index] = new action_link($url, $index, new popup_action('click', $url, 'reviewquestion', - array('width' => 450, 'height' => 650)), - array('title' => get_string('reviewresponse', 'question'))); + ['width' => 450, 'height' => 650]), + ['title' => get_string('reviewresponse', 'question')]); } $index++; } return $links; } - // Methods for processing ================================================== + // Methods for processing ==================================================. /** * Check this attempt, to see if there are any state transitions that should @@ -1472,7 +1561,7 @@ class quiz_attempt { if ($timeoverdue >= $graceperiod) { $this->process_abandon($timestamp, $studentisonline); } else { - // Overdue time has not yet expired + // Overdue time has not yet expired. $this->update_timecheckstate($timeclose + $graceperiod); } return; // ... and we are done. @@ -1501,7 +1590,6 @@ class quiz_attempt { // This is an overdue attempt with no overdue handling defined, so just abandon. $this->process_abandon($timestamp, $studentisonline); - return; } /** @@ -1512,9 +1600,9 @@ class quiz_attempt { * @param bool $becomingoverdue * @param array|null $simulatedresponses If not null, then we are testing, and this is an array of simulated data. * There are two formats supported here, for historical reasons. The newer approach is to pass an array created by - * {@link core_question_generator::get_simulated_post_data_for_questions_in_usage()}. + * {@see core_question_generator::get_simulated_post_data_for_questions_in_usage()}. * the second is to pass an array slot no => contains arrays representing student - * responses which will be passed to {@link question_definition::prepare_simulated_post_data()}. + * responses which will be passed to {@see question_definition::prepare_simulated_post_data()}. * This second method will probably get deprecated one day. */ public function process_submitted_actions($timestamp, $becomingoverdue = false, $simulatedresponses = null) { @@ -1557,7 +1645,7 @@ class quiz_attempt { * Replace a question in an attempt with a new attempt at the same question. * * Well, for randomised questions, it won't be the same question, it will be - * a different randomised selection. + * a different randomly selected pick from the available question. * * @param int $slot the question to restart. * @param int $timestamp the timestamp to record for this action. @@ -1585,7 +1673,7 @@ class quiz_attempt { if ($newquestion->get_num_variants() == 1) { $variant = 1; } else { - $variantstrategy = new core_question\engine\variants\least_used_strategy( + $variantstrategy = new \core_question\engine\variants\least_used_strategy( $this->quba, $qubaids); $variant = $variantstrategy->choose_variant($newquestion->get_num_variants(), $newquestion->get_variants_selection_seed()); @@ -1695,7 +1783,7 @@ class quiz_attempt { global $DB; if ($this->attempt->timecheckstate !== $time) { $this->attempt->timecheckstate = $time; - $DB->set_field('quiz_attempts', 'timecheckstate', $time, array('id' => $this->attempt->id)); + $DB->set_field('quiz_attempts', 'timecheckstate', $time, ['id' => $this->attempt->id]); } } @@ -1712,7 +1800,7 @@ class quiz_attempt { $this->attempt->timemodified = $timestamp; $this->attempt->state = self::OVERDUE; // If we knew the attempt close time, we could compute when the graceperiod ends. - // Instead we'll just fix it up through cron. + // Instead, we'll just fix it up through cron. $this->attempt->timecheckstate = $timestamp; $DB->update_record('quiz_attempts', $this->attempt); @@ -1753,30 +1841,30 @@ class quiz_attempt { protected function fire_state_transition_event($eventclass, $timestamp, $studentisonline) { global $USER; $quizrecord = $this->get_quiz(); - $params = array( + $params = [ 'context' => $this->get_quizobj()->get_context(), 'courseid' => $this->get_courseid(), 'objectid' => $this->attempt->id, 'relateduserid' => $this->attempt->userid, - 'other' => array( + 'other' => [ 'submitterid' => CLI_SCRIPT ? null : $USER->id, 'quizid' => $quizrecord->id, 'studentisonline' => $studentisonline - ) - ); + ] + ]; $event = $eventclass::create($params); $event->add_record_snapshot('quiz', $this->get_quiz()); $event->add_record_snapshot('quiz_attempts', $this->get_attempt()); $event->trigger(); } - // Private methods ========================================================= + // Private methods =========================================================. /** * Get a URL for a particular question on a particular page of the quiz. - * Used by {@link attempt_url()} and {@link review_url()}. + * Used by {@see attempt_url()} and {@see review_url()}. * - * @param string $script. Used in the URL like /mod/quiz/$script.php. + * @param string $script e.g. 'attempt' or 'review'. Used in the URL like /mod/quiz/$script.php. * @param int $slot identifies the specific question on the page to jump to. * 0 to just use the $page parameter. * @param int $page -1 to look up the page number from the slot, otherwise @@ -1826,7 +1914,7 @@ class quiz_attempt { } else { $url = new moodle_url('/mod/quiz/' . $script . '.php' . $fragment, - array('attempt' => $this->attempt->id, 'cmid' => $this->get_cmid())); + ['attempt' => $this->attempt->id, 'cmid' => $this->get_cmid()]); if ($page == 0 && $showall != $defaultshowall) { $url->param('showall', (int) $showall); } else if ($page > 0) { @@ -2005,7 +2093,7 @@ class quiz_attempt { global $DB; if ($this->check_page_access($page)) { - $DB->set_field('quiz_attempts', 'currentpage', $page, array('id' => $this->get_attemptid())); + $DB->set_field('quiz_attempts', 'currentpage', $page, ['id' => $this->get_attemptid()]); return true; } return false; @@ -2017,16 +2105,16 @@ class quiz_attempt { * @since Moodle 3.1 */ public function fire_attempt_viewed_event() { - $params = array( + $params = [ 'objectid' => $this->get_attemptid(), 'relateduserid' => $this->get_userid(), 'courseid' => $this->get_courseid(), 'context' => context_module::instance($this->get_cmid()), - 'other' => array( + 'other' => [ 'quizid' => $this->get_quizid(), 'page' => $this->get_currentpage() - ) - ); + ] + ]; $event = \mod_quiz\event\attempt_viewed::create($params); $event->add_record_snapshot('quiz_attempts', $this->get_attempt()); $event->trigger(); @@ -2106,15 +2194,15 @@ class quiz_attempt { */ public function fire_attempt_summary_viewed_event() { - $params = array( + $params = [ 'objectid' => $this->get_attemptid(), 'relateduserid' => $this->get_userid(), 'courseid' => $this->get_courseid(), 'context' => context_module::instance($this->get_cmid()), - 'other' => array( + 'other' => [ 'quizid' => $this->get_quizid() - ) - ); + ] + ]; $event = \mod_quiz\event\attempt_summary_viewed::create($params); $event->add_record_snapshot('quiz_attempts', $this->get_attempt()); $event->trigger(); @@ -2127,15 +2215,15 @@ class quiz_attempt { */ public function fire_attempt_reviewed_event() { - $params = array( + $params = [ 'objectid' => $this->get_attemptid(), 'relateduserid' => $this->get_userid(), 'courseid' => $this->get_courseid(), 'context' => context_module::instance($this->get_cmid()), - 'other' => array( + 'other' => [ 'quizid' => $this->get_quizid() - ) - ); + ] + ]; $event = \mod_quiz\event\attempt_reviewed::create($params); $event->add_record_snapshot('quiz_attempts', $this->get_attempt()); $event->trigger(); diff --git a/mod/quiz/lib.php b/mod/quiz/lib.php index dc1c46fdb3a..abf5afb16e0 100644 --- a/mod/quiz/lib.php +++ b/mod/quiz/lib.php @@ -689,9 +689,9 @@ function quiz_get_grade_format($quiz) { /** * Round a grade to the correct number of decimal places, and format it for display. * - * @param object $quiz The quiz table row, only $quiz->decimalpoints is used. + * @param stdClass $quiz The quiz table row, only $quiz->decimalpoints is used. * @param float $grade The grade to round. - * @return float + * @return string */ function quiz_format_question_grade($quiz, $grade) { return format_float($grade, quiz_get_grade_format($quiz)); diff --git a/question/type/questionbase.php b/question/type/questionbase.php index 0febd2fc466..cbee8a562b7 100644 --- a/question/type/questionbase.php +++ b/question/type/questionbase.php @@ -139,13 +139,12 @@ abstract class question_definition { * Constructor. Normally to get a question, you call * {@link question_bank::load_question()}, but questions can be created * directly, for example in unit test code. - * @return unknown_type */ public function __construct() { } /** - * @return the name of the question type (for example multichoice) that this + * @return string the name of the question type (for example multichoice) that this * question is. */ public function get_type_name() { From a9f854759965d3f64eef81aaeee70042cffe5172 Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Tue, 20 Dec 2022 10:50:35 +0000 Subject: [PATCH 26/30] MDL-76614 quiz: rename quiz => mod_quiz\quiz_settings --- .../tool/recyclebin/tests/course_bin_test.php | 3 +- .../tests/restore_stepslib_date_test.php | 4 +- lib/questionlib.php | 2 +- .../accessrule/delaybetweenattempts/rule.php | 3 +- .../delaybetweenattempts/tests/rule_test.php | 12 +- mod/quiz/accessrule/ipaddress/rule.php | 3 +- .../accessrule/ipaddress/tests/rule_test.php | 6 +- mod/quiz/accessrule/numattempts/rule.php | 3 +- .../numattempts/tests/rule_test.php | 4 +- mod/quiz/accessrule/offlineattempts/rule.php | 3 +- .../offlineattempts/tests/rule_test.php | 4 +- mod/quiz/accessrule/openclosedate/rule.php | 4 +- .../openclosedate/tests/rule_test.php | 12 +- mod/quiz/accessrule/password/rule.php | 3 +- .../accessrule/password/tests/rule_test.php | 4 +- .../accessrule/seb/classes/access_manager.php | 12 +- .../classes/external/validate_quiz_keys.php | 5 +- .../accessrule/seb/classes/quiz_settings.php | 2 +- mod/quiz/accessrule/seb/rule.php | 10 +- .../seb/tests/event/events_test.php | 6 +- .../seb/tests/quiz_settings_test.php | 4 +- .../seb/tests/test_helper_trait.php | 6 +- mod/quiz/accessrule/securewindow/rule.php | 3 +- .../securewindow/tests/rule_test.php | 4 +- mod/quiz/accessrule/timelimit/rule.php | 3 +- .../accessrule/timelimit/tests/rule_test.php | 6 +- mod/quiz/attemptlib.php | 544 +---------------- mod/quiz/classes/access_manager.php | 11 +- .../classes/completion/custom_completion.php | 4 +- mod/quiz/classes/external.php | 11 +- mod/quiz/classes/local/access_rule_base.php | 12 +- mod/quiz/classes/output/edit_renderer.php | 4 +- mod/quiz/classes/output/renderer.php | 6 +- .../privacy/legacy_quizaccess_polyfill.php | 12 +- mod/quiz/classes/privacy/provider.php | 8 +- .../classes/privacy/quizaccess_provider.php | 12 +- .../classes/question/bank/qbank_helper.php | 1 - .../question/qubaids_for_users_attempts.php | 1 - mod/quiz/classes/quiz_attempt.php | 10 +- mod/quiz/classes/quiz_settings.php | 556 ++++++++++++++++++ mod/quiz/classes/structure.php | 4 +- mod/quiz/db/renamedclasses.php | 1 + mod/quiz/deprecatedlib.php | 5 +- mod/quiz/edit.php | 3 +- mod/quiz/edit_rest.php | 4 +- mod/quiz/lib.php | 8 +- mod/quiz/locallib.php | 18 +- mod/quiz/repaginate.php | 4 +- .../report/overview/tests/report_test.php | 8 +- mod/quiz/report/reportlib.php | 3 +- .../statistics/classes/task/recalculate.php | 4 +- mod/quiz/startattempt.php | 3 +- mod/quiz/tests/attempt_test.php | 8 +- .../attempt_walkthrough_from_csv_test.php | 4 +- mod/quiz/tests/attempt_walkthrough_test.php | 12 +- mod/quiz/tests/attempts_test.php | 4 +- mod/quiz/tests/backup/restore_date_test.php | 2 +- mod/quiz/tests/custom_completion_test.php | 4 +- mod/quiz/tests/event/events_test.php | 13 +- mod/quiz/tests/external/external_test.php | 22 +- mod/quiz/tests/generator/lib.php | 3 +- mod/quiz/tests/lib_test.php | 20 +- mod/quiz/tests/privacy/provider_test.php | 4 +- ...rivacy_legacy_quizaccess_polyfill_test.php | 19 +- mod/quiz/tests/qbank_helper_test.php | 4 +- ..._attempt_manual_grading_completed_test.php | 6 +- .../tests/quiz_question_helper_test_trait.php | 3 +- mod/quiz/tests/quiz_question_restore_test.php | 6 +- mod/quiz/tests/quiz_question_version_test.php | 4 +- mod/quiz/tests/quizobj_test.php | 6 +- mod/quiz/tests/repaginate_test.php | 4 +- mod/quiz/tests/reportlib_test.php | 1 - mod/quiz/tests/structure_test.php | 12 +- mod/quiz/tests/tags_test.php | 4 +- mod/quiz/upgrade.txt | 2 + mod/quiz/view.php | 3 +- .../bank/statistics/tests/helper_test.php | 4 +- question/bank/usage/tests/helper_test.php | 2 +- .../bank/usage/tests/question_usage_test.php | 2 +- 79 files changed, 795 insertions(+), 771 deletions(-) create mode 100644 mod/quiz/classes/quiz_settings.php diff --git a/admin/tool/recyclebin/tests/course_bin_test.php b/admin/tool/recyclebin/tests/course_bin_test.php index 10a2427c98d..853a88c0c55 100644 --- a/admin/tool/recyclebin/tests/course_bin_test.php +++ b/admin/tool/recyclebin/tests/course_bin_test.php @@ -17,6 +17,7 @@ namespace tool_recyclebin; use mod_quiz\quiz_attempt; +use stdClass; /** * Recycle bin course tests. @@ -302,7 +303,7 @@ class course_bin_test extends \advanced_testcase { quiz_add_quiz_question($numq->id, $quiz); // Create quiz attempt. - $quizobj = \quiz::create($quiz->id, $student->id); + $quizobj = \mod_quiz\quiz_settings::create($quiz->id, $student->id); $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); $timenow = time(); diff --git a/backup/moodle2/tests/restore_stepslib_date_test.php b/backup/moodle2/tests/restore_stepslib_date_test.php index 009f57d3626..3832d4d16b0 100644 --- a/backup/moodle2/tests/restore_stepslib_date_test.php +++ b/backup/moodle2/tests/restore_stepslib_date_test.php @@ -381,7 +381,7 @@ class restore_stepslib_date_test extends \restore_date_testcase { // Make a user to do the quiz. $user1 = $this->getDataGenerator()->create_user(); - $quizobj = \quiz::create($quiz->id, $user1->id); + $quizobj = \mod_quiz\quiz_settings::create($quiz->id, $user1->id); // Start the attempt. $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); @@ -421,7 +421,7 @@ class restore_stepslib_date_test extends \restore_date_testcase { // Get the quiz for this new restored course. $quizdata = $DB->get_record('quiz', ['course' => $newcourseid]); - $quizobj = \quiz::create($quizdata->id, $user1->id); + $quizobj = \mod_quiz\quiz_settings::create($quizdata->id, $user1->id); $questionusage = $DB->get_record('question_usages', [ 'component' => 'mod_quiz', diff --git a/lib/questionlib.php b/lib/questionlib.php index 19de3ddc505..23325c85bed 100644 --- a/lib/questionlib.php +++ b/lib/questionlib.php @@ -841,7 +841,7 @@ function question_move_category_to_context($categoryid, $oldcontextid, $newconte /** * Given a list of ids, load the basic information about a set of questions from * the questions table. The $join and $extrafields arguments can be used together - * to pull in extra data. See, for example, the usage in mod/quiz/attemptlib.php, and + * to pull in extra data. See, for example, the usage in {@see \mod_quiz\quiz_attempt}, and * read the code below to see how the SQL is assembled. Throws exceptions on error. * * @param array $questionids array of question ids to load. If null, then all diff --git a/mod/quiz/accessrule/delaybetweenattempts/rule.php b/mod/quiz/accessrule/delaybetweenattempts/rule.php index 81a0581fb26..f2cf9d1f1fe 100644 --- a/mod/quiz/accessrule/delaybetweenattempts/rule.php +++ b/mod/quiz/accessrule/delaybetweenattempts/rule.php @@ -15,6 +15,7 @@ // along with Moodle. If not, see . use mod_quiz\local\access_rule_base; +use mod_quiz\quiz_settings; /** * A rule imposing the delay between attempts settings. @@ -25,7 +26,7 @@ use mod_quiz\local\access_rule_base; */ class quizaccess_delaybetweenattempts extends access_rule_base { - public static function make(quiz $quizobj, $timenow, $canignoretimelimits) { + public static function make(quiz_settings $quizobj, $timenow, $canignoretimelimits) { if (empty($quizobj->get_quiz()->delay1) && empty($quizobj->get_quiz()->delay2)) { return null; } diff --git a/mod/quiz/accessrule/delaybetweenattempts/tests/rule_test.php b/mod/quiz/accessrule/delaybetweenattempts/tests/rule_test.php index 812bc9228ac..72c902813af 100644 --- a/mod/quiz/accessrule/delaybetweenattempts/tests/rule_test.php +++ b/mod/quiz/accessrule/delaybetweenattempts/tests/rule_test.php @@ -16,7 +16,7 @@ namespace quizaccess_delaybetweenattempts; -use quiz; +use mod_quiz\quiz_settings; use quizaccess_delaybetweenattempts; defined('MOODLE_INTERNAL') || die(); @@ -43,7 +43,7 @@ class rule_test extends \basic_testcase { $quiz->timeclose = 0; $cm = new \stdClass(); $cm->id = 0; - $quizobj = new quiz($quiz, $cm, null); + $quizobj = new quiz_settings($quiz, $cm, null); $attempt = new \stdClass(); $attempt->timefinish = 10000; @@ -77,7 +77,7 @@ class rule_test extends \basic_testcase { $quiz->timeclose = 0; $cm = new \stdClass(); $cm->id = 0; - $quizobj = new quiz($quiz, $cm, null); + $quizobj = new quiz_settings($quiz, $cm, null); $attempt = new \stdClass(); $attempt->timefinish = 10000; @@ -116,7 +116,7 @@ class rule_test extends \basic_testcase { $quiz->timeclose = 0; $cm = new \stdClass(); $cm->id = 0; - $quizobj = new quiz($quiz, $cm, null); + $quizobj = new quiz_settings($quiz, $cm, null); $attempt = new \stdClass(); $attempt->timefinish = 10000; @@ -167,7 +167,7 @@ class rule_test extends \basic_testcase { $quiz->timeclose = 15000; $cm = new \stdClass(); $cm->id = 0; - $quizobj = new quiz($quiz, $cm, null); + $quizobj = new quiz_settings($quiz, $cm, null); $attempt = new \stdClass(); $attempt->timefinish = 13000; @@ -223,7 +223,7 @@ class rule_test extends \basic_testcase { $quiz->timeclose = 0; $cm = new \stdClass(); $cm->id = 0; - $quizobj = new quiz($quiz, $cm, null); + $quizobj = new quiz_settings($quiz, $cm, null); $attempt = new \stdClass(); $attempt->timestart = 9900; $attempt->timefinish = 10100; diff --git a/mod/quiz/accessrule/ipaddress/rule.php b/mod/quiz/accessrule/ipaddress/rule.php index f6670d87a1d..e0d78c914d7 100644 --- a/mod/quiz/accessrule/ipaddress/rule.php +++ b/mod/quiz/accessrule/ipaddress/rule.php @@ -15,6 +15,7 @@ // along with Moodle. If not, see . use mod_quiz\local\access_rule_base; +use mod_quiz\quiz_settings; /** * A rule implementing the ipaddress check against the ->subnet setting. @@ -25,7 +26,7 @@ use mod_quiz\local\access_rule_base; */ class quizaccess_ipaddress extends access_rule_base { - public static function make(quiz $quizobj, $timenow, $canignoretimelimits) { + public static function make(quiz_settings $quizobj, $timenow, $canignoretimelimits) { if (empty($quizobj->get_quiz()->subnet)) { return null; } diff --git a/mod/quiz/accessrule/ipaddress/tests/rule_test.php b/mod/quiz/accessrule/ipaddress/tests/rule_test.php index c5ff7a6eec8..9a4d73df39d 100644 --- a/mod/quiz/accessrule/ipaddress/tests/rule_test.php +++ b/mod/quiz/accessrule/ipaddress/tests/rule_test.php @@ -16,7 +16,7 @@ namespace quizaccess_ipaddress; -use quiz; +use mod_quiz\quiz_settings; use quizaccess_ipaddress; defined('MOODLE_INTERNAL') || die(); @@ -44,7 +44,7 @@ class rule_test extends \basic_testcase { // does not always work, for example using the mac install package on my laptop. $quiz->subnet = getremoteaddr(null); if (!empty($quiz->subnet)) { - $quizobj = new quiz($quiz, $cm, null); + $quizobj = new quiz_settings($quiz, $cm, null); $rule = new quizaccess_ipaddress($quizobj, 0); $this->assertFalse($rule->prevent_access()); @@ -56,7 +56,7 @@ class rule_test extends \basic_testcase { } $quiz->subnet = '0.0.0.0'; - $quizobj = new quiz($quiz, $cm, null); + $quizobj = new quiz_settings($quiz, $cm, null); $rule = new quizaccess_ipaddress($quizobj, 0); $this->assertNotEmpty($rule->prevent_access()); diff --git a/mod/quiz/accessrule/numattempts/rule.php b/mod/quiz/accessrule/numattempts/rule.php index 517ba24191b..3c87006e4d7 100644 --- a/mod/quiz/accessrule/numattempts/rule.php +++ b/mod/quiz/accessrule/numattempts/rule.php @@ -15,6 +15,7 @@ // along with Moodle. If not, see . use mod_quiz\local\access_rule_base; +use mod_quiz\quiz_settings; /** * A rule controlling the number of attempts allowed. @@ -25,7 +26,7 @@ use mod_quiz\local\access_rule_base; */ class quizaccess_numattempts extends access_rule_base { - public static function make(quiz $quizobj, $timenow, $canignoretimelimits) { + public static function make(quiz_settings $quizobj, $timenow, $canignoretimelimits) { if ($quizobj->get_num_attempts_allowed() == 0) { return null; diff --git a/mod/quiz/accessrule/numattempts/tests/rule_test.php b/mod/quiz/accessrule/numattempts/tests/rule_test.php index c500b0a792e..ada15512243 100644 --- a/mod/quiz/accessrule/numattempts/tests/rule_test.php +++ b/mod/quiz/accessrule/numattempts/tests/rule_test.php @@ -16,7 +16,7 @@ namespace quizaccess_numattempts; -use quiz; +use mod_quiz\quiz_settings; use quizaccess_numattempts; defined('MOODLE_INTERNAL') || die(); @@ -39,7 +39,7 @@ class rule_test extends \basic_testcase { $quiz->attempts = 3; $cm = new \stdClass(); $cm->id = 0; - $quizobj = new quiz($quiz, $cm, null); + $quizobj = new quiz_settings($quiz, $cm, null); $rule = new quizaccess_numattempts($quizobj, 0); $attempt = new \stdClass(); diff --git a/mod/quiz/accessrule/offlineattempts/rule.php b/mod/quiz/accessrule/offlineattempts/rule.php index 095b668902a..aaa3fe0053e 100644 --- a/mod/quiz/accessrule/offlineattempts/rule.php +++ b/mod/quiz/accessrule/offlineattempts/rule.php @@ -16,6 +16,7 @@ use mod_quiz\form\preflight_check_form; use mod_quiz\local\access_rule_base; +use mod_quiz\quiz_settings; /** * A rule implementing the offlineattempts check. @@ -27,7 +28,7 @@ use mod_quiz\local\access_rule_base; */ class quizaccess_offlineattempts extends access_rule_base { - public static function make(quiz $quizobj, $timenow, $canignoretimelimits) { + public static function make(quiz_settings $quizobj, $timenow, $canignoretimelimits) { global $CFG; // If mobile services are off, the user won't be able to use any external app. diff --git a/mod/quiz/accessrule/offlineattempts/tests/rule_test.php b/mod/quiz/accessrule/offlineattempts/tests/rule_test.php index ea314b37d5a..85e08089c3e 100644 --- a/mod/quiz/accessrule/offlineattempts/tests/rule_test.php +++ b/mod/quiz/accessrule/offlineattempts/tests/rule_test.php @@ -16,7 +16,7 @@ namespace quizaccess_offlineattempts; -use quiz; +use mod_quiz\quiz_settings; use quizaccess_offlineattempts; defined('MOODLE_INTERNAL') || die(); @@ -38,7 +38,7 @@ class rule_test extends \basic_testcase { $quiz->allowofflineattempts = 1; $cm = new \stdClass(); $cm->id = 0; - $quizobj = new quiz($quiz, $cm, null); + $quizobj = new quiz_settings($quiz, $cm, null); $rule = new quizaccess_offlineattempts($quizobj, 0); $attempt = new \stdClass(); diff --git a/mod/quiz/accessrule/openclosedate/rule.php b/mod/quiz/accessrule/openclosedate/rule.php index 62f332e7b5b..59ff513d103 100644 --- a/mod/quiz/accessrule/openclosedate/rule.php +++ b/mod/quiz/accessrule/openclosedate/rule.php @@ -15,7 +15,7 @@ // along with Moodle. If not, see . use mod_quiz\local\access_rule_base; - +use mod_quiz\quiz_settings; /** * A rule enforcing open and close dates. @@ -26,7 +26,7 @@ use mod_quiz\local\access_rule_base; */ class quizaccess_openclosedate extends access_rule_base { - public static function make(quiz $quizobj, $timenow, $canignoretimelimits) { + public static function make(quiz_settings $quizobj, $timenow, $canignoretimelimits) { // This rule is always used, even if the quiz has no open or close date. return new self($quizobj, $timenow); } diff --git a/mod/quiz/accessrule/openclosedate/tests/rule_test.php b/mod/quiz/accessrule/openclosedate/tests/rule_test.php index 5cfd2801db0..afa7940abd4 100644 --- a/mod/quiz/accessrule/openclosedate/tests/rule_test.php +++ b/mod/quiz/accessrule/openclosedate/tests/rule_test.php @@ -16,7 +16,7 @@ namespace quizaccess_openclosedate; -use quiz; +use mod_quiz\quiz_settings; use quizaccess_openclosedate; defined('MOODLE_INTERNAL') || die(); @@ -41,7 +41,7 @@ class rule_test extends \basic_testcase { $quiz->overduehandling = 'autosubmit'; $cm = new \stdClass(); $cm->id = 0; - $quizobj = new quiz($quiz, $cm, null); + $quizobj = new quiz_settings($quiz, $cm, null); $attempt = new \stdClass(); $attempt->preview = 0; @@ -68,7 +68,7 @@ class rule_test extends \basic_testcase { $quiz->overduehandling = 'autosubmit'; $cm = new \stdClass(); $cm->id = 0; - $quizobj = new quiz($quiz, $cm, null); + $quizobj = new quiz_settings($quiz, $cm, null); $attempt = new \stdClass(); $attempt->preview = 0; @@ -95,7 +95,7 @@ class rule_test extends \basic_testcase { $quiz->overduehandling = 'autosubmit'; $cm = new \stdClass(); $cm->id = 0; - $quizobj = new quiz($quiz, $cm, null); + $quizobj = new quiz_settings($quiz, $cm, null); $attempt = new \stdClass(); $attempt->preview = 0; @@ -129,7 +129,7 @@ class rule_test extends \basic_testcase { $quiz->overduehandling = 'autosubmit'; $cm = new \stdClass(); $cm->id = 0; - $quizobj = new quiz($quiz, $cm, null); + $quizobj = new quiz_settings($quiz, $cm, null); $attempt = new \stdClass(); $attempt->preview = 0; @@ -170,7 +170,7 @@ class rule_test extends \basic_testcase { $quiz->graceperiod = 1000; $cm = new \stdClass(); $cm->id = 0; - $quizobj = new quiz($quiz, $cm, null); + $quizobj = new quiz_settings($quiz, $cm, null); $attempt = new \stdClass(); $attempt->preview = 0; diff --git a/mod/quiz/accessrule/password/rule.php b/mod/quiz/accessrule/password/rule.php index 7c8c3f7cb99..956fc7b8365 100644 --- a/mod/quiz/accessrule/password/rule.php +++ b/mod/quiz/accessrule/password/rule.php @@ -16,6 +16,7 @@ use mod_quiz\form\preflight_check_form; use mod_quiz\local\access_rule_base; +use mod_quiz\quiz_settings; /** * A rule implementing the password check. @@ -26,7 +27,7 @@ use mod_quiz\local\access_rule_base; */ class quizaccess_password extends access_rule_base { - public static function make(quiz $quizobj, $timenow, $canignoretimelimits) { + public static function make(quiz_settings $quizobj, $timenow, $canignoretimelimits) { if (empty($quizobj->get_quiz()->password)) { return null; } diff --git a/mod/quiz/accessrule/password/tests/rule_test.php b/mod/quiz/accessrule/password/tests/rule_test.php index 0c5b6eace19..fb4c779e706 100644 --- a/mod/quiz/accessrule/password/tests/rule_test.php +++ b/mod/quiz/accessrule/password/tests/rule_test.php @@ -16,7 +16,7 @@ namespace quizaccess_password; -use quiz; +use mod_quiz\quiz_settings; use quizaccess_password; defined('MOODLE_INTERNAL') || die(); @@ -39,7 +39,7 @@ class rule_test extends \basic_testcase { $quiz->password = 'frog'; $cm = new \stdClass(); $cm->id = 0; - $quizobj = new quiz($quiz, $cm, null); + $quizobj = new quiz_settings($quiz, $cm, null); $rule = new quizaccess_password($quizobj, 0); $attempt = new \stdClass(); diff --git a/mod/quiz/accessrule/seb/classes/access_manager.php b/mod/quiz/accessrule/seb/classes/access_manager.php index 58f47b10205..446e61ad0df 100644 --- a/mod/quiz/accessrule/seb/classes/access_manager.php +++ b/mod/quiz/accessrule/seb/classes/access_manager.php @@ -29,7 +29,7 @@ namespace quizaccess_seb; use context_module; -use quiz; +use mod_quiz\quiz_settings; defined('MOODLE_INTERNAL') || die(); @@ -47,7 +47,7 @@ class access_manager { /** Header sent by Safe Exam Browser containing the Browser Exam Key hash. */ private const BROWSER_EXAM_KEY_HEADER = 'HTTP_X_SAFEEXAMBROWSER_REQUESTHASH'; - /** @var quiz $quiz A quiz object containing all information pertaining to current quiz. */ + /** @var quiz_settings $quiz A quiz object containing all information pertaining to current quiz. */ private $quiz; /** @var quiz_settings $quizsettings A quiz settings persistent object containing plugin settings */ @@ -62,9 +62,9 @@ class access_manager { /** * The access_manager constructor. * - * @param quiz $quiz The details of the quiz. + * @param quiz_settings $quiz The details of the quiz. */ - public function __construct(quiz $quiz) { + public function __construct(quiz_settings $quiz) { $this->quiz = $quiz; $this->context = context_module::instance($quiz->get_cmid()); $this->quizsettings = quiz_settings::get_by_quiz_id($quiz->get_quizid()); @@ -219,9 +219,9 @@ class access_manager { /** * Getter for the quiz object. * - * @return quiz + * @return \mod_quiz\quiz_settings */ - public function get_quiz() : quiz { + public function get_quiz() : quiz_settings { return $this->quiz; } diff --git a/mod/quiz/accessrule/seb/classes/external/validate_quiz_keys.php b/mod/quiz/accessrule/seb/classes/external/validate_quiz_keys.php index 1ec74bb7405..a5843805a1b 100644 --- a/mod/quiz/accessrule/seb/classes/external/validate_quiz_keys.php +++ b/mod/quiz/accessrule/seb/classes/external/validate_quiz_keys.php @@ -25,11 +25,10 @@ use external_function_parameters; use external_single_structure; use external_value; use invalid_parameter_exception; -use quiz; +use mod_quiz\quiz_settings; use quizaccess_seb\event\access_prevented; use quizaccess_seb\access_manager; -require_once($CFG->dirroot . '/mod/quiz/attemptlib.php'); require_once($CFG->libdir . '/externallib.php'); /** @@ -96,7 +95,7 @@ class validate_quiz_keys extends external_api { $result = ['configkey' => true, 'browserexamkey' => true]; - $accessmanager = new access_manager(quiz::create($quizid)); + $accessmanager = new access_manager(quiz_settings::create($quizid)); // Check if there is a valid config key. if (!$accessmanager->validate_config_key($configkey, $url)) { diff --git a/mod/quiz/accessrule/seb/classes/quiz_settings.php b/mod/quiz/accessrule/seb/classes/quiz_settings.php index 923fdb33d6d..4e87f11bec1 100644 --- a/mod/quiz/accessrule/seb/classes/quiz_settings.php +++ b/mod/quiz/accessrule/seb/classes/quiz_settings.php @@ -567,7 +567,7 @@ class quiz_settings extends persistent { } /** - * Sets the quitURL if found in the quiz_settings. + * Sets the quitURL if found in the mod_quiz\quiz_settings. */ private function process_quit_url_from_settings() { $settings = $this->to_record(); diff --git a/mod/quiz/accessrule/seb/rule.php b/mod/quiz/accessrule/seb/rule.php index efd8eef516d..b112fa34c47 100644 --- a/mod/quiz/accessrule/seb/rule.php +++ b/mod/quiz/accessrule/seb/rule.php @@ -38,11 +38,11 @@ class quizaccess_seb extends access_rule_base { /** * Create an instance of this rule for a particular quiz. * - * @param quiz $quizobj information about the quiz in question. + * @param \mod_quiz\quiz_settings $quizobj information about the quiz in question. * @param int $timenow the time that should be considered as 'now'. * @param access_manager $accessmanager the quiz accessmanager. */ - public function __construct(quiz $quizobj, int $timenow, access_manager $accessmanager) { + public function __construct(\mod_quiz\quiz_settings $quizobj, int $timenow, access_manager $accessmanager) { parent::__construct($quizobj, $timenow); $this->accessmanager = $accessmanager; } @@ -51,13 +51,13 @@ class quizaccess_seb extends access_rule_base { * Return an appropriately configured instance of this rule, if it is applicable * to the given quiz, otherwise return null. * - * @param quiz $quizobj information about the quiz in question. + * @param \mod_quiz\quiz_settings $quizobj information about the quiz in question. * @param int $timenow the time that should be considered as 'now'. * @param bool $canignoretimelimits whether the current user is exempt from * time limits by the mod/quiz:ignoretimelimits capability. * @return access_rule_base|null the rule, if applicable, else null. */ - public static function make(quiz $quizobj, $timenow, $canignoretimelimits) { + public static function make(\mod_quiz\quiz_settings $quizobj, $timenow, $canignoretimelimits) { $accessmanager = new access_manager($quizobj); // If Safe Exam Browser is not required, this access rule is not applicable. if (!$accessmanager->seb_required()) { @@ -218,7 +218,7 @@ class quizaccess_seb extends access_rule_base { /** * Return the bits of SQL needed to load all the settings from all the access * plugins in one DB query. The easiest way to understand what you need to do - * here is probably to read the code of {@see \mod\quiz\access_manager::load_settings()}. + * here is probably to read the code of {@see \mod_quiz\access_manager::load_settings()}. * * If you have some settings that cannot be loaded in this way, then you can * use the {@link get_extra_settings()} method instead, but that has diff --git a/mod/quiz/accessrule/seb/tests/event/events_test.php b/mod/quiz/accessrule/seb/tests/event/events_test.php index f96c951d974..d5a579fd901 100644 --- a/mod/quiz/accessrule/seb/tests/event/events_test.php +++ b/mod/quiz/accessrule/seb/tests/event/events_test.php @@ -16,7 +16,7 @@ namespace quizaccess_seb\event; -use quiz; +use mod_quiz\quiz_settings; defined('MOODLE_INTERNAL') || die(); @@ -53,7 +53,7 @@ class events_test extends \advanced_testcase { $this->setAdminUser(); $quiz = $this->create_test_quiz($this->course, \quizaccess_seb\settings_provider::USE_SEB_CONFIG_MANUALLY); - $accessmanager = new \quizaccess_seb\access_manager(new quiz($quiz, + $accessmanager = new \quizaccess_seb\access_manager(new quiz_settings($quiz, get_coursemodule_from_id('quiz', $quiz->cmid), $this->course)); // Set up event with data. @@ -103,7 +103,7 @@ class events_test extends \advanced_testcase { $this->setAdminUser(); $quiz = $this->create_test_quiz($this->course, \quizaccess_seb\settings_provider::USE_SEB_CONFIG_MANUALLY); - $accessmanager = new \quizaccess_seb\access_manager(new quiz($quiz, + $accessmanager = new \quizaccess_seb\access_manager(new quiz_settings($quiz, get_coursemodule_from_id('quiz', $quiz->cmid), $this->course)); // Set up event with data. diff --git a/mod/quiz/accessrule/seb/tests/quiz_settings_test.php b/mod/quiz/accessrule/seb/tests/quiz_settings_test.php index 57335a77f8d..94f8e2c2465 100644 --- a/mod/quiz/accessrule/seb/tests/quiz_settings_test.php +++ b/mod/quiz/accessrule/seb/tests/quiz_settings_test.php @@ -21,7 +21,7 @@ defined('MOODLE_INTERNAL') || die(); require_once(__DIR__ . '/test_helper_trait.php'); /** - * PHPUnit tests for quiz_settings class. + * PHPUnit tests for mod_quiz\quiz_settings class. * * @package quizaccess_seb * @author Andrew Madden @@ -748,7 +748,7 @@ class quiz_settings_test extends \advanced_testcase { } /** - * Test that we can get quiz_settings by quiz id. + * Test that we can get mod_quiz\quiz_settings by quiz id. */ public function test_get_quiz_settings_by_quiz_id() { $expected = quiz_settings::get_record(['quizid' => $this->quiz->id]); diff --git a/mod/quiz/accessrule/seb/tests/test_helper_trait.php b/mod/quiz/accessrule/seb/tests/test_helper_trait.php index 564f916bd5f..aca36de4408 100644 --- a/mod/quiz/accessrule/seb/tests/test_helper_trait.php +++ b/mod/quiz/accessrule/seb/tests/test_helper_trait.php @@ -191,7 +191,7 @@ trait quizaccess_seb_test_helper_trait { $this->setUser($user); $starttime = time(); - $quizobj = \quiz::create($quiz->id, $user->id); + $quizobj = mod_quiz\quiz_settings::create($quiz->id, $user->id); $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); @@ -242,7 +242,7 @@ trait quizaccess_seb_test_helper_trait { * @return \quizaccess_seb\access_manager */ protected function get_access_manager() { - return new access_manager(new \quiz($this->quiz, + return new access_manager(new mod_quiz\quiz_settings($this->quiz, get_coursemodule_from_id('quiz', $this->quiz->cmid), $this->course)); } @@ -253,7 +253,7 @@ trait quizaccess_seb_test_helper_trait { */ protected function make_rule() { return \quizaccess_seb::make( - new \quiz($this->quiz, get_coursemodule_from_id('quiz', $this->quiz->cmid), $this->course), + new mod_quiz\quiz_settings($this->quiz, get_coursemodule_from_id('quiz', $this->quiz->cmid), $this->course), 0, true ); diff --git a/mod/quiz/accessrule/securewindow/rule.php b/mod/quiz/accessrule/securewindow/rule.php index 75f8e2c4aa0..6aa636b6b82 100644 --- a/mod/quiz/accessrule/securewindow/rule.php +++ b/mod/quiz/accessrule/securewindow/rule.php @@ -15,6 +15,7 @@ // along with Moodle. If not, see . use mod_quiz\local\access_rule_base; +use mod_quiz\quiz_settings; /** * A rule for ensuring that the quiz is opened in a popup, with some JavaScript @@ -40,7 +41,7 @@ class quizaccess_securewindow extends access_rule_base { 'menubar' => false, ); - public static function make(quiz $quizobj, $timenow, $canignoretimelimits) { + public static function make(quiz_settings $quizobj, $timenow, $canignoretimelimits) { if ($quizobj->get_quiz()->browsersecurity !== 'securewindow') { return null; diff --git a/mod/quiz/accessrule/securewindow/tests/rule_test.php b/mod/quiz/accessrule/securewindow/tests/rule_test.php index 8ec234c97b1..cfc5117e46f 100644 --- a/mod/quiz/accessrule/securewindow/tests/rule_test.php +++ b/mod/quiz/accessrule/securewindow/tests/rule_test.php @@ -16,7 +16,7 @@ namespace quizaccess_securewindow; -use quiz; +use mod_quiz\quiz_settings; use quizaccess_securewindow; defined('MOODLE_INTERNAL') || die(); @@ -42,7 +42,7 @@ class rule_test extends \basic_testcase { $quiz->browsersecurity = 'securewindow'; $cm = new \stdClass(); $cm->id = 0; - $quizobj = new quiz($quiz, $cm, null); + $quizobj = new quiz_settings($quiz, $cm, null); $rule = new quizaccess_securewindow($quizobj, 0); $attempt = new \stdClass(); diff --git a/mod/quiz/accessrule/timelimit/rule.php b/mod/quiz/accessrule/timelimit/rule.php index 806d5591a7a..610c384a4a5 100644 --- a/mod/quiz/accessrule/timelimit/rule.php +++ b/mod/quiz/accessrule/timelimit/rule.php @@ -16,6 +16,7 @@ use mod_quiz\form\preflight_check_form; use mod_quiz\local\access_rule_base; +use mod_quiz\quiz_settings; /** * A rule representing the time limit. @@ -29,7 +30,7 @@ use mod_quiz\local\access_rule_base; */ class quizaccess_timelimit extends access_rule_base { - public static function make(quiz $quizobj, $timenow, $canignoretimelimits) { + public static function make(quiz_settings $quizobj, $timenow, $canignoretimelimits) { if (empty($quizobj->get_quiz()->timelimit) || $canignoretimelimits) { return null; diff --git a/mod/quiz/accessrule/timelimit/tests/rule_test.php b/mod/quiz/accessrule/timelimit/tests/rule_test.php index 36a075bc841..ab04271c2ce 100644 --- a/mod/quiz/accessrule/timelimit/tests/rule_test.php +++ b/mod/quiz/accessrule/timelimit/tests/rule_test.php @@ -16,7 +16,7 @@ namespace quizaccess_timelimit; -use quiz; +use mod_quiz\quiz_settings; use quizaccess_timelimit; defined('MOODLE_INTERNAL') || die(); @@ -39,7 +39,7 @@ class rule_test extends \basic_testcase { $quiz->timelimit = 3600; $cm = new \stdClass(); $cm->id = 0; - $quizobj = new quiz($quiz, $cm, null); + $quizobj = new quiz_settings($quiz, $cm, null); $rule = new quizaccess_timelimit($quizobj, 10000); $attempt = new \stdClass(); @@ -88,7 +88,7 @@ class rule_test extends \basic_testcase { $quiz->timelimit = $timelimit; $cm = new \stdClass(); $cm->id = 0; - $quizobj = new quiz($quiz, $cm, null); + $quizobj = new quiz_settings($quiz, $cm, null); $rule = new quizaccess_timelimit($quizobj, $timenow); $attempt = new \stdClass(); diff --git a/mod/quiz/attemptlib.php b/mod/quiz/attemptlib.php index a5c61f52f04..90919b9f7ee 100644 --- a/mod/quiz/attemptlib.php +++ b/mod/quiz/attemptlib.php @@ -14,543 +14,13 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Back-end code for handling data about quizzes and the current user's attempt. - * - * There are classes for loading all the information about a quiz and attempts, - * and for displaying the navigation panel. - * - * @package mod_quiz - * @copyright 2008 onwards Tim Hunt - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ +/** + * File only retained to prevent fatal errors in code that tries to require/include this. + * + * @todo MDL-76612 delete this file as part of Moodle 4.6 development. + * @deprecated This file is no longer required in Moodle 4.2+. + */ defined('MOODLE_INTERNAL') || die(); -use mod_quiz\access_manager; -use mod_quiz\output\links_to_other_attempts; -use mod_quiz\output\navigation_panel_base; -use mod_quiz\output\renderer; -use mod_quiz\question\bank\qbank_helper; -use mod_quiz\question\display_options; -use mod_quiz\quiz_attempt; - -/** - * A class encapsulating a quiz and the questions it contains, and making the - * information available to scripts like view.php. - * - * Initially, it only loads a minimal amout of information about each question - loading - * extra information only when necessary or when asked. The class tracks which questions - * are loaded. - * - * @copyright 2008 Tim Hunt - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @since Moodle 2.0 - */ -class quiz { - /** @var stdClass the course settings from the database. */ - protected $course; - /** @var stdClass the course_module settings from the database. */ - protected $cm; - /** @var stdClass the quiz settings from the database. */ - protected $quiz; - /** @var context the quiz context. */ - protected $context; - - /** - * @var stdClass[] of questions augmented with slot information. For non-random - * questions, the array key is question id. For random quesions it is 's' . $slotid. - * probalby best to use ->questionid field of the object instead. - */ - protected $questions = null; - /** @var stdClass[] of quiz_section rows. */ - protected $sections = null; - /** @var access_manager the access manager for this quiz. */ - protected $accessmanager = null; - /** @var bool whether the current user has capability mod/quiz:preview. */ - protected $ispreviewuser = null; - - // Constructor ============================================================= - /** - * Constructor, assuming we already have the necessary data loaded. - * - * @param object $quiz the row from the quiz table. - * @param object $cm the course_module object for this quiz. - * @param object $course the row from the course table for the course we belong to. - * @param bool $getcontext intended for testing - stops the constructor getting the context. - */ - public function __construct($quiz, $cm, $course, $getcontext = true) { - $this->quiz = $quiz; - $this->cm = $cm; - $this->quiz->cmid = $this->cm->id; - $this->course = $course; - if ($getcontext && !empty($cm->id)) { - $this->context = context_module::instance($cm->id); - } - } - - /** - * Static function to create a new quiz object for a specific user. - * - * @param int $quizid the the quiz id. - * @param int|null $userid the the userid (optional). If passed, relevant overrides are applied. - * @return quiz the new quiz object. - */ - public static function create($quizid, $userid = null) { - global $DB; - - $quiz = access_manager::load_quiz_and_settings($quizid); - $course = $DB->get_record('course', array('id' => $quiz->course), '*', MUST_EXIST); - $cm = get_coursemodule_from_instance('quiz', $quiz->id, $course->id, false, MUST_EXIST); - - // Update quiz with override information. - if ($userid) { - $quiz = quiz_update_effective_access($quiz, $userid); - } - - return new quiz($quiz, $cm, $course); - } - - /** - * Create a {@see quiz_attempt} for an attempt at this quiz. - * - * @param object $attemptdata row from the quiz_attempts table. - * @return quiz_attempt the new quiz_attempt object. - */ - public function create_attempt_object($attemptdata) { - return new quiz_attempt($attemptdata, $this->quiz, $this->cm, $this->course); - } - - // Functions for loading more data ========================================= - - /** - * Load just basic information about all the questions in this quiz. - */ - public function preload_questions() { - $slots = qbank_helper::get_question_structure($this->quiz->id, $this->context); - $this->questions = []; - foreach ($slots as $slot) { - $this->questions[$slot->questionid] = $slot; - } - } - - /** - * Fully load some or all of the questions for this quiz. You must call - * {@link preload_questions()} first. - * - * @param array|null $deprecated no longer supported (it was not used). - */ - public function load_questions($deprecated = null) { - if ($deprecated !== null) { - debugging('The argument to quiz::load_questions is no longer supported. ' . - 'All questions are always loaded.', DEBUG_DEVELOPER); - } - if ($this->questions === null) { - throw new coding_exception('You must call preload_questions before calling load_questions.'); - } - - $questionstoprocess = []; - foreach ($this->questions as $question) { - if (is_number($question->questionid)) { - $question->id = $question->questionid; - $questionstoprocess[$question->questionid] = $question; - } - } - get_question_options($questionstoprocess); - } - - /** - * Get an instance of the {@link \mod_quiz\structure} class for this quiz. - * @return \mod_quiz\structure describes the questions in the quiz. - */ - public function get_structure() { - return \mod_quiz\structure::create_for_quiz($this); - } - - // Simple getters ========================================================== - /** - * Get the id of the course this quiz belongs to. - * - * @return int the course id. - */ - public function get_courseid() { - return $this->course->id; - } - - /** - * Get the course settings object that this quiz belongs to. - * - * @return object the row of the course table. - */ - public function get_course() { - return $this->course; - } - - /** - * Get this quiz's id (in the quiz table). - * - * @return int the quiz id. - */ - public function get_quizid() { - return $this->quiz->id; - } - - /** - * Get the quiz settings object. - * - * @return stdClass the row of the quiz table. - */ - public function get_quiz() { - return $this->quiz; - } - - /** - * Get the quiz name. - * - * @return string the name of this quiz. - */ - public function get_quiz_name() { - return $this->quiz->name; - } - - /** - * Get the navigation method in use. - * - * @return int QUIZ_NAVMETHOD_FREE or QUIZ_NAVMETHOD_SEQ. - */ - public function get_navigation_method() { - return $this->quiz->navmethod; - } - - /** @return int the number of attempts allowed at this quiz (0 = infinite). */ - public function get_num_attempts_allowed() { - return $this->quiz->attempts; - } - - /** - * Get the course-module id for this quiz. - * - * @return int the course_module id. - */ - public function get_cmid() { - return $this->cm->id; - } - - /** - * Get the course-module object for this quiz. - * - * @return object the course_module object. - */ - public function get_cm() { - return $this->cm; - } - - /** - * Get the quiz context. - * - * @return context_module the module context for this quiz. - */ - public function get_context() { - return $this->context; - } - - /** - * @return bool whether the current user is someone who previews the quiz, - * rather than attempting it. - */ - public function is_preview_user() { - if (is_null($this->ispreviewuser)) { - $this->ispreviewuser = has_capability('mod/quiz:preview', $this->context); - } - return $this->ispreviewuser; - } - - /** - * Checks user enrollment in the current course. - * - * @param int $userid the id of the user to check. - * @return bool whether the user is enrolled. - */ - public function is_participant($userid) { - return is_enrolled($this->get_context(), $userid, 'mod/quiz:attempt', $this->show_only_active_users()); - } - - /** - * Check is only active users in course should be shown. - * - * @return bool true if only active users should be shown. - */ - public function show_only_active_users() { - return !has_capability('moodle/course:viewsuspendedusers', $this->get_context()); - } - - /** - * @return bool whether any questions have been added to this quiz. - */ - public function has_questions() { - if ($this->questions === null) { - $this->preload_questions(); - } - return !empty($this->questions); - } - - /** - * @param int $id the question id. - * @return stdClass the question object with that id. - */ - public function get_question($id) { - return $this->questions[$id]; - } - - /** - * @param array|null $questionids question ids of the questions to load. null for all. - * @return stdClass[] the question data objects. - */ - public function get_questions($questionids = null) { - if (is_null($questionids)) { - $questionids = array_keys($this->questions); - } - $questions = array(); - foreach ($questionids as $id) { - if (!array_key_exists($id, $this->questions)) { - throw new moodle_exception('cannotstartmissingquestion', 'quiz', $this->view_url()); - } - $questions[$id] = $this->questions[$id]; - $this->ensure_question_loaded($id); - } - return $questions; - } - - /** - * Get all the sections in this quiz. - * - * @return array 0, 1, 2, ... => quiz_sections row from the database. - */ - public function get_sections() { - global $DB; - if ($this->sections === null) { - $this->sections = array_values($DB->get_records('quiz_sections', - array('quizid' => $this->get_quizid()), 'firstslot')); - } - return $this->sections; - } - - /** - * Return access_manager and instance of the access_manager class - * for this quiz at this time. - * - * @param int $timenow the current time as a unix timestamp. - * @return access_manager and instance of the access_manager class - * for this quiz at this time. - */ - public function get_access_manager($timenow) { - if (is_null($this->accessmanager)) { - $this->accessmanager = new access_manager($this, $timenow, - has_capability('mod/quiz:ignoretimelimits', $this->context, null, false)); - } - return $this->accessmanager; - } - - /** - * Wrapper round the has_capability funciton that automatically passes in the quiz context. - * - * @param string $capability the name of the capability to check. For example mod/quiz:view. - * @param int|null $userid A user id. By default (null) checks the permissions of the current user. - * @param bool $doanything If false, ignore effect of admin role assignment. - * @return boolean true if the user has this capability. Otherwise false. - */ - public function has_capability($capability, $userid = null, $doanything = true) { - return has_capability($capability, $this->context, $userid, $doanything); - } - - /** - * Wrapper round the require_capability function that automatically passes in the quiz context. - * - * @param string $capability the name of the capability to check. For example mod/quiz:view. - * @param int|null $userid A user id. By default (null) checks the permissions of the current user. - * @param bool $doanything If false, ignore effect of admin role assignment. - */ - public function require_capability($capability, $userid = null, $doanything = true) { - require_capability($capability, $this->context, $userid, $doanything); - } - - // URLs related to this attempt ============================================ - /** - * @return string the URL of this quiz's view page. - */ - public function view_url() { - global $CFG; - return $CFG->wwwroot . '/mod/quiz/view.php?id=' . $this->cm->id; - } - - /** - * @return string the URL of this quiz's edit page. - */ - public function edit_url() { - global $CFG; - return $CFG->wwwroot . '/mod/quiz/edit.php?cmid=' . $this->cm->id; - } - - /** - * @param int $attemptid the id of an attempt. - * @param int $page optional page number to go to in the attempt. - * @return string the URL of that attempt. - */ - public function attempt_url($attemptid, $page = 0) { - global $CFG; - $url = $CFG->wwwroot . '/mod/quiz/attempt.php?attempt=' . $attemptid; - if ($page) { - $url .= '&page=' . $page; - } - $url .= '&cmid=' . $this->get_cmid(); - return $url; - } - - /** - * Get the URL to start/continue an attempt. - * - * @param int $page page in the attempt to start on (optional). - * @return moodle_url the URL of this quiz's edit page. Needs to be POSTed to with a cmid parameter. - */ - public function start_attempt_url($page = 0) { - $params = array('cmid' => $this->cm->id, 'sesskey' => sesskey()); - if ($page) { - $params['page'] = $page; - } - return new moodle_url('/mod/quiz/startattempt.php', $params); - } - - /** - * @param int $attemptid the id of an attempt. - * @return string the URL of the review of that attempt. - */ - public function review_url($attemptid) { - return new moodle_url('/mod/quiz/review.php', array('attempt' => $attemptid, 'cmid' => $this->get_cmid())); - } - - /** - * @param int $attemptid the id of an attempt. - * @return string the URL of the review of that attempt. - */ - public function summary_url($attemptid) { - return new moodle_url('/mod/quiz/summary.php', array('attempt' => $attemptid, 'cmid' => $this->get_cmid())); - } - - // Bits of content ========================================================= - - /** - * @param bool $notused not used. - * @return string an empty string. - * @deprecated since 3.1. This sort of functionality is now entirely handled by quiz access rules. - */ - public function confirm_start_attempt_message($notused) { - debugging('confirm_start_attempt_message is deprecated. ' . - 'This sort of functionality is now entirely handled by quiz access rules.'); - return ''; - } - - /** - * If $reviewoptions->attempt is false, meaning that students can't review this - * attempt at the moment, return an appropriate string explaining why. - * - * @param int $when One of the display_options::DURING, - * IMMEDIATELY_AFTER, LATER_WHILE_OPEN or AFTER_CLOSE constants. - * @param bool $short if true, return a shorter string. - * @return string an appropraite message. - */ - public function cannot_review_message($when, $short = false) { - - if ($short) { - $langstrsuffix = 'short'; - $dateformat = get_string('strftimedatetimeshort', 'langconfig'); - } else { - $langstrsuffix = ''; - $dateformat = ''; - } - - if ($when == display_options::DURING || - $when == display_options::IMMEDIATELY_AFTER) { - return ''; - } else if ($when == display_options::LATER_WHILE_OPEN && $this->quiz->timeclose && - $this->quiz->reviewattempt & display_options::AFTER_CLOSE) { - return get_string('noreviewuntil' . $langstrsuffix, 'quiz', - userdate($this->quiz->timeclose, $dateformat)); - } else { - return get_string('noreview' . $langstrsuffix, 'quiz'); - } - } - - /** - * Probably not used any more, but left for backwards compatibility. - * - * @param string $title the name of this particular quiz page. - * @return string always returns ''. - */ - public function navigation($title) { - global $PAGE; - $PAGE->navbar->add($title); - return ''; - } - - // Private methods ========================================================= - /** - * Check that the definition of a particular question is loaded, and if not throw an exception. - * - * @param int $id a question id. - */ - protected function ensure_question_loaded($id) { - if (isset($this->questions[$id]->_partiallyloaded)) { - throw new moodle_exception('questionnotloaded', 'quiz', $this->view_url(), $id); - } - } - - /** - * Return all the question types used in this quiz. - * - * @param boolean $includepotential if the quiz include random questions, - * setting this flag to true will make the function to return all the - * possible question types in the random questions category. - * @return array a sorted array including the different question types. - * @since Moodle 3.1 - */ - public function get_all_question_types_used($includepotential = false) { - $questiontypes = array(); - - // To control if we need to look in categories for questions. - $qcategories = array(); - - foreach ($this->get_questions() as $questiondata) { - if ($questiondata->qtype === 'random' && $includepotential) { - if (!isset($qcategories[$questiondata->category])) { - $qcategories[$questiondata->category] = false; - } - if (!empty($questiondata->filtercondition)) { - $filtercondition = json_decode($questiondata->filtercondition); - $qcategories[$questiondata->category] = !empty($filtercondition->includingsubcategories); - } - } else { - if (!in_array($questiondata->qtype, $questiontypes)) { - $questiontypes[] = $questiondata->qtype; - } - } - } - - if (!empty($qcategories)) { - // We have to look for all the question types in these categories. - $categoriestolook = array(); - foreach ($qcategories as $cat => $includesubcats) { - if ($includesubcats) { - $categoriestolook = array_merge($categoriestolook, question_categorylist($cat)); - } else { - $categoriestolook[] = $cat; - } - } - $questiontypesincategories = question_bank::get_all_question_types_in_categories($categoriestolook); - $questiontypes = array_merge($questiontypes, $questiontypesincategories); - } - $questiontypes = array_unique($questiontypes); - sort($questiontypes); - - return $questiontypes; - } -} +debugging('This file is no longer required in Moodle 4.2+. Please do not include/require it.', DEBUG_DEVELOPER); diff --git a/mod/quiz/classes/access_manager.php b/mod/quiz/classes/access_manager.php index 8ba515df7af..7e4ddb9e0a3 100644 --- a/mod/quiz/classes/access_manager.php +++ b/mod/quiz/classes/access_manager.php @@ -25,7 +25,6 @@ use mod_quiz\output\renderer; use moodle_page; use moodle_url; use MoodleQuickForm; -use quiz; use stdClass; /** @@ -40,7 +39,7 @@ use stdClass; * @since Moodle 2.2 */ class access_manager { - /** @var quiz the quiz settings object. */ + /** @var quiz_settings the quiz settings object. */ protected $quizobj; /** @var int the time to be considered as 'now'. */ @@ -52,13 +51,13 @@ class access_manager { /** * Create an instance for a particular quiz. * - * @param quiz $quizobj An instance of the class quiz from attemptlib.php. + * @param quiz_settings $quizobj the quiz settings. * The quiz we will be controlling access to. * @param int $timenow The time to use as 'now'. * @param bool $canignoretimelimits Whether this user is exempt from time * limits (has_capability('mod/quiz:ignoretimelimits', ...)). */ - public function __construct(quiz $quizobj, int $timenow, bool $canignoretimelimits) { + public function __construct(quiz_settings $quizobj, int $timenow, bool $canignoretimelimits) { $this->quizobj = $quizobj; $this->timenow = $timenow; $this->rules = $this->make_rules($quizobj, $timenow, $canignoretimelimits); @@ -67,13 +66,13 @@ class access_manager { /** * Make all the rules relevant to a particular quiz. * - * @param quiz $quizobj information about the quiz in question. + * @param quiz_settings $quizobj information about the quiz in question. * @param int $timenow the time that should be considered as 'now'. * @param bool $canignoretimelimits whether the current user is exempt from * time limits by the mod/quiz:ignoretimelimits capability. * @return access_rule_base[] rules that apply to this quiz. */ - protected function make_rules(quiz $quizobj, int $timenow, bool $canignoretimelimits): array { + protected function make_rules(quiz_settings $quizobj, int $timenow, bool $canignoretimelimits): array { $rules = []; foreach (self::get_rule_classes() as $ruleclass) { diff --git a/mod/quiz/classes/completion/custom_completion.php b/mod/quiz/classes/completion/custom_completion.php index 3921c02e37e..43a7f7c72ee 100644 --- a/mod/quiz/classes/completion/custom_completion.php +++ b/mod/quiz/classes/completion/custom_completion.php @@ -20,7 +20,7 @@ namespace mod_quiz\completion; use context_module; use core_completion\activity_custom_completion; -use quiz; +use mod_quiz\quiz_settings; use mod_quiz\access_manager; /** @@ -69,7 +69,7 @@ class custom_completion extends activity_custom_completion { } $lastfinishedattempt = end($attempts); $context = context_module::instance($this->cm->id); - $quizobj = quiz::create($this->cm->instance, $this->userid); + $quizobj = quiz_settings::create($this->cm->instance, $this->userid); $accessmanager = new access_manager( $quizobj, time(), diff --git a/mod/quiz/classes/external.php b/mod/quiz/classes/external.php index da33c5da4f3..26bf39486fc 100644 --- a/mod/quiz/classes/external.php +++ b/mod/quiz/classes/external.php @@ -27,6 +27,7 @@ use core_course\external\helper_for_get_mods_by_courses; use mod_quiz\access_manager; use mod_quiz\quiz_attempt; +use mod_quiz\quiz_settings; defined('MOODLE_INTERNAL') || die; @@ -113,7 +114,7 @@ class mod_quiz_external extends external_api { $quizdetails['hasfeedback'] = (!empty($hasfeedback)) ? 1 : 0; $timenow = time(); - $quizobj = quiz::create($quiz->id, $USER->id); + $quizobj = quiz_settings::create($quiz->id, $USER->id); $accessmanager = new access_manager($quizobj, $timenow, has_capability('mod/quiz:ignoretimelimits', $context, null, false)); @@ -727,7 +728,7 @@ class mod_quiz_external extends external_api { list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']); - $quizobj = quiz::create($cm->instance, $USER->id); + $quizobj = quiz_settings::create($cm->instance, $USER->id); // Check questions. if (!$quizobj->has_questions()) { @@ -1798,7 +1799,7 @@ class mod_quiz_external extends external_api { $result['canviewreports'] = has_capability('mod/quiz:viewreports', $context);; // Access manager now. - $quizobj = quiz::create($cm->instance, $USER->id); + $quizobj = quiz_settings::create($cm->instance, $USER->id); $ignoretimelimits = has_capability('mod/quiz:ignoretimelimits', $context, null, false); $timenow = time(); $accessmanager = new access_manager($quizobj, $timenow, $ignoretimelimits); @@ -1883,7 +1884,7 @@ class mod_quiz_external extends external_api { } // Access manager now. - $quizobj = quiz::create($cm->instance, $USER->id); + $quizobj = quiz_settings::create($cm->instance, $USER->id); $ignoretimelimits = has_capability('mod/quiz:ignoretimelimits', $context, null, false); $timenow = time(); $accessmanager = new access_manager($quizobj, $timenow, $ignoretimelimits); @@ -1977,7 +1978,7 @@ class mod_quiz_external extends external_api { list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']); - $quizobj = quiz::create($cm->instance, $USER->id); + $quizobj = quiz_settings::create($cm->instance, $USER->id); $quizobj->preload_questions(); $quizobj->load_questions(); diff --git a/mod/quiz/classes/local/access_rule_base.php b/mod/quiz/classes/local/access_rule_base.php index 62e7d15bbb0..9ab7b85c92b 100644 --- a/mod/quiz/classes/local/access_rule_base.php +++ b/mod/quiz/classes/local/access_rule_base.php @@ -20,7 +20,7 @@ use mod_quiz\form\preflight_check_form; use mod_quiz_mod_form; use moodle_page; use MoodleQuickForm; -use quiz; +use mod_quiz\quiz_settings; use stdClass; defined('MOODLE_INTERNAL') || die(); @@ -47,14 +47,15 @@ require_once($CFG->dirroot . '/mod/quiz/locallib.php'); abstract class access_rule_base { /** @var stdClass the quiz settings. */ protected $quiz; - /** @var quiz the quiz object. */ + /** @var quiz_settings the quiz object. */ protected $quizobj; /** @var int the time to use as 'now'. */ protected $timenow; /** * Create an instance of this rule for a particular quiz. - * @param quiz $quizobj information about the quiz in question. + * + * @param quiz_settings $quizobj information about the quiz in question. * @param int $timenow the time that should be considered as 'now'. */ public function __construct($quizobj, $timenow) { @@ -66,13 +67,14 @@ abstract class access_rule_base { /** * Return an appropriately configured instance of this rule, if it is applicable * to the given quiz, otherwise return null. - * @param quiz $quizobj information about the quiz in question. + * + * @param quiz_settings $quizobj information about the quiz in question. * @param int $timenow the time that should be considered as 'now'. * @param bool $canignoretimelimits whether the current user is exempt from * time limits by the mod/quiz:ignoretimelimits capability. * @return self|null the rule, if applicable, else null. */ - public static function make(quiz $quizobj, $timenow, $canignoretimelimits) { + public static function make(quiz_settings $quizobj, $timenow, $canignoretimelimits) { return null; } diff --git a/mod/quiz/classes/output/edit_renderer.php b/mod/quiz/classes/output/edit_renderer.php index f46d8cae8e8..e1aa588bf42 100644 --- a/mod/quiz/classes/output/edit_renderer.php +++ b/mod/quiz/classes/output/edit_renderer.php @@ -44,14 +44,14 @@ class edit_renderer extends \plugin_renderer_base { /** * Render the edit page * - * @param \quiz $quizobj object containing all the quiz settings information. + * @param \mod_quiz\quiz_settings $quizobj object containing all the quiz settings information. * @param structure $structure object containing the structure of the quiz. * @param \core_question\local\bank\question_edit_contexts $contexts the relevant question bank contexts. * @param \moodle_url $pageurl the canonical URL of this page. * @param array $pagevars the variables from {@link question_edit_setup()}. * @return string HTML to output. */ - public function edit_page(\quiz $quizobj, structure $structure, + public function edit_page(\mod_quiz\quiz_settings $quizobj, structure $structure, \core_question\local\bank\question_edit_contexts $contexts, \moodle_url $pageurl, array $pagevars) { $output = ''; diff --git a/mod/quiz/classes/output/renderer.php b/mod/quiz/classes/output/renderer.php index 0f62095b2fa..d5a06c8187d 100644 --- a/mod/quiz/classes/output/renderer.php +++ b/mod/quiz/classes/output/renderer.php @@ -31,7 +31,7 @@ use moodle_url; use plugin_renderer_base; use popup_action; use question_display_options; -use quiz; +use mod_quiz\quiz_settings; use renderable; use single_button; use stdClass; @@ -448,11 +448,11 @@ class renderer extends plugin_renderer_base { * The student gets here if their interaction with the preflight check * from fails in some way (e.g. they typed the wrong password). * - * @param quiz $quizobj + * @param \mod_quiz\quiz_settings $quizobj * @param preflight_check_form $mform * @return string */ - public function start_attempt_page(quiz $quizobj, preflight_check_form $mform) { + public function start_attempt_page(quiz_settings $quizobj, preflight_check_form $mform) { $output = ''; $output .= $this->header(); $output .= $this->during_attempt_tertiary_nav($quizobj->view_url()); diff --git a/mod/quiz/classes/privacy/legacy_quizaccess_polyfill.php b/mod/quiz/classes/privacy/legacy_quizaccess_polyfill.php index b70c651fb04..786befdb84f 100644 --- a/mod/quiz/classes/privacy/legacy_quizaccess_polyfill.php +++ b/mod/quiz/classes/privacy/legacy_quizaccess_polyfill.php @@ -39,30 +39,30 @@ trait legacy_quizaccess_polyfill { /** * Export all user data for the specified user, for the specified quiz. * - * @param \quiz $quiz The quiz being exported + * @param \mod_quiz\quiz_settings $quiz The quiz being exported * @param \stdClass $user The user to export data for * @return \stdClass The data to be exported for this access rule. */ - public static function export_quizaccess_user_data(\quiz $quiz, \stdClass $user) : \stdClass { + public static function export_quizaccess_user_data(\mod_quiz\quiz_settings $quiz, \stdClass $user) : \stdClass { return static::_export_quizaccess_user_data($quiz, $user); } /** * Delete all data for all users in the specified quiz. * - * @param \quiz $quiz The quiz being deleted + * @param \mod_quiz\quiz_settings $quiz The quiz being deleted */ - public static function delete_quizaccess_data_for_all_users_in_context(\quiz $quiz) { + public static function delete_quizaccess_data_for_all_users_in_context(\mod_quiz\quiz_settings $quiz) { static::_delete_quizaccess_data_for_all_users_in_context($quiz); } /** * Delete all user data for the specified user, in the specified quiz. * - * @param \quiz $quiz The quiz being deleted + * @param \mod_quiz\quiz_settings $quiz The quiz being deleted * @param \stdClass $user The user to export data for */ - public static function delete_quizaccess_data_for_user(\quiz $quiz, \stdClass $user) { + public static function delete_quizaccess_data_for_user(\mod_quiz\quiz_settings $quiz, \stdClass $user) { static::_delete_quizaccess_data_for_user($quiz, $user); } diff --git a/mod/quiz/classes/privacy/provider.php b/mod/quiz/classes/privacy/provider.php index 94cbc33e3a2..c0ca490e9ab 100644 --- a/mod/quiz/classes/privacy/provider.php +++ b/mod/quiz/classes/privacy/provider.php @@ -277,7 +277,7 @@ class provider implements $quizzes = $DB->get_recordset_sql($sql, $params); foreach ($quizzes as $quiz) { list($course, $cm) = get_course_and_cm_from_cmid($quiz->cmid, 'quiz'); - $quizobj = new \quiz($quiz, $cm, $course); + $quizobj = new \mod_quiz\quiz_settings($quiz, $cm, $course); $context = $quizobj->get_context(); $quizdata = \core_privacy\local\request\helper::get_context_data($context, $contextlist->get_user()); @@ -354,7 +354,7 @@ class provider implements return; } - $quizobj = \quiz::create($cm->instance); + $quizobj = \mod_quiz\quiz_settings::create($cm->instance); $quiz = $quizobj->get_quiz(); // Handle the 'quizaccess' subplugin. @@ -393,7 +393,7 @@ class provider implements } // Fetch the details of the data to be removed. - $quizobj = \quiz::create($cm->instance); + $quizobj = \mod_quiz\quiz_settings::create($cm->instance); $quiz = $quizobj->get_quiz(); $user = $contextlist->get_user(); @@ -441,7 +441,7 @@ class provider implements return; } - $quizobj = \quiz::create($cm->instance); + $quizobj = \mod_quiz\quiz_settings::create($cm->instance); $quiz = $quizobj->get_quiz(); $userids = $userlist->get_userids(); diff --git a/mod/quiz/classes/privacy/quizaccess_provider.php b/mod/quiz/classes/privacy/quizaccess_provider.php index e18d582f298..385e80d0c9a 100644 --- a/mod/quiz/classes/privacy/quizaccess_provider.php +++ b/mod/quiz/classes/privacy/quizaccess_provider.php @@ -40,24 +40,24 @@ interface quizaccess_provider extends \core_privacy\local\request\plugin\subplug /** * Export all user data for the specified user, for the specified quiz. * - * @param \quiz $quiz The quiz being exported + * @param \mod_quiz\quiz_settings $quiz The quiz being exported * @param \stdClass $user The user to export data for * @return \stdClass The data to be exported for this access rule. */ - public static function export_quizaccess_user_data(\quiz $quiz, \stdClass $user) : \stdClass; + public static function export_quizaccess_user_data(\mod_quiz\quiz_settings $quiz, \stdClass $user) : \stdClass; /** * Delete all data for all users in the specified quiz. * - * @param \quiz $quiz The quiz being deleted + * @param \mod_quiz\quiz_settings $quiz The quiz being deleted */ - public static function delete_quizaccess_data_for_all_users_in_context(\quiz $quiz); + public static function delete_quizaccess_data_for_all_users_in_context(\mod_quiz\quiz_settings $quiz); /** * Delete all user data for the specified user, in the specified quiz. * - * @param \quiz $quiz The quiz being deleted + * @param \mod_quiz\quiz_settings $quiz The quiz being deleted * @param \stdClass $user The user to export data for */ - public static function delete_quizaccess_data_for_user(\quiz $quiz, \stdClass $user); + public static function delete_quizaccess_data_for_user(\mod_quiz\quiz_settings $quiz, \stdClass $user); } diff --git a/mod/quiz/classes/question/bank/qbank_helper.php b/mod/quiz/classes/question/bank/qbank_helper.php index b719974b756..fd6cdbf4ef8 100644 --- a/mod/quiz/classes/question/bank/qbank_helper.php +++ b/mod/quiz/classes/question/bank/qbank_helper.php @@ -23,7 +23,6 @@ use qubaid_condition; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/question/engine/bank.php'); -require_once($CFG->dirroot . '/mod/quiz/attemptlib.php'); /** * Helper class for question bank and its associated data. diff --git a/mod/quiz/classes/question/qubaids_for_users_attempts.php b/mod/quiz/classes/question/qubaids_for_users_attempts.php index 2f4c7ba1abf..9248627ca06 100644 --- a/mod/quiz/classes/question/qubaids_for_users_attempts.php +++ b/mod/quiz/classes/question/qubaids_for_users_attempts.php @@ -21,7 +21,6 @@ use mod_quiz\quiz_attempt; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/question/engine/datalib.php'); -require_once($CFG->dirroot.'/mod/quiz/attemptlib.php'); /** * A {@see qubaid_condition} representing all the attempts by one user at a given quiz. diff --git a/mod/quiz/classes/quiz_attempt.php b/mod/quiz/classes/quiz_attempt.php index b105e2c7f72..26a37db44bc 100644 --- a/mod/quiz/classes/quiz_attempt.php +++ b/mod/quiz/classes/quiz_attempt.php @@ -38,7 +38,7 @@ use question_engine; use question_out_of_sequence_exception; use question_state; use question_usage_by_activity; -use quiz; +use mod_quiz\quiz_settings; use stdClass; /** @@ -62,7 +62,7 @@ class quiz_attempt { /** @var int maximum number of slots in the quiz for the review page to default to show all. */ const MAX_SLOTS_FOR_DEFAULT_REVIEW_SHOW_ALL = 50; - /** @var quiz object containing the quiz settings. */ + /** @var quiz_settings object containing the quiz settings. */ protected $quizobj; /** @var stdClass the quiz_attempts row. */ @@ -108,7 +108,7 @@ class quiz_attempt { */ public function __construct($attempt, $quiz, $cm, $course, $loadquestions = true) { $this->attempt = $attempt; - $this->quizobj = new quiz($quiz, $cm, $course); + $this->quizobj = new quiz_settings($quiz, $cm, $course); if ($loadquestions) { $this->load_questions(); @@ -293,9 +293,9 @@ class quiz_attempt { } /** - * Get the {@see quiz} object for this quiz. + * Get the {@see quiz_settings} object for this quiz. * - * @return quiz + * @return quiz_settings */ public function get_quizobj() { return $this->quizobj; diff --git a/mod/quiz/classes/quiz_settings.php b/mod/quiz/classes/quiz_settings.php new file mode 100644 index 00000000000..64ac60278b2 --- /dev/null +++ b/mod/quiz/classes/quiz_settings.php @@ -0,0 +1,556 @@ +. + +namespace mod_quiz; + +use coding_exception; +use context; +use context_module; +use mod_quiz\question\bank\qbank_helper; +use mod_quiz\question\display_options; +use moodle_exception; +use moodle_url; +use question_bank; +use stdClass; + +/** + * A class encapsulating the settings for a quiz. + * + * When this class is initialised, it may have the settings adjusted to account + * for the overrides for a particular user. See the create methods. + * + * Initially, it only loads a minimal amount of information about each question - loading + * extra information only when necessary or when asked. The class tracks which questions + * are loaded. + * + * @copyright 2008 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 2.0 + */ +class quiz_settings { + /** @var stdClass the course settings from the database. */ + protected $course; + /** @var stdClass the course_module settings from the database. */ + protected $cm; + /** @var stdClass the quiz settings from the database. */ + protected $quiz; + /** @var context the quiz context. */ + protected $context; + + /** + * @var stdClass[] of questions augmented with slot information. For non-random + * questions, the array key is question id. For random quesions it is 's' . $slotid. + * probalby best to use ->questionid field of the object instead. + */ + protected $questions = null; + /** @var stdClass[] of quiz_section rows. */ + protected $sections = null; + /** @var access_manager the access manager for this quiz. */ + protected $accessmanager = null; + /** @var bool whether the current user has capability mod/quiz:preview. */ + protected $ispreviewuser = null; + + // Constructor ============================================================= + + /** + * Constructor, assuming we already have the necessary data loaded. + * + * @param object $quiz the row from the quiz table. + * @param object $cm the course_module object for this quiz. + * @param object $course the row from the course table for the course we belong to. + * @param bool $getcontext intended for testing - stops the constructor getting the context. + */ + public function __construct($quiz, $cm, $course, $getcontext = true) { + $this->quiz = $quiz; + $this->cm = $cm; + $this->quiz->cmid = $this->cm->id; + $this->course = $course; + if ($getcontext && !empty($cm->id)) { + $this->context = context_module::instance($cm->id); + } + } + + /** + * Static function to create a new quiz object for a specific user. + * + * @param int $quizid the the quiz id. + * @param int|null $userid the the userid (optional). If passed, relevant overrides are applied. + * @return quiz_settings the new quiz object. + */ + public static function create($quizid, $userid = null) { + global $DB; + + $quiz = access_manager::load_quiz_and_settings($quizid); + $course = $DB->get_record('course', array('id' => $quiz->course), '*', MUST_EXIST); + $cm = get_coursemodule_from_instance('quiz', $quiz->id, $course->id, false, MUST_EXIST); + + // Update quiz with override information. + if ($userid) { + $quiz = quiz_update_effective_access($quiz, $userid); + } + + return new quiz_settings($quiz, $cm, $course); + } + + /** + * Create a {@see quiz_attempt} for an attempt at this quiz. + * + * @param object $attemptdata row from the quiz_attempts table. + * @return quiz_attempt the new quiz_attempt object. + */ + public function create_attempt_object($attemptdata) { + return new quiz_attempt($attemptdata, $this->quiz, $this->cm, $this->course); + } + + // Functions for loading more data ========================================= + + /** + * Load just basic information about all the questions in this quiz. + */ + public function preload_questions() { + $slots = qbank_helper::get_question_structure($this->quiz->id, $this->context); + $this->questions = []; + foreach ($slots as $slot) { + $this->questions[$slot->questionid] = $slot; + } + } + + /** + * Fully load some or all of the questions for this quiz. You must call + * {@link preload_questions()} first. + * + * @param array|null $deprecated no longer supported (it was not used). + */ + public function load_questions($deprecated = null) { + if ($deprecated !== null) { + debugging('The argument to quiz::load_questions is no longer supported. ' . + 'All questions are always loaded.', DEBUG_DEVELOPER); + } + if ($this->questions === null) { + throw new coding_exception('You must call preload_questions before calling load_questions.'); + } + + $questionstoprocess = []; + foreach ($this->questions as $question) { + if (is_number($question->questionid)) { + $question->id = $question->questionid; + $questionstoprocess[$question->questionid] = $question; + } + } + get_question_options($questionstoprocess); + } + + /** + * Get an instance of the {@see \mod_quiz\structure} class for this quiz. + * + * @return structure describes the questions in the quiz. + */ + public function get_structure() { + return structure::create_for_quiz($this); + } + + // Simple getters ========================================================== + + /** + * Get the id of the course this quiz belongs to. + * + * @return int the course id. + */ + public function get_courseid() { + return $this->course->id; + } + + /** + * Get the course settings object that this quiz belongs to. + * + * @return object the row of the course table. + */ + public function get_course() { + return $this->course; + } + + /** + * Get this quiz's id (in the quiz table). + * + * @return int the quiz id. + */ + public function get_quizid() { + return $this->quiz->id; + } + + /** + * Get the quiz settings object. + * + * @return stdClass the row of the quiz table. + */ + public function get_quiz() { + return $this->quiz; + } + + /** + * Get the quiz name. + * + * @return string the name of this quiz. + */ + public function get_quiz_name() { + return $this->quiz->name; + } + + /** + * Get the navigation method in use. + * + * @return int QUIZ_NAVMETHOD_FREE or QUIZ_NAVMETHOD_SEQ. + */ + public function get_navigation_method() { + return $this->quiz->navmethod; + } + + /** @return int the number of attempts allowed at this quiz (0 = infinite). */ + public function get_num_attempts_allowed() { + return $this->quiz->attempts; + } + + /** + * Get the course-module id for this quiz. + * + * @return int the course_module id. + */ + public function get_cmid() { + return $this->cm->id; + } + + /** + * Get the course-module object for this quiz. + * + * @return object the course_module object. + */ + public function get_cm() { + return $this->cm; + } + + /** + * Get the quiz context. + * + * @return context_module the module context for this quiz. + */ + public function get_context() { + return $this->context; + } + + /** + * @return bool whether the current user is someone who previews the quiz, + * rather than attempting it. + */ + public function is_preview_user() { + if (is_null($this->ispreviewuser)) { + $this->ispreviewuser = has_capability('mod/quiz:preview', $this->context); + } + return $this->ispreviewuser; + } + + /** + * Checks user enrollment in the current course. + * + * @param int $userid the id of the user to check. + * @return bool whether the user is enrolled. + */ + public function is_participant($userid) { + return is_enrolled($this->get_context(), $userid, 'mod/quiz:attempt', $this->show_only_active_users()); + } + + /** + * Check is only active users in course should be shown. + * + * @return bool true if only active users should be shown. + */ + public function show_only_active_users() { + return !has_capability('moodle/course:viewsuspendedusers', $this->get_context()); + } + + /** + * @return bool whether any questions have been added to this quiz. + */ + public function has_questions() { + if ($this->questions === null) { + $this->preload_questions(); + } + return !empty($this->questions); + } + + /** + * @param int $id the question id. + * @return stdClass the question object with that id. + */ + public function get_question($id) { + return $this->questions[$id]; + } + + /** + * @param array|null $questionids question ids of the questions to load. null for all. + * @return stdClass[] the question data objects. + */ + public function get_questions($questionids = null) { + if (is_null($questionids)) { + $questionids = array_keys($this->questions); + } + $questions = array(); + foreach ($questionids as $id) { + if (!array_key_exists($id, $this->questions)) { + throw new moodle_exception('cannotstartmissingquestion', 'quiz', $this->view_url()); + } + $questions[$id] = $this->questions[$id]; + $this->ensure_question_loaded($id); + } + return $questions; + } + + /** + * Get all the sections in this quiz. + * + * @return array 0, 1, 2, ... => quiz_sections row from the database. + */ + public function get_sections() { + global $DB; + if ($this->sections === null) { + $this->sections = array_values($DB->get_records('quiz_sections', + array('quizid' => $this->get_quizid()), 'firstslot')); + } + return $this->sections; + } + + /** + * Return access_manager and instance of the access_manager class + * for this quiz at this time. + * + * @param int $timenow the current time as a unix timestamp. + * @return access_manager and instance of the access_manager class + * for this quiz at this time. + */ + public function get_access_manager($timenow) { + if (is_null($this->accessmanager)) { + $this->accessmanager = new access_manager($this, $timenow, + has_capability('mod/quiz:ignoretimelimits', $this->context, null, false)); + } + return $this->accessmanager; + } + + /** + * Wrapper round the has_capability funciton that automatically passes in the quiz context. + * + * @param string $capability the name of the capability to check. For example mod/quiz:view. + * @param int|null $userid A user id. By default (null) checks the permissions of the current user. + * @param bool $doanything If false, ignore effect of admin role assignment. + * @return boolean true if the user has this capability. Otherwise false. + */ + public function has_capability($capability, $userid = null, $doanything = true) { + return has_capability($capability, $this->context, $userid, $doanything); + } + + /** + * Wrapper round the require_capability function that automatically passes in the quiz context. + * + * @param string $capability the name of the capability to check. For example mod/quiz:view. + * @param int|null $userid A user id. By default (null) checks the permissions of the current user. + * @param bool $doanything If false, ignore effect of admin role assignment. + */ + public function require_capability($capability, $userid = null, $doanything = true) { + require_capability($capability, $this->context, $userid, $doanything); + } + + // URLs related to this attempt ============================================ + + /** + * @return string the URL of this quiz's view page. + */ + public function view_url() { + global $CFG; + return $CFG->wwwroot . '/mod/quiz/view.php?id=' . $this->cm->id; + } + + /** + * @return string the URL of this quiz's edit page. + */ + public function edit_url() { + global $CFG; + return $CFG->wwwroot . '/mod/quiz/edit.php?cmid=' . $this->cm->id; + } + + /** + * @param int $attemptid the id of an attempt. + * @param int $page optional page number to go to in the attempt. + * @return string the URL of that attempt. + */ + public function attempt_url($attemptid, $page = 0) { + global $CFG; + $url = $CFG->wwwroot . '/mod/quiz/attempt.php?attempt=' . $attemptid; + if ($page) { + $url .= '&page=' . $page; + } + $url .= '&cmid=' . $this->get_cmid(); + return $url; + } + + /** + * Get the URL to start/continue an attempt. + * + * @param int $page page in the attempt to start on (optional). + * @return moodle_url the URL of this quiz's edit page. Needs to be POSTed to with a cmid parameter. + */ + public function start_attempt_url($page = 0) { + $params = array('cmid' => $this->cm->id, 'sesskey' => sesskey()); + if ($page) { + $params['page'] = $page; + } + return new moodle_url('/mod/quiz/startattempt.php', $params); + } + + /** + * @param int $attemptid the id of an attempt. + * @return string the URL of the review of that attempt. + */ + public function review_url($attemptid) { + return new moodle_url('/mod/quiz/review.php', array('attempt' => $attemptid, 'cmid' => $this->get_cmid())); + } + + /** + * @param int $attemptid the id of an attempt. + * @return string the URL of the review of that attempt. + */ + public function summary_url($attemptid) { + return new moodle_url('/mod/quiz/summary.php', array('attempt' => $attemptid, 'cmid' => $this->get_cmid())); + } + + // Bits of content ========================================================= + + /** + * @param bool $notused not used. + * @return string an empty string. + * @deprecated since 3.1. This sort of functionality is now entirely handled by quiz access rules. + */ + public function confirm_start_attempt_message($notused) { + debugging('confirm_start_attempt_message is deprecated. ' . + 'This sort of functionality is now entirely handled by quiz access rules.'); + return ''; + } + + /** + * If $reviewoptions->attempt is false, meaning that students can't review this + * attempt at the moment, return an appropriate string explaining why. + * + * @param int $when One of the display_options::DURING, + * IMMEDIATELY_AFTER, LATER_WHILE_OPEN or AFTER_CLOSE constants. + * @param bool $short if true, return a shorter string. + * @return string an appropraite message. + */ + public function cannot_review_message($when, $short = false) { + + if ($short) { + $langstrsuffix = 'short'; + $dateformat = get_string('strftimedatetimeshort', 'langconfig'); + } else { + $langstrsuffix = ''; + $dateformat = ''; + } + + if ($when == display_options::DURING || + $when == display_options::IMMEDIATELY_AFTER) { + return ''; + } else { + if ($when == display_options::LATER_WHILE_OPEN && $this->quiz->timeclose && + $this->quiz->reviewattempt & display_options::AFTER_CLOSE) { + return get_string('noreviewuntil' . $langstrsuffix, 'quiz', + userdate($this->quiz->timeclose, $dateformat)); + } else { + return get_string('noreview' . $langstrsuffix, 'quiz'); + } + } + } + + /** + * Probably not used any more, but left for backwards compatibility. + * + * @param string $title the name of this particular quiz page. + * @return string always returns ''. + */ + public function navigation($title) { + global $PAGE; + $PAGE->navbar->add($title); + return ''; + } + + // Private methods ========================================================= + + /** + * Check that the definition of a particular question is loaded, and if not throw an exception. + * + * @param int $id a question id. + */ + protected function ensure_question_loaded($id) { + if (isset($this->questions[$id]->_partiallyloaded)) { + throw new moodle_exception('questionnotloaded', 'quiz', $this->view_url(), $id); + } + } + + /** + * Return all the question types used in this quiz. + * + * @param boolean $includepotential if the quiz include random questions, + * setting this flag to true will make the function to return all the + * possible question types in the random questions category. + * @return array a sorted array including the different question types. + * @since Moodle 3.1 + */ + public function get_all_question_types_used($includepotential = false) { + $questiontypes = array(); + + // To control if we need to look in categories for questions. + $qcategories = array(); + + foreach ($this->get_questions() as $questiondata) { + if ($questiondata->qtype === 'random' && $includepotential) { + if (!isset($qcategories[$questiondata->category])) { + $qcategories[$questiondata->category] = false; + } + if (!empty($questiondata->filtercondition)) { + $filtercondition = json_decode($questiondata->filtercondition); + $qcategories[$questiondata->category] = !empty($filtercondition->includingsubcategories); + } + } else { + if (!in_array($questiondata->qtype, $questiontypes)) { + $questiontypes[] = $questiondata->qtype; + } + } + } + + if (!empty($qcategories)) { + // We have to look for all the question types in these categories. + $categoriestolook = array(); + foreach ($qcategories as $cat => $includesubcats) { + if ($includesubcats) { + $categoriestolook = array_merge($categoriestolook, question_categorylist($cat)); + } else { + $categoriestolook[] = $cat; + } + } + $questiontypesincategories = question_bank::get_all_question_types_in_categories($categoriestolook); + $questiontypes = array_merge($questiontypes, $questiontypesincategories); + } + $questiontypes = array_unique($questiontypes); + sort($questiontypes); + + return $questiontypes; + } +} diff --git a/mod/quiz/classes/structure.php b/mod/quiz/classes/structure.php index c2ab857f469..6cea4d504f6 100644 --- a/mod/quiz/classes/structure.php +++ b/mod/quiz/classes/structure.php @@ -39,7 +39,7 @@ use mod_quiz\question\qubaids_for_quiz; * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class structure { - /** @var \quiz the quiz this is the structure of. */ + /** @var \mod_quiz\quiz_settings the quiz this is the structure of. */ protected $quizobj = null; /** @@ -83,7 +83,7 @@ class structure { /** * Create an instance of this class representing the structure of a given quiz. * - * @param \quiz $quizobj the quiz. + * @param \mod_quiz\quiz_settings $quizobj the quiz. * @return structure */ public static function create_for_quiz($quizobj) { diff --git a/mod/quiz/db/renamedclasses.php b/mod/quiz/db/renamedclasses.php index 059eadccb9f..4c9b218c1dc 100644 --- a/mod/quiz/db/renamedclasses.php +++ b/mod/quiz/db/renamedclasses.php @@ -65,4 +65,5 @@ $renamedclasses = [ 'quiz_attempt_nav_panel' => 'mod_quiz\output\navigation_panel_attempt', 'quiz_review_nav_panel' => 'mod_quiz\output\navigation_panel_review', 'quiz_attempt' => 'mod_quiz\quiz_attempt', + 'quiz' => 'mod_quiz\quiz_settings', ]; diff --git a/mod/quiz/deprecatedlib.php b/mod/quiz/deprecatedlib.php index 0195153b86e..69982742a76 100644 --- a/mod/quiz/deprecatedlib.php +++ b/mod/quiz/deprecatedlib.php @@ -23,6 +23,7 @@ */ use mod_quiz\access_manager; +use mod_quiz\quiz_settings; use mod_quiz\task\update_overdue_attempts; /** @@ -71,7 +72,7 @@ function quiz_completion_check_passing_grade_or_all_attempts($course, $cm, $user } $lastfinishedattempt = end($attempts); $context = context_module::instance($cm->id); - $quizobj = quiz::create($quiz->id, $userid); + $quizobj = quiz_settings::create($quiz->id, $userid); $accessmanager = new access_manager($quizobj, time(), has_capability('mod/quiz:ignoretimelimits', $context, $userid, false)); @@ -177,7 +178,7 @@ class moodle_quiz_exception extends moodle_exception { /** * Constructor. * - * @param quiz $quizobj the quiz the error relates to. + * @param quiz_settings $quizobj the quiz the error relates to. * @param string $errorcode The name of the string from error.php to print. * @param mixed $a Extra words and phrases that might be required in the error string. * @param string $link The url where the user will be prompted to continue. diff --git a/mod/quiz/edit.php b/mod/quiz/edit.php index 93c93eabb4d..667289850c3 100644 --- a/mod/quiz/edit.php +++ b/mod/quiz/edit.php @@ -40,6 +40,7 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +use mod_quiz\quiz_settings; require_once(__DIR__ . '/../../config.php'); require_once($CFG->dirroot . '/mod/quiz/locallib.php'); @@ -62,7 +63,7 @@ $PAGE->set_secondary_active_tab("mod_quiz_edit"); // Get the course object and related bits. $course = $DB->get_record('course', array('id' => $quiz->course), '*', MUST_EXIST); -$quizobj = new quiz($quiz, $cm, $course); +$quizobj = new quiz_settings($quiz, $cm, $course); $structure = $quizobj->get_structure(); // You need mod/quiz:manage in addition to question capabilities to access this page. diff --git a/mod/quiz/edit_rest.php b/mod/quiz/edit_rest.php index 4d20fde23a7..e7fe66d1990 100644 --- a/mod/quiz/edit_rest.php +++ b/mod/quiz/edit_rest.php @@ -22,6 +22,8 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +use mod_quiz\quiz_settings; + if (!defined('AJAX_SCRIPT')) { define('AJAX_SCRIPT', true); } @@ -57,7 +59,7 @@ $cm = get_coursemodule_from_instance('quiz', $quiz->id, $quiz->course); $course = $DB->get_record('course', array('id' => $quiz->course), '*', MUST_EXIST); require_login($course, false, $cm); -$quizobj = new quiz($quiz, $cm, $course); +$quizobj = new quiz_settings($quiz, $cm, $course); $structure = $quizobj->get_structure(); $modcontext = context_module::instance($cm->id); diff --git a/mod/quiz/lib.php b/mod/quiz/lib.php index abf5afb16e0..a18327cb8cb 100644 --- a/mod/quiz/lib.php +++ b/mod/quiz/lib.php @@ -36,9 +36,9 @@ 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\quiz_attempt; +use mod_quiz\quiz_settings; require_once($CFG->dirroot . '/calendar/lib.php'); -require_once($CFG->dirroot . '/mod/quiz/attemptlib.php'); /**#@+ * Option controlling what options are offered on the quiz settings form. @@ -1183,7 +1183,7 @@ function mod_quiz_inplace_editable(string $itemtype, int $itemid, string $newval // Check permission of the user to update this item (customise question number). require_capability('mod/quiz:manage', $context); - $quizobj = new quiz($quiz, $cm, $course); + $quizobj = new quiz_settings($quiz, $cm, $course); $structure = $quizobj->get_structure(); $warning = false; // Clean input and update the record. @@ -1963,7 +1963,7 @@ function quiz_check_updates_since(cm_info $cm, $from, $filter = array()) { // Check if questions were updated. $updates->questions = (object) array('updated' => false); - $quizobj = quiz::create($cm->instance, $USER->id); + $quizobj = quiz_settings::create($cm->instance, $USER->id); $quizobj->preload_questions(); $quizobj->load_questions(); $questionids = array_keys($quizobj->get_questions()); @@ -2060,7 +2060,7 @@ function mod_quiz_core_calendar_provide_event_action(calendar_event $event, } $cm = get_fast_modinfo($event->courseid, $userid)->instances['quiz'][$event->instance]; - $quizobj = quiz::create($cm->instance, $userid); + $quizobj = quiz_settings::create($cm->instance, $userid); $quiz = $quizobj->get_quiz(); // Check they have capabilities allowing them to view the quiz. diff --git a/mod/quiz/locallib.php b/mod/quiz/locallib.php index a31e243b389..89c15d61c03 100644 --- a/mod/quiz/locallib.php +++ b/mod/quiz/locallib.php @@ -31,7 +31,6 @@ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/mod/quiz/lib.php'); -require_once($CFG->dirroot . '/mod/quiz/attemptlib.php'); require_once($CFG->libdir . '/completionlib.php'); require_once($CFG->libdir . '/filelib.php'); require_once($CFG->libdir . '/questionlib.php'); @@ -40,6 +39,7 @@ use mod_quiz\access_manager; use mod_quiz\question\bank\qbank_helper; use mod_quiz\question\display_options; use mod_quiz\quiz_attempt; +use mod_quiz\quiz_settings; /** * @var int We show the countdown timer if there is less than this amount of time left before the @@ -89,7 +89,7 @@ define('QUIZ_SHOWIMAGE_LARGE', 2); * * @return object the newly created attempt object. */ -function quiz_create_attempt(quiz $quizobj, $attemptnumber, $lastattempt, $timenow, $ispreview = false, $userid = null) { +function quiz_create_attempt(quiz_settings $quizobj, $attemptnumber, $lastattempt, $timenow, $ispreview = false, $userid = null) { global $USER; if ($userid === null) { @@ -145,7 +145,7 @@ function quiz_create_attempt(quiz $quizobj, $attemptnumber, $lastattempt, $timen /** * Start a normal, new, quiz attempt. * - * @param quiz $quizobj the quiz object to start an attempt for. + * @param quiz_settings $quizobj the quiz object to start an attempt for. * @param question_usage_by_activity $quba * @param object $attempt * @param integer $attemptnumber starting from 1 @@ -155,8 +155,8 @@ function quiz_create_attempt(quiz $quizobj, $attemptnumber, $lastattempt, $timen * @param array $forcedvariantsbyslot slot number => variant. Used for questions with variants, * to force the choice of a particular variant. Intended for testing * purposes only. - * @throws moodle_exception * @return object modified attempt object + *@throws moodle_exception */ function quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $timenow, $questionids = array(), $forcedvariantsbyslot = array()) { @@ -340,7 +340,7 @@ function quiz_start_attempt_built_on_last($quba, $attempt, $lastattempt) { /** * The save started question usage and quiz attempt in db and log the started attempt. * - * @param quiz $quizobj + * @param quiz_settings $quizobj * @param question_usage_by_activity $quba * @param object $attempt * @return object attempt object with uniqueid and id set. @@ -2436,7 +2436,7 @@ function quiz_view($quiz, $course, $cm, $context) { /** * Validate permissions for creating a new attempt and start a new preview attempt if required. * - * @param quiz $quizobj quiz object + * @param quiz_settings $quizobj quiz object * @param access_manager $accessmanager quiz access manager * @param bool $forcenew whether was required to start a new preview attempt * @param int $page page to jump to in the attempt @@ -2444,7 +2444,7 @@ function quiz_view($quiz, $course, $cm, $context) { * @return array an array containing the attempt information, access error messages and the page to jump to in the attempt * @since Moodle 3.1 */ -function quiz_validate_new_attempt(quiz $quizobj, access_manager $accessmanager, $forcenew, $page, $redirect) { +function quiz_validate_new_attempt(quiz_settings $quizobj, access_manager $accessmanager, $forcenew, $page, $redirect) { global $DB, $USER; $timenow = time(); @@ -2520,7 +2520,7 @@ function quiz_validate_new_attempt(quiz $quizobj, access_manager $accessmanager, /** * Prepare and start a new attempt deleting the previous preview attempts. * - * @param quiz $quizobj quiz object + * @param quiz_settings $quizobj quiz object * @param int $attemptnumber the attempt number * @param object $lastattempt last attempt object * @param bool $offlineattempt whether is an offline attempt or not @@ -2532,7 +2532,7 @@ function quiz_validate_new_attempt(quiz $quizobj, access_manager $accessmanager, * @return object the new attempt * @since Moodle 3.1 */ -function quiz_prepare_and_start_new_attempt(quiz $quizobj, $attemptnumber, $lastattempt, +function quiz_prepare_and_start_new_attempt(quiz_settings $quizobj, $attemptnumber, $lastattempt, $offlineattempt = false, $forcedrandomquestions = [], $forcedvariants = [], $userid = null) { global $DB, $USER; diff --git a/mod/quiz/repaginate.php b/mod/quiz/repaginate.php index 08ce25f84f1..4ccb37da705 100644 --- a/mod/quiz/repaginate.php +++ b/mod/quiz/repaginate.php @@ -22,6 +22,8 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +use mod_quiz\quiz_settings; + require_once(__DIR__ . '/../../config.php'); require_once($CFG->dirroot . '/mod/quiz/locallib.php'); @@ -30,7 +32,7 @@ $slotnumber = required_param('slot', PARAM_INT); $repagtype = required_param('repag', PARAM_INT); require_sesskey(); -$quizobj = quiz::create($quizid); +$quizobj = quiz_settings::create($quizid); require_login($quizobj->get_course(), false, $quizobj->get_cm()); require_capability('mod/quiz:manage', $quizobj->get_context()); if (quiz_has_attempts($quizid)) { diff --git a/mod/quiz/report/overview/tests/report_test.php b/mod/quiz/report/overview/tests/report_test.php index 6d4fc89384c..1d0e583d56b 100644 --- a/mod/quiz/report/overview/tests/report_test.php +++ b/mod/quiz/report/overview/tests/report_test.php @@ -20,7 +20,7 @@ use core_question\local\bank\question_version_status; use mod_quiz\external\submit_question_version; use mod_quiz\quiz_attempt; use question_engine; -use quiz; +use mod_quiz\quiz_settings; use mod_quiz\local\reports\attempts_report; use quiz_overview_options; use quiz_overview_report; @@ -122,7 +122,7 @@ class report_test extends \advanced_testcase { list($quiz, $student, $attemptnumber, $sumgrades, $state) = $attemptdata; $timestart = $timestamp + $attemptnumber * 3600; - $quizobj = quiz::create($quiz->id, $student->id); + $quizobj = quiz_settings::create($quiz->id, $student->id); $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); @@ -312,7 +312,7 @@ class report_test extends \advanced_testcase { $quizattemptsreport = new \testable_quiz_attempts_report(); // Create the new attempt and initialize the question sessions. - $quizobj = quiz::create($quiz->id, $student->id); + $quizobj = quiz_settings::create($quiz->id, $student->id); $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); $attempt = quiz_create_attempt($quizobj, 1, null, $timestart, false, $student->id); @@ -358,7 +358,7 @@ class report_test extends \advanced_testcase { quiz_add_quiz_question($q2->id, $quiz, 0, 10); // Attempt the quiz, submitting response 'toad'. - $quizobj = quiz::create($quiz->id); + $quizobj = quiz_settings::create($quiz->id); $attempt = quiz_prepare_and_start_new_attempt($quizobj, 1, null); $attemptobj = quiz_attempt::create($attempt->id); $attemptobj->process_submitted_actions(time(), false, [1 => ['answer' => 'toad']]); diff --git a/mod/quiz/report/reportlib.php b/mod/quiz/report/reportlib.php index 611428cdc8f..53ba5ed622b 100644 --- a/mod/quiz/report/reportlib.php +++ b/mod/quiz/report/reportlib.php @@ -26,7 +26,6 @@ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/mod/quiz/lib.php'); -require_once($CFG->dirroot . '/mod/quiz/attemptlib.php'); require_once($CFG->libdir . '/filelib.php'); use mod_quiz\question\display_options; @@ -97,7 +96,7 @@ function quiz_has_questions($quizid) { */ function quiz_report_get_significant_questions($quiz) { global $DB; - $quizobj = \quiz::create($quiz->id); + $quizobj = mod_quiz\quiz_settings::create($quiz->id); $structure = \mod_quiz\structure::create_for_quiz($quizobj); $slots = $structure->get_slots(); diff --git a/mod/quiz/report/statistics/classes/task/recalculate.php b/mod/quiz/report/statistics/classes/task/recalculate.php index 425925f0eba..ba3001f0bcf 100644 --- a/mod/quiz/report/statistics/classes/task/recalculate.php +++ b/mod/quiz/report/statistics/classes/task/recalculate.php @@ -17,7 +17,7 @@ namespace quiz_statistics\task; use mod_quiz\quiz_attempt; -use quiz; +use mod_quiz\quiz_settings; use quiz_statistics_report; defined('MOODLE_INTERNAL') || die(); @@ -60,7 +60,7 @@ class recalculate extends \core\task\scheduled_task { $latestattempts = $DB->get_records_sql($sql, $params); foreach ($latestattempts as $attempt) { - $quizobj = quiz::create($attempt->quiz); + $quizobj = quiz_settings::create($attempt->quiz); $quiz = $quizobj->get_quiz(); // Hash code for question stats option in question bank. $qubaids = quiz_statistics_qubaids_condition($quiz->id, new \core\dml\sql_join(), $quiz->grademethod); diff --git a/mod/quiz/startattempt.php b/mod/quiz/startattempt.php index f1dea6bce10..b4a132f7ccc 100644 --- a/mod/quiz/startattempt.php +++ b/mod/quiz/startattempt.php @@ -27,6 +27,7 @@ */ use mod_quiz\quiz_attempt; +use mod_quiz\quiz_settings; require_once(__DIR__ . '/../../config.php'); require_once($CFG->dirroot . '/mod/quiz/locallib.php'); @@ -43,7 +44,7 @@ if (!$course = $DB->get_record('course', array('id' => $cm->course))) { throw new \moodle_exception("coursemisconf"); } -$quizobj = quiz::create($cm->instance, $USER->id); +$quizobj = quiz_settings::create($cm->instance, $USER->id); // This script should only ever be posted to, so set page URL to the view page. $PAGE->set_url($quizobj->view_url()); // During quiz attempts, the browser back/forwards buttons should force a reload. diff --git a/mod/quiz/tests/attempt_test.php b/mod/quiz/tests/attempt_test.php index 8faefc11d4a..7dbf1128c84 100644 --- a/mod/quiz/tests/attempt_test.php +++ b/mod/quiz/tests/attempt_test.php @@ -17,7 +17,7 @@ namespace mod_quiz; use question_engine; -use quiz; +use mod_quiz\quiz_settings; defined('MOODLE_INTERNAL') || die(); @@ -52,7 +52,7 @@ class attempt_test extends \advanced_testcase { $quiz = $quizgenerator->create_instance(['course' => $course->id, 'grade' => 100.0, 'sumgrades' => 2, 'layout' => $layout, 'navmethod' => $navmethod]); - $quizobj = quiz::create($quiz->id, $user->id); + $quizobj = quiz_settings::create($quiz->id, $user->id); $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); @@ -290,7 +290,7 @@ class attempt_test extends \advanced_testcase { $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $student2 = $this->getDataGenerator()->create_and_enrol($course, 'student', [], 'manual', 0, 0, ENROL_USER_SUSPENDED); $quiz = $this->getDataGenerator()->create_module('quiz', array('course' => $course->id)); - $quizobj = quiz::create($quiz->id); + $quizobj = quiz_settings::create($quiz->id); // Login as student. $this->setUser($student); @@ -338,7 +338,7 @@ class attempt_test extends \advanced_testcase { $question = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]); quiz_add_quiz_question($question->id, $quiz, 1); - $quizobj = quiz::create($quiz->id); + $quizobj = quiz_settings::create($quiz->id); // Login as student1. $this->setUser($student1); diff --git a/mod/quiz/tests/attempt_walkthrough_from_csv_test.php b/mod/quiz/tests/attempt_walkthrough_from_csv_test.php index 4798e9036da..987195e8107 100644 --- a/mod/quiz/tests/attempt_walkthrough_from_csv_test.php +++ b/mod/quiz/tests/attempt_walkthrough_from_csv_test.php @@ -17,7 +17,7 @@ namespace mod_quiz; use question_engine; -use quiz; +use mod_quiz\quiz_settings; defined('MOODLE_INTERNAL') || die(); @@ -245,7 +245,7 @@ class attempt_walkthrough_from_csv_test extends \advanced_testcase { if (!isset($attemptids[$step['quizattempt']])) { // Start the attempt. - $quizobj = quiz::create($this->quiz->id, $user->id); + $quizobj = quiz_settings::create($this->quiz->id, $user->id); $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); diff --git a/mod/quiz/tests/attempt_walkthrough_test.php b/mod/quiz/tests/attempt_walkthrough_test.php index 8d3a80d753a..b30236c97f5 100644 --- a/mod/quiz/tests/attempt_walkthrough_test.php +++ b/mod/quiz/tests/attempt_walkthrough_test.php @@ -18,7 +18,7 @@ namespace mod_quiz; use question_bank; use question_engine; -use quiz; +use mod_quiz\quiz_settings; defined('MOODLE_INTERNAL') || die(); @@ -67,7 +67,7 @@ class attempt_walkthrough_test extends \advanced_testcase { // Make a user to do the quiz. $user1 = $this->getDataGenerator()->create_user(); - $quizobj = quiz::create($quiz->id, $user1->id); + $quizobj = quiz_settings::create($quiz->id, $user1->id); // Start the attempt. $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); @@ -188,7 +188,7 @@ class attempt_walkthrough_test extends \advanced_testcase { // Make a user to do the quiz. $user = $this->getDataGenerator()->create_user(); $this->setUser($user); - $quizobj = quiz::create($quiz->id, $user->id); + $quizobj = quiz_settings::create($quiz->id, $user->id); // Start the attempt. $attempt = quiz_prepare_and_start_new_attempt($quizobj, 1, null); @@ -234,7 +234,7 @@ class attempt_walkthrough_test extends \advanced_testcase { // Make a user to do the quiz. $user = $this->getDataGenerator()->create_user(); $this->setUser($user); - $quizobj = quiz::create($quiz->id, $user->id); + $quizobj = quiz_settings::create($quiz->id, $user->id); // Start the attempt. $attempt = quiz_prepare_and_start_new_attempt($quizobj, 1, null); @@ -307,7 +307,7 @@ class attempt_walkthrough_test extends \advanced_testcase { $user1 = $this->getDataGenerator()->create_user(); $this->setUser($user1); - $quizobj = quiz::create($quiz->id, $user1->id); + $quizobj = quiz_settings::create($quiz->id, $user1->id); // Start the attempt. $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); @@ -408,7 +408,7 @@ class attempt_walkthrough_test extends \advanced_testcase { // Make a new user to do the quiz. $user1 = $this->getDataGenerator()->create_user(); $this->setUser($user1); - $quizobj = quiz::create($this->quizwithvariants->id, $user1->id); + $quizobj = quiz_settings::create($this->quizwithvariants->id, $user1->id); // Start the attempt. $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); diff --git a/mod/quiz/tests/attempts_test.php b/mod/quiz/tests/attempts_test.php index da714842221..1de3efb8732 100644 --- a/mod/quiz/tests/attempts_test.php +++ b/mod/quiz/tests/attempts_test.php @@ -20,7 +20,7 @@ use core_question_generator; use mod_quiz\task\update_overdue_attempts; use mod_quiz_generator; use question_engine; -use quiz; +use mod_quiz\quiz_settings; defined('MOODLE_INTERNAL') || die(); @@ -560,7 +560,7 @@ class attempts_test extends \advanced_testcase { // Add them to the quiz. quiz_add_quiz_question($saq->id, $quiz); quiz_add_quiz_question($numq->id, $quiz); - $quizobj = quiz::create($quiz->id, $user1->id); + $quizobj = quiz_settings::create($quiz->id, $user1->id); $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); $timenow = time(); diff --git a/mod/quiz/tests/backup/restore_date_test.php b/mod/quiz/tests/backup/restore_date_test.php index f2f700de814..621ee50a3d8 100644 --- a/mod/quiz/tests/backup/restore_date_test.php +++ b/mod/quiz/tests/backup/restore_date_test.php @@ -50,7 +50,7 @@ class restore_date_test extends \restore_date_testcase { // Create an attempt. $timestamp = 100; - $quizobj = \quiz::create($quiz->id); + $quizobj = \mod_quiz\quiz_settings::create($quiz->id); $attempt = quiz_create_attempt($quizobj, 1, false, $timestamp, false); $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); diff --git a/mod/quiz/tests/custom_completion_test.php b/mod/quiz/tests/custom_completion_test.php index 7ba00e1ddaa..bf4c6085c5d 100644 --- a/mod/quiz/tests/custom_completion_test.php +++ b/mod/quiz/tests/custom_completion_test.php @@ -24,7 +24,7 @@ use core_completion\cm_completion_details; use grade_item; use mod_quiz\completion\custom_completion; use question_engine; -use quiz; +use mod_quiz\quiz_settings; defined('MOODLE_INTERNAL') || die(); @@ -112,7 +112,7 @@ class custom_completion_test extends advanced_testcase { * @param array $attemptoptions ['quiz'] => object, ['student'] => object, ['tosubmit'] => array, ['attemptnumber'] => int */ private function do_attempt_quiz(array $attemptoptions) { - $quizobj = quiz::create($attemptoptions['quiz']->id); + $quizobj = quiz_settings::create($attemptoptions['quiz']->id); // Start the passing attempt. $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); diff --git a/mod/quiz/tests/event/events_test.php b/mod/quiz/tests/event/events_test.php index 78f6fe40e6c..c998654197a 100644 --- a/mod/quiz/tests/event/events_test.php +++ b/mod/quiz/tests/event/events_test.php @@ -26,14 +26,9 @@ namespace mod_quiz\event; use mod_quiz\quiz_attempt; -use quiz; +use mod_quiz\quiz_settings; use context_module; -defined('MOODLE_INTERNAL') || die(); - -global $CFG; -require_once($CFG->dirroot . '/mod/quiz/attemptlib.php'); - /** * Unit tests for quiz events. * @@ -47,7 +42,7 @@ class events_test extends \advanced_testcase { /** * Setup a quiz. * - * @return quiz the generated quiz. + * @return quiz_settings the generated quiz. */ protected function prepare_quiz() { @@ -79,13 +74,13 @@ class events_test extends \advanced_testcase { $user1 = $this->getDataGenerator()->create_user(); $this->setUser($user1); - return quiz::create($quiz->id, $user1->id); + return quiz_settings::create($quiz->id, $user1->id); } /** * Setup a quiz attempt at the quiz created by {@link prepare_quiz()}. * - * @param quiz $quizobj the generated quiz. + * @param \mod_quiz\quiz_settings $quizobj the generated quiz. * @param bool $ispreview Make the attempt a preview attempt when true. * @return array with three elements, array($quizobj, $quba, $attempt) */ diff --git a/mod/quiz/tests/external/external_test.php b/mod/quiz/tests/external/external_test.php index 66f32c13e3b..9541c096337 100644 --- a/mod/quiz/tests/external/external_test.php +++ b/mod/quiz/tests/external/external_test.php @@ -31,7 +31,7 @@ use mod_quiz\quiz_attempt; use mod_quiz_external; use mod_quiz\question\display_options; use moodle_exception; -use quiz; +use mod_quiz\quiz_settings; defined('MOODLE_INTERNAL') || die(); @@ -149,7 +149,7 @@ class external_test extends externallib_advanced_testcase { quiz_add_quiz_question($question->id, $quiz); } - $quizobj = quiz::create($quiz->id, $this->student->id); + $quizobj = quiz_settings::create($quiz->id, $this->student->id); // Set grade to pass. $item = \grade_item::fetch(array('courseid' => $this->course->id, 'itemtype' => 'mod', @@ -545,8 +545,8 @@ class external_test extends externallib_advanced_testcase { quiz_add_quiz_question($question->id, $quizapi2); // Create quiz object. - $quizapiobj1 = quiz::create($quizapi1->id, $this->student->id); - $quizapiobj2 = quiz::create($quizapi2->id, $this->student->id); + $quizapiobj1 = quiz_settings::create($quizapi1->id, $this->student->id); + $quizapiobj2 = quiz_settings::create($quizapi2->id, $this->student->id); // Set grade to pass. $item = \grade_item::fetch([ @@ -700,7 +700,7 @@ class external_test extends externallib_advanced_testcase { $question = $questiongenerator->create_question('numerical', null, array('category' => $cat->id)); quiz_add_quiz_question($question->id, $quiz); - $quizobj = quiz::create($quiz->id, $this->student->id); + $quizobj = quiz_settings::create($quiz->id, $this->student->id); // Set grade to pass. $item = \grade_item::fetch(array('courseid' => $this->course->id, 'itemtype' => 'mod', @@ -1866,7 +1866,7 @@ class external_test extends externallib_advanced_testcase { quiz_add_random_questions($quiz, 0, $cat->id, 1, false); - $quizobj = quiz::create($quiz->id, $this->student->id); + $quizobj = quiz_settings::create($quiz->id, $this->student->id); // Set grade to pass. $item = \grade_item::fetch(array('courseid' => $this->course->id, 'itemtype' => 'mod', @@ -2087,9 +2087,9 @@ class external_test extends externallib_advanced_testcase { /** * Prepare quiz for sequential navigation tests * - * @return quiz + * @return quiz_settings */ - private function prepare_sequential_quiz(): quiz { + private function prepare_sequential_quiz(): quiz_settings { // Create a new quiz with 5 questions and one attempt started. // Create a new quiz with attempts. $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); @@ -2112,7 +2112,7 @@ class external_test extends externallib_advanced_testcase { quiz_add_quiz_question($question->id, $quiz, $pageindex); } - $quizobj = quiz::create($quiz->id, $this->student->id); + $quizobj = quiz_settings::create($quiz->id, $this->student->id); // Set grade to pass. $item = \grade_item::fetch(array('courseid' => $this->course->id, 'itemtype' => 'mod', 'itemmodule' => 'quiz', 'iteminstance' => $quiz->id, 'outcomeid' => null)); @@ -2124,13 +2124,13 @@ class external_test extends externallib_advanced_testcase { /** * Create question attempt * - * @param quiz $quizobj + * @param quiz_settings $quizobj * @param int|null $userid * @param bool|null $ispreview * @return quiz_attempt * @throws moodle_exception */ - private function create_quiz_attempt_object(quiz $quizobj, ?int $userid = null, ?bool $ispreview = false): quiz_attempt { + private function create_quiz_attempt_object(quiz_settings $quizobj, ?int $userid = null, ?bool $ispreview = false): quiz_attempt { global $USER; $timenow = time(); // Now, do one attempt. diff --git a/mod/quiz/tests/generator/lib.php b/mod/quiz/tests/generator/lib.php index 9e728ce1f38..416000b1239 100644 --- a/mod/quiz/tests/generator/lib.php +++ b/mod/quiz/tests/generator/lib.php @@ -15,6 +15,7 @@ // along with Moodle. If not, see . use mod_quiz\quiz_attempt; +use mod_quiz\quiz_settings; defined('MOODLE_INTERNAL') || die(); @@ -117,7 +118,7 @@ class mod_quiz_generator extends testing_module_generator { public function create_attempt($quizid, $userid, array $forcedrandomquestions = [], array $forcedvariants = []) { // Build quiz object and load questions. - $quizobj = quiz::create($quizid, $userid); + $quizobj = quiz_settings::create($quizid, $userid); $attemptnumber = 1; $attempt = null; diff --git a/mod/quiz/tests/lib_test.php b/mod/quiz/tests/lib_test.php index 5515b4a338e..a4826dd8845 100644 --- a/mod/quiz/tests/lib_test.php +++ b/mod/quiz/tests/lib_test.php @@ -24,7 +24,7 @@ */ namespace mod_quiz; -use quiz; +use mod_quiz\quiz_settings; defined('MOODLE_INTERNAL') || die(); @@ -197,7 +197,7 @@ class lib_test extends \advanced_testcase { * @param $attemptoptions ['quiz'] => object, ['student'] => object, ['tosubmit'] => array, ['attemptnumber'] => int */ private function do_attempt_quiz($attemptoptions) { - $quizobj = quiz::create($attemptoptions['quiz']->id); + $quizobj = quiz_settings::create($attemptoptions['quiz']->id); // Start the passing attempt. $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); @@ -454,11 +454,11 @@ class lib_test extends \advanced_testcase { quiz_add_quiz_question($question->id, $quiz1); quiz_add_quiz_question($question->id, $quiz2); - $quizobj1a = quiz::create($quiz1->id, $u1->id); - $quizobj1b = quiz::create($quiz1->id, $u2->id); - $quizobj1c = quiz::create($quiz1->id, $u3->id); - $quizobj1d = quiz::create($quiz1->id, $u4->id); - $quizobj2a = quiz::create($quiz2->id, $u1->id); + $quizobj1a = quiz_settings::create($quiz1->id, $u1->id); + $quizobj1b = quiz_settings::create($quiz1->id, $u2->id); + $quizobj1c = quiz_settings::create($quiz1->id, $u3->id); + $quizobj1d = quiz_settings::create($quiz1->id, $u4->id); + $quizobj2a = quiz_settings::create($quiz2->id, $u1->id); // Set attempts. $quba1a = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj1a->get_context()); @@ -964,7 +964,7 @@ class lib_test extends \advanced_testcase { quiz_add_quiz_question($question->id, $quiz); // Get the quiz object. - $quizobj = quiz::create($quiz->id, $student->id); + $quizobj = quiz_settings::create($quiz->id, $student->id); // Create an attempt for the student in the quiz. $timenow = time(); @@ -1019,7 +1019,7 @@ class lib_test extends \advanced_testcase { quiz_add_quiz_question($question->id, $quiz); // Get the quiz object. - $quizobj = quiz::create($quiz->id, $student->id); + $quizobj = quiz_settings::create($quiz->id, $student->id); // Create an attempt for the student in the quiz. $timenow = time(); @@ -1245,7 +1245,7 @@ class lib_test extends \advanced_testcase { quiz_add_quiz_question($question->id, $quiz); // Create the quiz object. - $quizobj = new quiz($quiz, $cm, $course); + $quizobj = new quiz_settings($quiz, $cm, $course); $structure = $quizobj->get_structure(); $slots = $structure->get_slots(); diff --git a/mod/quiz/tests/privacy/provider_test.php b/mod/quiz/tests/privacy/provider_test.php index 88fa323d2f3..1f74e996d6c 100644 --- a/mod/quiz/tests/privacy/provider_test.php +++ b/mod/quiz/tests/privacy/provider_test.php @@ -247,7 +247,7 @@ class provider_test extends \core_privacy\tests\provider_testcase { // Run as the user and make an attempt on the quiz. $this->setUser($user); $starttime = time(); - $quizobj = \quiz::create($quiz->id, $user->id); + $quizobj = \mod_quiz\quiz_settings::create($quiz->id, $user->id); $context = $quizobj->get_context(); $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); @@ -430,7 +430,7 @@ class provider_test extends \core_privacy\tests\provider_testcase { $this->setUser($user); $starttime = time(); - $quizobj = \quiz::create($quiz->id, $user->id); + $quizobj = \mod_quiz\quiz_settings::create($quiz->id, $user->id); $context = $quizobj->get_context(); $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); diff --git a/mod/quiz/tests/privacy_legacy_quizaccess_polyfill_test.php b/mod/quiz/tests/privacy_legacy_quizaccess_polyfill_test.php index a908760957d..f34b01a0458 100644 --- a/mod/quiz/tests/privacy_legacy_quizaccess_polyfill_test.php +++ b/mod/quiz/tests/privacy_legacy_quizaccess_polyfill_test.php @@ -23,13 +23,6 @@ namespace mod_quiz; -use quiz; - -defined('MOODLE_INTERNAL') || die(); - -global $CFG; -require_once($CFG->dirroot . '/mod/quiz/attemptlib.php'); - /** * Unit tests for the privacy legacy polyfill for quiz access rules. * @@ -42,7 +35,7 @@ class privacy_legacy_quizaccess_polyfill_test extends \advanced_testcase { * be called. */ public function test_export_quizaccess_user_data() { - $quiz = $this->createMock(quiz::class); + $quiz = $this->createMock(quiz_settings::class); $user = (object) []; $returnvalue = (object) []; @@ -63,7 +56,7 @@ class privacy_legacy_quizaccess_polyfill_test extends \advanced_testcase { public function test_delete_quizaccess_for_context() { $context = \context_system::instance(); - $quiz = $this->createMock(quiz::class); + $quiz = $this->createMock(quiz_settings::class); $mock = $this->createMock(test_privacy_legacy_quizaccess_polyfill_mock_wrapper::class); $mock->expects($this->once()) @@ -80,7 +73,7 @@ class privacy_legacy_quizaccess_polyfill_test extends \advanced_testcase { public function test_delete_quizaccess_for_user() { $context = \context_system::instance(); - $quiz = $this->createMock(quiz::class); + $quiz = $this->createMock(quiz_settings::class); $user = (object) []; $mock = $this->createMock(test_privacy_legacy_quizaccess_polyfill_mock_wrapper::class); @@ -132,7 +125,7 @@ class test_privacy_legacy_quizaccess_polyfill_provider implements /** * Export all user data for the quizaccess plugin. * - * @param \quiz $quiz + * @param \mod_quiz\quiz_settings $quiz * @param \stdClass $user */ protected static function _export_quizaccess_user_data($quiz, $user) { @@ -142,7 +135,7 @@ class test_privacy_legacy_quizaccess_polyfill_provider implements /** * Deletes all user data for the given context. * - * @param \quiz $quiz + * @param \mod_quiz\quiz_settings $quiz */ protected static function _delete_quizaccess_data_for_all_users_in_context($quiz) { static::$mock->get_return_value(__FUNCTION__, func_get_args()); @@ -151,7 +144,7 @@ class test_privacy_legacy_quizaccess_polyfill_provider implements /** * Delete personal data for the given user and context. * - * @param \quiz $quiz The quiz being deleted + * @param \mod_quiz\quiz_settings $quiz The quiz being deleted * @param \stdClass $user The user to export data for */ protected static function _delete_quizaccess_data_for_user($quiz, $user) { diff --git a/mod/quiz/tests/qbank_helper_test.php b/mod/quiz/tests/qbank_helper_test.php index 71d3474dbbf..456d9a0b649 100644 --- a/mod/quiz/tests/qbank_helper_test.php +++ b/mod/quiz/tests/qbank_helper_test.php @@ -78,7 +78,7 @@ class qbank_helper_test extends \advanced_testcase { quiz_add_quiz_question($numq->id, $quiz); // Create the quiz object. - $quizobj = \quiz::create($quiz->id); + $quizobj = \mod_quiz\quiz_settings::create($quiz->id); $quizobj->preload_questions(); $quizobj->load_questions(); $questions = $quizobj->get_questions(); @@ -131,7 +131,7 @@ class qbank_helper_test extends \advanced_testcase { quiz_add_quiz_question($q->id, $quiz); // Load the quiz object and check. - $quizobj = \quiz::create($quiz->id); + $quizobj = \mod_quiz\quiz_settings::create($quiz->id); $quizobj->preload_questions(); $quizobj->load_questions(); $questions = $quizobj->get_questions(); diff --git a/mod/quiz/tests/quiz_notify_attempt_manual_grading_completed_test.php b/mod/quiz/tests/quiz_notify_attempt_manual_grading_completed_test.php index 11968dfd905..544e4a3e285 100644 --- a/mod/quiz/tests/quiz_notify_attempt_manual_grading_completed_test.php +++ b/mod/quiz/tests/quiz_notify_attempt_manual_grading_completed_test.php @@ -29,7 +29,7 @@ use context_course; use context_module; use mod_quiz\task\quiz_notify_attempt_manual_grading_completed; use question_engine; -use quiz; +use mod_quiz\quiz_settings; use stdClass; defined('MOODLE_INTERNAL') || die(); @@ -61,7 +61,7 @@ class quiz_notify_attempt_manual_grading_completed_test extends advanced_testcas /** @var stdClass The teacher test. */ protected $teacher; - /** @var quiz Object containing the quiz settings. */ + /** @var quiz_settings Object containing the quiz settings. */ protected $quizobj; /** @var question_usage_by_activity The question usage for this quiz attempt. */ @@ -117,7 +117,7 @@ class quiz_notify_attempt_manual_grading_completed_test extends advanced_testcas quiz_add_quiz_question($truefalse->id, $this->quiz); quiz_add_quiz_question($essay->id, $this->quiz); - $this->quizobj = quiz::create($this->quiz->id); + $this->quizobj = quiz_settings::create($this->quiz->id); $this->quba = question_engine::make_questions_usage_by_activity('mod_quiz', $this->quizobj->get_context()); $this->quba->set_preferred_behaviour($this->quizobj->get_quiz()->preferredbehaviour); } diff --git a/mod/quiz/tests/quiz_question_helper_test_trait.php b/mod/quiz/tests/quiz_question_helper_test_trait.php index b41722a57ec..35e027df16e 100644 --- a/mod/quiz/tests/quiz_question_helper_test_trait.php +++ b/mod/quiz/tests/quiz_question_helper_test_trait.php @@ -14,6 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . use mod_quiz\quiz_attempt; +use mod_quiz\quiz_settings; /** * Helper trait for quiz question unit tests. @@ -106,7 +107,7 @@ trait quiz_question_helper_test_trait { $this->setUser($user); $starttime = time(); - $quizobj = quiz::create($quiz->id, $user->id); + $quizobj = quiz_settings::create($quiz->id, $user->id); $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); diff --git a/mod/quiz/tests/quiz_question_restore_test.php b/mod/quiz/tests/quiz_question_restore_test.php index 0e4726d7482..be24aa3609f 100644 --- a/mod/quiz/tests/quiz_question_restore_test.php +++ b/mod/quiz/tests/quiz_question_restore_test.php @@ -259,7 +259,7 @@ class quiz_question_restore_test extends \advanced_testcase { // Get the information about the resulting course and check that it is set up correctly. $modinfo = get_fast_modinfo($newcourseid); $quiz = array_values($modinfo->get_instances_of('quiz'))[0]; - $quizobj = \quiz::create($quiz->instance); + $quizobj = \mod_quiz\quiz_settings::create($quiz->instance); $structure = structure::create_for_quiz($quizobj); // Are the correct slots returned? @@ -302,7 +302,7 @@ class quiz_question_restore_test extends \advanced_testcase { // Get the information about the resulting course and check that it is set up correctly. $modinfo = get_fast_modinfo($newcourseid); $quiz = array_values($modinfo->get_instances_of('quiz'))[0]; - $quizobj = \quiz::create($quiz->instance); + $quizobj = \mod_quiz\quiz_settings::create($quiz->instance); $structure = structure::create_for_quiz($quizobj); // Are the correct slots returned? @@ -354,7 +354,7 @@ class quiz_question_restore_test extends \advanced_testcase { // Get the information about the resulting course and check that it is set up correctly. $modinfo = get_fast_modinfo($newcourseid); $quiz = array_values($modinfo->get_instances_of('quiz'))[0]; - $quizobj = \quiz::create($quiz->instance); + $quizobj = \mod_quiz\quiz_settings::create($quiz->instance); $structure = \mod_quiz\structure::create_for_quiz($quizobj); // Count the questions in quiz qbank. diff --git a/mod/quiz/tests/quiz_question_version_test.php b/mod/quiz/tests/quiz_question_version_test.php index c59250c28d9..dbfe6331346 100644 --- a/mod/quiz/tests/quiz_question_version_test.php +++ b/mod/quiz/tests/quiz_question_version_test.php @@ -68,7 +68,7 @@ class quiz_question_version_test extends \advanced_testcase { $questiongenerator->update_question($numq, null, ['name' => 'This is the third version']); quiz_add_quiz_question($numq->id, $quiz); // Create the quiz object. - $quizobj = \quiz::create($quiz->id); + $quizobj = \mod_quiz\quiz_settings::create($quiz->id); $structure = \mod_quiz\structure::create_for_quiz($quizobj); $slots = $structure->get_slots(); $slot = reset($slots); @@ -151,7 +151,7 @@ class quiz_question_version_test extends \advanced_testcase { list($quizobj, $quba, $attemptobj) = $this->attempt_quiz($quiz, $this->student); $this->assertEquals('This is the third version', $attemptobj->get_question_attempt(1)->get_question()->name); // Create the quiz object. - $quizobj = \quiz::create($quiz->id); + $quizobj = \mod_quiz\quiz_settings::create($quiz->id); $structure = \mod_quiz\structure::create_for_quiz($quizobj); $slots = $structure->get_slots(); $slot = reset($slots); diff --git a/mod/quiz/tests/quizobj_test.php b/mod/quiz/tests/quizobj_test.php index 2877b19cf4e..ce6e4fb2f21 100644 --- a/mod/quiz/tests/quizobj_test.php +++ b/mod/quiz/tests/quizobj_test.php @@ -17,7 +17,7 @@ namespace mod_quiz; use mod_quiz\question\display_options; -use quiz; +use mod_quiz\quiz_settings; defined('MOODLE_INTERNAL') || die(); @@ -41,7 +41,7 @@ class quizobj_test extends \basic_testcase { $cm = new \stdClass(); $cm->id = 123; - $quizobj = new quiz($quiz, $cm, new \stdClass(), false); + $quizobj = new quiz_settings($quiz, $cm, new \stdClass(), false); $this->assertEquals('', $quizobj->cannot_review_message(display_options::DURING)); @@ -54,7 +54,7 @@ class quizobj_test extends \basic_testcase { $closetime = time() + 10000; $quiz->timeclose = $closetime; - $quizobj = new quiz($quiz, $cm, new \stdClass(), false); + $quizobj = new quiz_settings($quiz, $cm, new \stdClass(), false); $this->assertEquals(get_string('noreviewuntil', 'quiz', userdate($closetime)), $quizobj->cannot_review_message(display_options::LATER_WHILE_OPEN)); diff --git a/mod/quiz/tests/repaginate_test.php b/mod/quiz/tests/repaginate_test.php index 51422068c56..d0a98367ff8 100644 --- a/mod/quiz/tests/repaginate_test.php +++ b/mod/quiz/tests/repaginate_test.php @@ -24,7 +24,7 @@ namespace mod_quiz; -use quiz; +use mod_quiz\quiz_settings; defined('MOODLE_INTERNAL') || die(); @@ -113,7 +113,7 @@ class repaginate_test extends \advanced_testcase { quiz_add_quiz_question($match->id, $quiz); // Return the quiz object. - $quizobj = new quiz($quiz, $cm, $SITE); + $quizobj = new quiz_settings($quiz, $cm, $SITE); return structure::create_for_quiz($quizobj); } diff --git a/mod/quiz/tests/reportlib_test.php b/mod/quiz/tests/reportlib_test.php index bcd41c1ae50..b5bcfb7aa82 100644 --- a/mod/quiz/tests/reportlib_test.php +++ b/mod/quiz/tests/reportlib_test.php @@ -19,7 +19,6 @@ namespace mod_quiz; defined('MOODLE_INTERNAL') || die(); global $CFG; -require_once($CFG->dirroot . '/mod/quiz/attemptlib.php'); require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php'); /** diff --git a/mod/quiz/tests/structure_test.php b/mod/quiz/tests/structure_test.php index 5a1c83d28ab..1e8fcc975ff 100644 --- a/mod/quiz/tests/structure_test.php +++ b/mod/quiz/tests/structure_test.php @@ -16,14 +16,6 @@ namespace mod_quiz; -use mod_quiz\question\bank\qbank_helper; -use quiz; - -defined('MOODLE_INTERNAL') || die(); - -global $CFG; -require_once($CFG->dirroot . '/mod/quiz/attemptlib.php'); - /** * Unit tests for quiz events. * @@ -74,7 +66,7 @@ class structure_test extends \advanced_testcase { * The elements in the question array are name, page number, and question type. * * @param array $layout as above. - * @return quiz the created quiz. + * @return quiz_settings the created quiz. */ protected function create_test_quiz($layout) { list($quiz, $cm, $course) = $this->prepare_quiz_data(); @@ -105,7 +97,7 @@ class structure_test extends \advanced_testcase { } } - $quizobj = new quiz($quiz, $cm, $course); + $quizobj = new quiz_settings($quiz, $cm, $course); $structure = structure::create_for_quiz($quizobj); if (isset($headings[1])) { list($heading, $shuffle) = $this->parse_section_name($headings[1]); diff --git a/mod/quiz/tests/tags_test.php b/mod/quiz/tests/tags_test.php index 386d0dcc2d3..e9fdfe6428f 100644 --- a/mod/quiz/tests/tags_test.php +++ b/mod/quiz/tests/tags_test.php @@ -17,7 +17,7 @@ namespace mod_quiz; use mod_quiz\question\bank\qbank_helper; -use quiz; +use mod_quiz\quiz_settings; /** * Test the restore of random question tags. @@ -56,7 +56,7 @@ class tags_test extends \advanced_testcase { // Get the information about the resulting course and check that it is set up correctly. $modinfo = get_fast_modinfo($newcourseid); $quiz = array_values($modinfo->get_instances_of('quiz'))[0]; - $quizobj = quiz::create($quiz->instance); + $quizobj = quiz_settings::create($quiz->instance); $structure = structure::create_for_quiz($quizobj); // Are the correct slots returned? diff --git a/mod/quiz/upgrade.txt b/mod/quiz/upgrade.txt index 68671836b1f..ad49e5c8858 100644 --- a/mod/quiz/upgrade.txt +++ b/mod/quiz/upgrade.txt @@ -49,6 +49,7 @@ This files describes API changes in the quiz code. - quiz_attempt_nav_panel => mod_quiz\output\navigation_panel_attempt - quiz_review_nav_panel => mod_quiz\output\navigation_panel_review - quiz_attempt => mod_quiz\quiz_attempt + - quiz => mod_quiz\quiz_settings * The following classes have been deprecated: - mod_quiz_overdue_attempt_updater - merged into mod_quiz\task\update_overdue_attempts @@ -67,6 +68,7 @@ This files describes API changes in the quiz code. - mod/quiz/override_form.php - mod/quiz/accessrule/accessrulebase.php - mod/quiz/renderer.php + - mod/quiz/attemptlib.php === 4.1 === diff --git a/mod/quiz/view.php b/mod/quiz/view.php index eb1ac271364..7691856833b 100644 --- a/mod/quiz/view.php +++ b/mod/quiz/view.php @@ -27,6 +27,7 @@ use mod_quiz\access_manager; use mod_quiz\output\renderer; use mod_quiz\output\view_page; use mod_quiz\quiz_attempt; +use mod_quiz\quiz_settings; require_once(__DIR__ . '/../../config.php'); require_once($CFG->libdir.'/gradelib.php'); @@ -68,7 +69,7 @@ $canpreview = has_capability('mod/quiz:preview', $context); // Create an object to manage all the other (non-roles) access rules. $timenow = time(); -$quizobj = quiz::create($cm->instance, $USER->id); +$quizobj = quiz_settings::create($cm->instance, $USER->id); $accessmanager = new access_manager($quizobj, $timenow, has_capability('mod/quiz:ignoretimelimits', $context, null, false)); $quiz = $quizobj->get_quiz(); diff --git a/question/bank/statistics/tests/helper_test.php b/question/bank/statistics/tests/helper_test.php index 777cb79a4df..e05d70dabb6 100644 --- a/question/bank/statistics/tests/helper_test.php +++ b/question/bank/statistics/tests/helper_test.php @@ -18,7 +18,7 @@ namespace qbank_statistics; use core_question\statistics\questions\all_calculated_for_qubaid_condition; use mod_quiz\quiz_attempt; -use quiz; +use mod_quiz\quiz_settings; use question_engine; /** @@ -171,7 +171,7 @@ class helper_test extends \advanced_testcase { // Create user. $user = $this->getDataGenerator()->create_user(); // Create attempt. - $quizobj = quiz::create($quiz->id, $user->id); + $quizobj = quiz_settings::create($quiz->id, $user->id); $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); $timenow = time(); diff --git a/question/bank/usage/tests/helper_test.php b/question/bank/usage/tests/helper_test.php index 3f50a0a4966..841b968fc97 100644 --- a/question/bank/usage/tests/helper_test.php +++ b/question/bank/usage/tests/helper_test.php @@ -53,7 +53,7 @@ class helper_test extends \advanced_testcase { $this->quiz = $quizgenerator->create_instance(['course' => $course->id, 'grade' => 100.0, 'sumgrades' => 2, 'layout' => $layout]); - $quizobj = \quiz::create($this->quiz->id, $user->id); + $quizobj = \mod_quiz\quiz_settings::create($this->quiz->id, $user->id); $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); diff --git a/question/bank/usage/tests/question_usage_test.php b/question/bank/usage/tests/question_usage_test.php index 377246a6cb2..aa9f1e62bcd 100644 --- a/question/bank/usage/tests/question_usage_test.php +++ b/question/bank/usage/tests/question_usage_test.php @@ -45,7 +45,7 @@ class question_usage_test extends \advanced_testcase { $quiz = $quizgenerator->create_instance(['course' => $course->id, 'grade' => 100.0, 'sumgrades' => 2, 'layout' => $layout]); - $quizobj = \quiz::create($quiz->id, $user->id); + $quizobj = \mod_quiz\quiz_settings::create($quiz->id, $user->id); $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); From 58e4bc9a3ea36139049797aad7a39f523956067b Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Tue, 20 Dec 2022 11:55:50 +0000 Subject: [PATCH 27/30] MDL-76614 quiz: clean up the quiz_settings class --- mod/quiz/classes/quiz_attempt.php | 3 +- mod/quiz/classes/quiz_settings.php | 96 ++++++++++++----------- mod/quiz/locallib.php | 9 +-- mod/quiz/tests/external/external_test.php | 7 +- mod/quiz/upgrade.txt | 6 +- 5 files changed, 67 insertions(+), 54 deletions(-) diff --git a/mod/quiz/classes/quiz_attempt.php b/mod/quiz/classes/quiz_attempt.php index 26a37db44bc..69ac1b64007 100644 --- a/mod/quiz/classes/quiz_attempt.php +++ b/mod/quiz/classes/quiz_attempt.php @@ -38,7 +38,6 @@ use question_engine; use question_out_of_sequence_exception; use question_state; use question_usage_by_activity; -use mod_quiz\quiz_settings; use stdClass; /** @@ -1079,7 +1078,7 @@ class quiz_attempt { * @return moodle_url quiz view url. */ public function view_url() { - return new moodle_url($this->quizobj->view_url()); + return $this->quizobj->view_url(); } /** diff --git a/mod/quiz/classes/quiz_settings.php b/mod/quiz/classes/quiz_settings.php index 64ac60278b2..de053228c96 100644 --- a/mod/quiz/classes/quiz_settings.php +++ b/mod/quiz/classes/quiz_settings.php @@ -36,9 +36,9 @@ use stdClass; * extra information only when necessary or when asked. The class tracks which questions * are loaded. * - * @copyright 2008 Tim Hunt - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @since Moodle 2.0 + * @package mod_quiz + * @copyright 2008 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class quiz_settings { /** @var stdClass the course settings from the database. */ @@ -63,7 +63,7 @@ class quiz_settings { /** @var bool whether the current user has capability mod/quiz:preview. */ protected $ispreviewuser = null; - // Constructor ============================================================= + // Constructor =============================================================. /** * Constructor, assuming we already have the necessary data loaded. @@ -94,7 +94,7 @@ class quiz_settings { global $DB; $quiz = access_manager::load_quiz_and_settings($quizid); - $course = $DB->get_record('course', array('id' => $quiz->course), '*', MUST_EXIST); + $course = $DB->get_record('course', ['id' => $quiz->course], '*', MUST_EXIST); $cm = get_coursemodule_from_instance('quiz', $quiz->id, $course->id, false, MUST_EXIST); // Update quiz with override information. @@ -115,7 +115,7 @@ class quiz_settings { return new quiz_attempt($attemptdata, $this->quiz, $this->cm, $this->course); } - // Functions for loading more data ========================================= + // Functions for loading more data =========================================. /** * Load just basic information about all the questions in this quiz. @@ -130,7 +130,7 @@ class quiz_settings { /** * Fully load some or all of the questions for this quiz. You must call - * {@link preload_questions()} first. + * {@see preload_questions()} first. * * @param array|null $deprecated no longer supported (it was not used). */ @@ -162,7 +162,7 @@ class quiz_settings { return structure::create_for_quiz($this); } - // Simple getters ========================================================== + // Simple getters ==========================================================. /** * Get the id of the course this quiz belongs to. @@ -218,7 +218,11 @@ class quiz_settings { return $this->quiz->navmethod; } - /** @return int the number of attempts allowed at this quiz (0 = infinite). */ + /** + * How many attepts is the user allowed at this quiz? + * + * @return int the number of attempts allowed at this quiz (0 = infinite). + */ public function get_num_attempts_allowed() { return $this->quiz->attempts; } @@ -251,8 +255,9 @@ class quiz_settings { } /** - * @return bool whether the current user is someone who previews the quiz, - * rather than attempting it. + * Is the current user is someone who previews the quiz, rather than attempting it? + * + * @return bool true user is a preview user. False, if they can do real attempts. */ public function is_preview_user() { if (is_null($this->ispreviewuser)) { @@ -281,6 +286,8 @@ class quiz_settings { } /** + * Have any questions been added to this quiz yet? + * * @return bool whether any questions have been added to this quiz. */ public function has_questions() { @@ -291,6 +298,8 @@ class quiz_settings { } /** + * Get a particular question in this quiz, by its id. + * * @param int $id the question id. * @return stdClass the question object with that id. */ @@ -299,6 +308,8 @@ class quiz_settings { } /** + * Get some of the question in this quiz. + * * @param array|null $questionids question ids of the questions to load. null for all. * @return stdClass[] the question data objects. */ @@ -306,7 +317,7 @@ class quiz_settings { if (is_null($questionids)) { $questionids = array_keys($this->questions); } - $questions = array(); + $questions = []; foreach ($questionids as $id) { if (!array_key_exists($id, $this->questions)) { throw new moodle_exception('cannotstartmissingquestion', 'quiz', $this->view_url()); @@ -326,7 +337,7 @@ class quiz_settings { global $DB; if ($this->sections === null) { $this->sections = array_values($DB->get_records('quiz_sections', - array('quizid' => $this->get_quizid()), 'firstslot')); + ['quizid' => $this->get_quizid()], 'firstslot')); } return $this->sections; } @@ -370,37 +381,39 @@ class quiz_settings { require_capability($capability, $this->context, $userid, $doanything); } - // URLs related to this attempt ============================================ + // URLs related to this attempt ============================================. /** - * @return string the URL of this quiz's view page. + * Get the URL of this quiz's view.php page. + * + * @return moodle_url the URL of this quiz's view page. */ public function view_url() { - global $CFG; - return $CFG->wwwroot . '/mod/quiz/view.php?id=' . $this->cm->id; + return new moodle_url('/mod/quiz/view.php', ['id' => $this->cm->id]); } /** - * @return string the URL of this quiz's edit page. + * Get the URL of this quiz's edit questions page. + * + * @return moodle_url the URL of this quiz's edit page. */ public function edit_url() { - global $CFG; - return $CFG->wwwroot . '/mod/quiz/edit.php?cmid=' . $this->cm->id; + return new moodle_url('/mod/quiz/edit.php', ['cmid' => $this->cm->id]); } /** + * Get the URL of a particular page within an attempt. + * * @param int $attemptid the id of an attempt. * @param int $page optional page number to go to in the attempt. - * @return string the URL of that attempt. + * @return moodle_url the URL of that attempt. */ public function attempt_url($attemptid, $page = 0) { - global $CFG; - $url = $CFG->wwwroot . '/mod/quiz/attempt.php?attempt=' . $attemptid; + $params = ['attempt' => $attemptid, 'cmid' => $this->get_cmid()]; if ($page) { - $url .= '&page=' . $page; + $params['page'] = $page; } - $url .= '&cmid=' . $this->get_cmid(); - return $url; + return new moodle_url('/mod/quiz/attempt.php', $params); } /** @@ -410,7 +423,7 @@ class quiz_settings { * @return moodle_url the URL of this quiz's edit page. Needs to be POSTed to with a cmid parameter. */ public function start_attempt_url($page = 0) { - $params = array('cmid' => $this->cm->id, 'sesskey' => sesskey()); + $params = ['cmid' => $this->cm->id, 'sesskey' => sesskey()]; if ($page) { $params['page'] = $page; } @@ -418,33 +431,26 @@ class quiz_settings { } /** + * Get the URL to review a particular quiz attempt. + * * @param int $attemptid the id of an attempt. * @return string the URL of the review of that attempt. */ public function review_url($attemptid) { - return new moodle_url('/mod/quiz/review.php', array('attempt' => $attemptid, 'cmid' => $this->get_cmid())); + return new moodle_url('/mod/quiz/review.php', ['attempt' => $attemptid, 'cmid' => $this->get_cmid()]); } /** + * Get the URL for the summary page for a particular attempt. + * * @param int $attemptid the id of an attempt. * @return string the URL of the review of that attempt. */ public function summary_url($attemptid) { - return new moodle_url('/mod/quiz/summary.php', array('attempt' => $attemptid, 'cmid' => $this->get_cmid())); + return new moodle_url('/mod/quiz/summary.php', ['attempt' => $attemptid, 'cmid' => $this->get_cmid()]); } - // Bits of content ========================================================= - - /** - * @param bool $notused not used. - * @return string an empty string. - * @deprecated since 3.1. This sort of functionality is now entirely handled by quiz access rules. - */ - public function confirm_start_attempt_message($notused) { - debugging('confirm_start_attempt_message is deprecated. ' . - 'This sort of functionality is now entirely handled by quiz access rules.'); - return ''; - } + // Bits of content =========================================================. /** * If $reviewoptions->attempt is false, meaning that students can't review this @@ -491,7 +497,7 @@ class quiz_settings { return ''; } - // Private methods ========================================================= + // Private methods =========================================================. /** * Check that the definition of a particular question is loaded, and if not throw an exception. @@ -514,10 +520,10 @@ class quiz_settings { * @since Moodle 3.1 */ public function get_all_question_types_used($includepotential = false) { - $questiontypes = array(); + $questiontypes = []; // To control if we need to look in categories for questions. - $qcategories = array(); + $qcategories = []; foreach ($this->get_questions() as $questiondata) { if ($questiondata->qtype === 'random' && $includepotential) { @@ -537,7 +543,7 @@ class quiz_settings { if (!empty($qcategories)) { // We have to look for all the question types in these categories. - $categoriestolook = array(); + $categoriestolook = []; foreach ($qcategories as $cat => $includesubcats) { if ($includesubcats) { $categoriestolook = array_merge($categoriestolook, question_categorylist($cat)); diff --git a/mod/quiz/locallib.php b/mod/quiz/locallib.php index 89c15d61c03..8dc47ffc740 100644 --- a/mod/quiz/locallib.php +++ b/mod/quiz/locallib.php @@ -145,18 +145,17 @@ function quiz_create_attempt(quiz_settings $quizobj, $attemptnumber, $lastattemp /** * Start a normal, new, quiz attempt. * - * @param quiz_settings $quizobj the quiz object to start an attempt for. + * @param quiz_settings $quizobj the quiz object to start an attempt for. * @param question_usage_by_activity $quba * @param object $attempt * @param integer $attemptnumber starting from 1 * @param integer $timenow the attempt start time * @param array $questionids slot number => question id. Used for random questions, to force the choice - * of a particular actual question. Intended for testing purposes only. + * of a particular actual question. Intended for testing purposes only. * @param array $forcedvariantsbyslot slot number => variant. Used for questions with variants, - * to force the choice of a particular variant. Intended for testing - * purposes only. + * to force the choice of a particular variant. Intended for testing + * purposes only. * @return object modified attempt object - *@throws moodle_exception */ function quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $timenow, $questionids = array(), $forcedvariantsbyslot = array()) { diff --git a/mod/quiz/tests/external/external_test.php b/mod/quiz/tests/external/external_test.php index 9541c096337..5b6de7293b9 100644 --- a/mod/quiz/tests/external/external_test.php +++ b/mod/quiz/tests/external/external_test.php @@ -2130,8 +2130,13 @@ class external_test extends externallib_advanced_testcase { * @return quiz_attempt * @throws moodle_exception */ - private function create_quiz_attempt_object(quiz_settings $quizobj, ?int $userid = null, ?bool $ispreview = false): quiz_attempt { + private function create_quiz_attempt_object( + quiz_settings $quizobj, + ?int $userid = null, + ?bool $ispreview = false + ): quiz_attempt { global $USER; + $timenow = time(); // Now, do one attempt. $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); diff --git a/mod/quiz/upgrade.txt b/mod/quiz/upgrade.txt index ad49e5c8858..fc861f29635 100644 --- a/mod/quiz/upgrade.txt +++ b/mod/quiz/upgrade.txt @@ -2,6 +2,10 @@ This files describes API changes in the quiz code. === 4.2 === +* The methods in the quiz_settings class which return a URL now all return a moodle_url. Previously + some returns a moodle_url and others aa string. +* The method quiz_settings::confirm_start_attempt_message, which was deprecated in Moodle 3.1, is now completely removed. +* The field view_page::$startattemptwarning, which was deprecated in Moodle 3.1, is now completely removed. * The quiz has a lot of old classes in lib.php files. These have now been moved into the classes folder, and so are now in namespaces. Because of Moodle's class renaming support, your code should continue working, but output deprecated warnings, so you probably want to update. This should mostly be @@ -67,7 +71,7 @@ This files describes API changes in the quiz code. - mod/quiz/cronlib.php - mod/quiz/override_form.php - mod/quiz/accessrule/accessrulebase.php - - mod/quiz/renderer.php + - mod/quiz/renderer.php - actually, no debugging ouput for this one because of how renderer factories work. - mod/quiz/attemptlib.php From ca7fb1eb028b1cea15ead1e65eb706136ffc3c15 Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Tue, 20 Dec 2022 12:22:04 +0000 Subject: [PATCH 28/30] MDL-76614 quizaccess_seb: rename access_manager & quiz_settings classes This is to avoid confusion between the main mod_quiz classes and SEB's own. --- .../backup_quizaccess_seb_subplugin.class.php | 6 +- ...restore_quizaccess_seb_subplugin.class.php | 6 +- .../seb/classes/event/access_prevented.php | 6 +- .../classes/external/validate_quiz_keys.php | 4 +- mod/quiz/accessrule/seb/classes/helper.php | 2 +- .../seb/classes/privacy/provider.php | 4 +- ...ess_manager.php => seb_access_manager.php} | 8 +- ...uiz_settings.php => seb_quiz_settings.php} | 6 +- .../seb/classes/settings_provider.php | 4 +- mod/quiz/accessrule/seb/classes/template.php | 2 +- mod/quiz/accessrule/seb/db/renamedclasses.php | 31 ++++++ mod/quiz/accessrule/seb/rule.php | 20 ++-- .../seb/tests/access_manager_test.php | 32 +++--- .../seb/tests/backup_restore_test.php | 38 +++---- .../seb/tests/event/events_test.php | 4 +- .../external/validate_quiz_access_test.php | 10 +- .../seb/tests/privacy/provider_test.php | 20 ++-- .../seb/tests/quiz_settings_test.php | 101 +++++++++--------- mod/quiz/accessrule/seb/tests/rule_test.php | 38 +++---- .../seb/tests/settings_provider_test.php | 14 +-- .../accessrule/seb/tests/template_test.php | 2 +- .../seb/tests/test_helper_trait.php | 6 +- mod/quiz/classes/quiz_attempt.php | 2 +- mod/quiz/upgrade.txt | 2 + 24 files changed, 202 insertions(+), 166 deletions(-) rename mod/quiz/accessrule/seb/classes/{access_manager.php => seb_access_manager.php} (97%) rename mod/quiz/accessrule/seb/classes/{quiz_settings.php => seb_quiz_settings.php} (99%) create mode 100644 mod/quiz/accessrule/seb/db/renamedclasses.php diff --git a/mod/quiz/accessrule/seb/backup/moodle2/backup_quizaccess_seb_subplugin.class.php b/mod/quiz/accessrule/seb/backup/moodle2/backup_quizaccess_seb_subplugin.class.php index bc57ed834d6..0d8ba022e66 100644 --- a/mod/quiz/accessrule/seb/backup/moodle2/backup_quizaccess_seb_subplugin.class.php +++ b/mod/quiz/accessrule/seb/backup/moodle2/backup_quizaccess_seb_subplugin.class.php @@ -58,7 +58,7 @@ class backup_quizaccess_seb_subplugin extends backup_mod_quiz_access_subplugin { $subplugintemplatesettings = new backup_nested_element('quizaccess_seb_template', null, $templatekeys); // Get quiz settings keys to save. - $settings = new \quizaccess_seb\quiz_settings(); + $settings = new \quizaccess_seb\seb_quiz_settings(); $blanksettingsarray = (array) $settings->to_record(); unset($blanksettingsarray['id']); // We don't need to save reference to settings record in current instance. // We don't need to save the data about who last modified the settings as they will be overwritten on restore. Also @@ -77,7 +77,7 @@ class backup_quizaccess_seb_subplugin extends backup_mod_quiz_access_subplugin { $subpluginquizsettings->add_child($subplugintemplatesettings); // Set source to populate the settings data by referencing the ID of quiz being backed up. - $subpluginquizsettings->set_source_table(quizaccess_seb\quiz_settings::TABLE, ['quizid' => $quizid]); + $subpluginquizsettings->set_source_table(quizaccess_seb\seb_quiz_settings::TABLE, ['quizid' => $quizid]); $subpluginquizsettings->annotate_files('quizaccess_seb', 'filemanager_sebconfigfile', null); @@ -86,4 +86,4 @@ class backup_quizaccess_seb_subplugin extends backup_mod_quiz_access_subplugin { return $subplugin; } -} \ No newline at end of file +} diff --git a/mod/quiz/accessrule/seb/backup/moodle2/restore_quizaccess_seb_subplugin.class.php b/mod/quiz/accessrule/seb/backup/moodle2/restore_quizaccess_seb_subplugin.class.php index 928fb42943e..1cbdd677f00 100644 --- a/mod/quiz/accessrule/seb/backup/moodle2/restore_quizaccess_seb_subplugin.class.php +++ b/mod/quiz/accessrule/seb/backup/moodle2/restore_quizaccess_seb_subplugin.class.php @@ -24,7 +24,7 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -use quizaccess_seb\quiz_settings; +use quizaccess_seb\seb_quiz_settings; defined('MOODLE_INTERNAL') || die(); @@ -73,7 +73,7 @@ class restore_quizaccess_seb_subplugin extends restore_mod_quiz_access_subplugin unset($data->id); $data->timecreated = $data->timemodified = time(); $data->usermodified = $USER->id; - $DB->insert_record(quizaccess_seb\quiz_settings::TABLE, $data); + $DB->insert_record(quizaccess_seb\seb_quiz_settings::TABLE, $data); // Process attached files. $this->add_related_files('quizaccess_seb', 'filemanager_sebconfigfile', null); @@ -112,7 +112,7 @@ class restore_quizaccess_seb_subplugin extends restore_mod_quiz_access_subplugin } // Update the restored quiz settings to use restored template. - $DB->set_field(\quizaccess_seb\quiz_settings::TABLE, 'templateid', $template->get('id'), ['quizid' => $quizid]); + $DB->set_field(\quizaccess_seb\seb_quiz_settings::TABLE, 'templateid', $template->get('id'), ['quizid' => $quizid]); } } diff --git a/mod/quiz/accessrule/seb/classes/event/access_prevented.php b/mod/quiz/accessrule/seb/classes/event/access_prevented.php index 22aba91ec9f..f9e919d1a56 100644 --- a/mod/quiz/accessrule/seb/classes/event/access_prevented.php +++ b/mod/quiz/accessrule/seb/classes/event/access_prevented.php @@ -26,7 +26,7 @@ namespace quizaccess_seb\event; use core\event\base; -use quizaccess_seb\access_manager; +use quizaccess_seb\seb_access_manager; defined('MOODLE_INTERNAL') || die(); @@ -44,13 +44,13 @@ class access_prevented extends base { * Define strict parameters to create event with instead of relying on internal validation of array. Better code practice. * Easier for consumers of this class to know what data must be supplied and observers can have more trust in event data. * - * @param access_manager $accessmanager Access manager. + * @param seb_access_manager $accessmanager Access manager. * @param string $reason Reason that access was prevented. * @param string|null $configkey A Safe Exam Browser config key. * @param string|null $browserexamkey A Safe Exam Browser browser exam key. * @return base */ - public static function create_strict(access_manager $accessmanager, string $reason, + public static function create_strict(seb_access_manager $accessmanager, string $reason, ?string $configkey = null, ?string $browserexamkey = null) : base { global $USER; diff --git a/mod/quiz/accessrule/seb/classes/external/validate_quiz_keys.php b/mod/quiz/accessrule/seb/classes/external/validate_quiz_keys.php index a5843805a1b..def7a16b3f4 100644 --- a/mod/quiz/accessrule/seb/classes/external/validate_quiz_keys.php +++ b/mod/quiz/accessrule/seb/classes/external/validate_quiz_keys.php @@ -27,7 +27,7 @@ use external_value; use invalid_parameter_exception; use mod_quiz\quiz_settings; use quizaccess_seb\event\access_prevented; -use quizaccess_seb\access_manager; +use quizaccess_seb\seb_access_manager; require_once($CFG->libdir . '/externallib.php'); @@ -95,7 +95,7 @@ class validate_quiz_keys extends external_api { $result = ['configkey' => true, 'browserexamkey' => true]; - $accessmanager = new access_manager(quiz_settings::create($quizid)); + $accessmanager = new seb_access_manager(quiz_settings::create($quizid)); // Check if there is a valid config key. if (!$accessmanager->validate_config_key($configkey, $url)) { diff --git a/mod/quiz/accessrule/seb/classes/helper.php b/mod/quiz/accessrule/seb/classes/helper.php index 2ef33cece4d..65acf56a875 100644 --- a/mod/quiz/accessrule/seb/classes/helper.php +++ b/mod/quiz/accessrule/seb/classes/helper.php @@ -121,7 +121,7 @@ class helper { require_login($cm->course, false, $cm); // Retrieve the config for quiz. - $config = quiz_settings::get_config_by_quiz_id($cm->instance); + $config = seb_quiz_settings::get_config_by_quiz_id($cm->instance); if (empty($config)) { throw new \moodle_exception('noconfigfound', 'quizaccess_seb', '', $cm->id); } diff --git a/mod/quiz/accessrule/seb/classes/privacy/provider.php b/mod/quiz/accessrule/seb/classes/privacy/provider.php index 266481d2f9d..d6a33b36ffc 100644 --- a/mod/quiz/accessrule/seb/classes/privacy/provider.php +++ b/mod/quiz/accessrule/seb/classes/privacy/provider.php @@ -33,7 +33,7 @@ use core_privacy\local\request\contextlist; use core_privacy\local\request\transform; use core_privacy\local\request\userlist; use core_privacy\local\request\writer; -use quizaccess_seb\quiz_settings; +use quizaccess_seb\seb_quiz_settings; use quizaccess_seb\template; defined('MOODLE_INTERNAL') || die(); @@ -162,7 +162,7 @@ class provider implements $index++; $subcontext = [ get_string('pluginname', 'quizaccess_seb'), - quiz_settings::TABLE, + seb_quiz_settings::TABLE, $index ]; diff --git a/mod/quiz/accessrule/seb/classes/access_manager.php b/mod/quiz/accessrule/seb/classes/seb_access_manager.php similarity index 97% rename from mod/quiz/accessrule/seb/classes/access_manager.php rename to mod/quiz/accessrule/seb/classes/seb_access_manager.php index 446e61ad0df..d195155b190 100644 --- a/mod/quiz/accessrule/seb/classes/access_manager.php +++ b/mod/quiz/accessrule/seb/classes/seb_access_manager.php @@ -39,7 +39,7 @@ defined('MOODLE_INTERNAL') || die(); * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class access_manager { +class seb_access_manager { /** Header sent by Safe Exam Browser containing the Config Key hash. */ private const CONFIG_KEY_HEADER = 'HTTP_X_SAFEEXAMBROWSER_CONFIGKEYHASH'; @@ -50,7 +50,7 @@ class access_manager { /** @var quiz_settings $quiz A quiz object containing all information pertaining to current quiz. */ private $quiz; - /** @var quiz_settings $quizsettings A quiz settings persistent object containing plugin settings */ + /** @var seb_quiz_settings $quizsettings A quiz settings persistent object containing plugin settings */ private $quizsettings; /** @var context_module $context Context of this quiz activity. */ @@ -67,8 +67,8 @@ class access_manager { public function __construct(quiz_settings $quiz) { $this->quiz = $quiz; $this->context = context_module::instance($quiz->get_cmid()); - $this->quizsettings = quiz_settings::get_by_quiz_id($quiz->get_quizid()); - $this->validconfigkey = quiz_settings::get_config_key_by_quiz_id($quiz->get_quizid()); + $this->quizsettings = seb_quiz_settings::get_by_quiz_id($quiz->get_quizid()); + $this->validconfigkey = seb_quiz_settings::get_config_key_by_quiz_id($quiz->get_quizid()); } /** diff --git a/mod/quiz/accessrule/seb/classes/quiz_settings.php b/mod/quiz/accessrule/seb/classes/seb_quiz_settings.php similarity index 99% rename from mod/quiz/accessrule/seb/classes/quiz_settings.php rename to mod/quiz/accessrule/seb/classes/seb_quiz_settings.php index 4e87f11bec1..b702acfc9bc 100644 --- a/mod/quiz/accessrule/seb/classes/quiz_settings.php +++ b/mod/quiz/accessrule/seb/classes/seb_quiz_settings.php @@ -43,7 +43,7 @@ defined('MOODLE_INTERNAL') || die(); * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class quiz_settings extends persistent { +class seb_quiz_settings extends persistent { /** Table name for the persistent. */ const TABLE = 'quizaccess_seb_quizsettings'; @@ -193,7 +193,7 @@ class quiz_settings extends persistent { * This method gets data from cache before doing any DB calls. * * @param int $quizid Quiz id. - * @return false|\quizaccess_seb\quiz_settings + * @return false|\quizaccess_seb\seb_quiz_settings */ public static function get_by_quiz_id(int $quizid) { if ($data = self::get_quiz_settings_cache()->get($quizid)) { @@ -567,7 +567,7 @@ class quiz_settings extends persistent { } /** - * Sets the quitURL if found in the mod_quiz\quiz_settings. + * Sets the quitURL if found in the seb_quiz_settings. */ private function process_quit_url_from_settings() { $settings = $this->to_record(); diff --git a/mod/quiz/accessrule/seb/classes/settings_provider.php b/mod/quiz/accessrule/seb/classes/settings_provider.php index 40805bfab1a..c0551ed711d 100644 --- a/mod/quiz/accessrule/seb/classes/settings_provider.php +++ b/mod/quiz/accessrule/seb/classes/settings_provider.php @@ -395,7 +395,7 @@ class settings_provider { self::freeze_element($quizform, $mform, 'seb_showsebdownloadlink'); self::freeze_element($quizform, $mform, 'seb_allowedbrowserexamkeys'); - $quizsettings = quiz_settings::get_by_quiz_id((int) $quizform->get_instance()); + $quizsettings = seb_quiz_settings::get_by_quiz_id((int) $quizform->get_instance()); // If the file has been uploaded, then replace it with the link to download the file. if (!empty($quizsettings) && $quizsettings->get('requiresafeexambrowser') == self::USE_SEB_UPLOAD_CONFIG) { @@ -528,7 +528,7 @@ class settings_provider { return false; } - $settings = quiz_settings::get_record(['cmid' => (int) $context->instanceid]); + $settings = seb_quiz_settings::get_record(['cmid' => (int) $context->instanceid]); if (empty($settings)) { return false; diff --git a/mod/quiz/accessrule/seb/classes/template.php b/mod/quiz/accessrule/seb/classes/template.php index 708bf2aa9e6..c92262ea9c7 100644 --- a/mod/quiz/accessrule/seb/classes/template.php +++ b/mod/quiz/accessrule/seb/classes/template.php @@ -125,7 +125,7 @@ class template extends persistent { $result = true; if ($this->get('id')) { - $settings = quiz_settings::get_records(['templateid' => $this->get('id')]); + $settings = seb_quiz_settings::get_records(['templateid' => $this->get('id')]); $result = empty($settings); } diff --git a/mod/quiz/accessrule/seb/db/renamedclasses.php b/mod/quiz/accessrule/seb/db/renamedclasses.php new file mode 100644 index 00000000000..896867ba502 --- /dev/null +++ b/mod/quiz/accessrule/seb/db/renamedclasses.php @@ -0,0 +1,31 @@ +. + +/** + * This file contains mappings for classes that have been renamed. + * + * @package quizaccess_seb + * @copyright 2022 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$renamedclasses = [ + // Since Moodle 4.2. + 'quizaccess_seb\quiz_settings' => 'quizaccess_seb\seb_quiz_settings', + 'quizaccess_seb\access_manager' => 'quizaccess_seb\seb_access_manager', +]; diff --git a/mod/quiz/accessrule/seb/rule.php b/mod/quiz/accessrule/seb/rule.php index b112fa34c47..3c4e02aa7f0 100644 --- a/mod/quiz/accessrule/seb/rule.php +++ b/mod/quiz/accessrule/seb/rule.php @@ -16,8 +16,8 @@ use mod_quiz\local\access_rule_base; use mod_quiz\quiz_attempt; -use quizaccess_seb\access_manager; -use quizaccess_seb\quiz_settings; +use quizaccess_seb\seb_access_manager; +use quizaccess_seb\seb_quiz_settings; use quizaccess_seb\settings_provider; use quizaccess_seb\event\access_prevented; @@ -32,7 +32,7 @@ use quizaccess_seb\event\access_prevented; */ class quizaccess_seb extends access_rule_base { - /** @var access_manager $accessmanager Instance to manage the access to the quiz for this plugin. */ + /** @var seb_access_manager $accessmanager Instance to manage the access to the quiz for this plugin. */ private $accessmanager; /** @@ -40,9 +40,9 @@ class quizaccess_seb extends access_rule_base { * * @param \mod_quiz\quiz_settings $quizobj information about the quiz in question. * @param int $timenow the time that should be considered as 'now'. - * @param access_manager $accessmanager the quiz accessmanager. + * @param seb_access_manager $accessmanager the quiz accessmanager. */ - public function __construct(\mod_quiz\quiz_settings $quizobj, int $timenow, access_manager $accessmanager) { + public function __construct(\mod_quiz\quiz_settings $quizobj, int $timenow, seb_access_manager $accessmanager) { parent::__construct($quizobj, $timenow); $this->accessmanager = $accessmanager; } @@ -58,7 +58,7 @@ class quizaccess_seb extends access_rule_base { * @return access_rule_base|null the rule, if applicable, else null. */ public static function make(\mod_quiz\quiz_settings $quizobj, $timenow, $canignoretimelimits) { - $accessmanager = new access_manager($quizobj); + $accessmanager = new seb_access_manager($quizobj); // If Safe Exam Browser is not required, this access rule is not applicable. if (!$accessmanager->seb_required()) { return null; @@ -110,7 +110,7 @@ class quizaccess_seb extends access_rule_base { $settings = settings_provider::filter_plugin_settings((object) $data); // Validate basic settings using persistent class. - $quizsettings = (new quiz_settings())->from_record($settings); + $quizsettings = (new seb_quiz_settings())->from_record($settings); // Set non-form fields. $quizsettings->set('quizid', $quizid); $quizsettings->set('cmid', $cmid); @@ -176,9 +176,9 @@ class quizaccess_seb extends access_rule_base { $settings->cmid = $cm->id; // Get existing settings or create new settings if none exist. - $quizsettings = quiz_settings::get_by_quiz_id($quiz->id); + $quizsettings = seb_quiz_settings::get_by_quiz_id($quiz->id); if (empty($quizsettings)) { - $quizsettings = new quiz_settings(0, $settings); + $quizsettings = new seb_quiz_settings(0, $settings); } else { $settings->id = $quizsettings->get('id'); $quizsettings->from_record($settings); @@ -208,7 +208,7 @@ class quizaccess_seb extends access_rule_base { * which is the id of the quiz being deleted. */ public static function delete_settings($quiz) { - $quizsettings = quiz_settings::get_by_quiz_id($quiz->id); + $quizsettings = seb_quiz_settings::get_by_quiz_id($quiz->id); // Check that there are existing settings. if ($quizsettings !== false) { $quizsettings->delete(); diff --git a/mod/quiz/accessrule/seb/tests/access_manager_test.php b/mod/quiz/accessrule/seb/tests/access_manager_test.php index 3a7743add70..610c2c3f5e0 100644 --- a/mod/quiz/accessrule/seb/tests/access_manager_test.php +++ b/mod/quiz/accessrule/seb/tests/access_manager_test.php @@ -27,7 +27,7 @@ require_once(__DIR__ . '/test_helper_trait.php'); * @author Andrew Madden * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @covers \quizaccess_seb\access_manager + * @covers \quizaccess_seb\seb_access_manager */ class access_manager_test extends \advanced_testcase { use \quizaccess_seb_test_helper_trait; @@ -53,7 +53,7 @@ class access_manager_test extends \advanced_testcase { $this->assertFalse($accessmanager->seb_required()); - $reflection = new \ReflectionClass('\quizaccess_seb\access_manager'); + $reflection = new \ReflectionClass('\quizaccess_seb\seb_access_manager'); $property = $reflection->getProperty('quizsettings'); $property->setAccessible(true); @@ -153,7 +153,7 @@ class access_manager_test extends \advanced_testcase { $accessmanager = $this->get_access_manager(); - $configkey = quiz_settings::get_record(['quizid' => $this->quiz->id])->get_config_key(); + $configkey = seb_quiz_settings::get_record(['quizid' => $this->quiz->id])->get_config_key(); // Set up dummy request. $FULLME = 'https://example.com/moodle/mod/quiz/attempt.php?attemptid=123&page=4'; @@ -171,7 +171,7 @@ class access_manager_test extends \advanced_testcase { $url = 'https://www.example.com/moodle'; $accessmanager = $this->get_access_manager(); - $configkey = quiz_settings::get_record(['quizid' => $this->quiz->id])->get_config_key(); + $configkey = seb_quiz_settings::get_record(['quizid' => $this->quiz->id])->get_config_key(); $fullconfigkey = hash('sha256', $url . $configkey); $this->assertTrue($accessmanager->validate_config_key($fullconfigkey, $url)); @@ -202,7 +202,7 @@ class access_manager_test extends \advanced_testcase { public function test_no_browser_exam_keys_cause_check_to_be_successful() { $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG); - $settings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $settings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $settings->set('allowedbrowserexamkeys', ''); $settings->save(); $accessmanager = $this->get_access_manager(); @@ -216,7 +216,7 @@ class access_manager_test extends \advanced_testcase { public function test_access_keys_fail_if_browser_exam_key_header_does_not_exist() { $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG); - $settings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $settings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $settings->set('allowedbrowserexamkeys', hash('sha256', 'one') . "\n" . hash('sha256', 'two')); $settings->save(); $accessmanager = $this->get_access_manager(); @@ -229,7 +229,7 @@ class access_manager_test extends \advanced_testcase { public function test_access_keys_fail_if_browser_exam_key_header_does_not_match_provided_hash() { $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG); - $settings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $settings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $settings->set('allowedbrowserexamkeys', hash('sha256', 'one') . "\n" . hash('sha256', 'two')); $settings->save(); $accessmanager = $this->get_access_manager(); @@ -244,7 +244,7 @@ class access_manager_test extends \advanced_testcase { global $FULLME; $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG); - $settings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $settings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $browserexamkey = hash('sha256', 'browserexamkey'); $settings->set('allowedbrowserexamkeys', $browserexamkey); // Add a hashed BEK. $settings->save(); @@ -263,7 +263,7 @@ class access_manager_test extends \advanced_testcase { public function test_browser_exam_keys_match_provided_browser_exam_key() { $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG); $url = 'https://www.example.com/moodle'; - $settings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $settings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $browserexamkey = hash('sha256', 'browserexamkey'); $fullbrowserexamkey = hash('sha256', $url . $browserexamkey); $settings->set('allowedbrowserexamkeys', $browserexamkey); // Add a hashed BEK. @@ -315,7 +315,7 @@ class access_manager_test extends \advanced_testcase { // Use template. $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); - $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_TEMPLATE); $quizsettings->set('templateid', $this->create_template()->get('id')); $quizsettings->save(); @@ -324,7 +324,7 @@ class access_manager_test extends \advanced_testcase { // Use uploaded config. $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); - $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_UPLOAD_CONFIG); // Doesn't check basic header. $xml = file_get_contents(__DIR__ . '/fixtures/unencrypted.seb'); $this->create_module_test_file($xml, $this->quiz->cmid); @@ -362,7 +362,7 @@ class access_manager_test extends \advanced_testcase { * @dataProvider should_validate_basic_header_data_provider */ public function test_should_validate_basic_header($type, $expected) { - $accessmanager = $this->getMockBuilder(access_manager::class) + $accessmanager = $this->getMockBuilder(seb_access_manager::class) ->disableOriginalConstructor() ->onlyMethods(['get_seb_use_type']) ->getMock(); @@ -396,7 +396,7 @@ class access_manager_test extends \advanced_testcase { * @dataProvider should_validate_config_key_data_provider */ public function test_should_validate_config_key($type, $expected) { - $accessmanager = $this->getMockBuilder(access_manager::class) + $accessmanager = $this->getMockBuilder(seb_access_manager::class) ->disableOriginalConstructor() ->onlyMethods(['get_seb_use_type']) ->getMock(); @@ -429,7 +429,7 @@ class access_manager_test extends \advanced_testcase { * @dataProvider should_validate_browser_exam_key_data_provider */ public function test_should_validate_browser_exam_key($type, $expected) { - $accessmanager = $this->getMockBuilder(access_manager::class) + $accessmanager = $this->getMockBuilder(seb_access_manager::class) ->disableOriginalConstructor() ->onlyMethods(['get_seb_use_type']) ->getMock(); @@ -457,7 +457,7 @@ class access_manager_test extends \advanced_testcase { $this->assertTrue($accessmanager->validate_config_key()); // Change settings (but don't save) and check that still can validate config key. - $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->set('showsebtaskbar', 0); $this->assertNotEquals($quizsettings->get_config_key(), $configkey); $this->assertTrue($accessmanager->validate_config_key()); @@ -479,7 +479,7 @@ class access_manager_test extends \advanced_testcase { $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_NO); $accessmanager = $this->get_access_manager(); - $this->assertEmpty(quiz_settings::get_record(['quizid' => $this->quiz->id])); + $this->assertEmpty(seb_quiz_settings::get_record(['quizid' => $this->quiz->id])); $this->assertNull($accessmanager->get_valid_config_key()); } diff --git a/mod/quiz/accessrule/seb/tests/backup_restore_test.php b/mod/quiz/accessrule/seb/tests/backup_restore_test.php index aa8abe3ed95..15ff6376fc6 100644 --- a/mod/quiz/accessrule/seb/tests/backup_restore_test.php +++ b/mod/quiz/accessrule/seb/tests/backup_restore_test.php @@ -54,11 +54,11 @@ class backup_restore_test extends \advanced_testcase { /** * A helper method to create a quiz with template usage of SEB. * - * @return quiz_settings + * @return seb_quiz_settings */ protected function create_quiz_with_template() { $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); - $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_TEMPLATE); $quizsettings->set('templateid', $this->template->get('id')); $quizsettings->save(); @@ -129,10 +129,10 @@ class backup_restore_test extends \advanced_testcase { * @param cm_info $newcm Restored course_module object. */ protected function validate_backup_restore(\cm_info $newcm) { - $this->assertEquals(2, quiz_settings::count_records()); - $actual = quiz_settings::get_record(['quizid' => $newcm->instance]); + $this->assertEquals(2, seb_quiz_settings::count_records()); + $actual = seb_quiz_settings::get_record(['quizid' => $newcm->instance]); - $expected = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $expected = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $this->assertEquals($expected->get('templateid'), $actual->get('templateid')); $this->assertEquals($expected->get('requiresafeexambrowser'), $actual->get('requiresafeexambrowser')); $this->assertEquals($expected->get('showsebdownloadlink'), $actual->get('showsebdownloadlink')); @@ -152,10 +152,10 @@ class backup_restore_test extends \advanced_testcase { */ public function test_backup_restore_no_seb() { $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_NO); - $this->assertEquals(0, quiz_settings::count_records()); + $this->assertEquals(0, seb_quiz_settings::count_records()); $this->backup_and_restore_quiz(); - $this->assertEquals(0, quiz_settings::count_records()); + $this->assertEquals(0, seb_quiz_settings::count_records()); } /** @@ -164,12 +164,12 @@ class backup_restore_test extends \advanced_testcase { public function test_backup_restore_manual_config() { $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); - $expected = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $expected = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $expected->set('showsebdownloadlink', 0); $expected->set('quitpassword', '123'); $expected->save(); - $this->assertEquals(1, quiz_settings::count_records()); + $this->assertEquals(1, seb_quiz_settings::count_records()); $newcm = $this->backup_and_restore_quiz(); $this->validate_backup_restore($newcm); @@ -181,13 +181,13 @@ class backup_restore_test extends \advanced_testcase { public function test_backup_restore_template_config() { $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); - $expected = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $expected = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $template = $this->create_template(); $expected->set('requiresafeexambrowser', settings_provider::USE_SEB_TEMPLATE); $expected->set('templateid', $template->get('id')); $expected->save(); - $this->assertEquals(1, quiz_settings::count_records()); + $this->assertEquals(1, seb_quiz_settings::count_records()); $newcm = $this->backup_and_restore_quiz(); $this->validate_backup_restore($newcm); @@ -199,13 +199,13 @@ class backup_restore_test extends \advanced_testcase { public function test_backup_restore_uploaded_config() { $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); - $expected = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $expected = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $expected->set('requiresafeexambrowser', settings_provider::USE_SEB_UPLOAD_CONFIG); $xml = file_get_contents(__DIR__ . '/fixtures/unencrypted.seb'); $this->create_module_test_file($xml, $this->quiz->cmid); $expected->save(); - $this->assertEquals(1, quiz_settings::count_records()); + $this->assertEquals(1, seb_quiz_settings::count_records()); $newcm = $this->backup_and_restore_quiz(); $this->validate_backup_restore($newcm); @@ -224,14 +224,14 @@ class backup_restore_test extends \advanced_testcase { $this->create_quiz_with_template(); $backupid = $this->backup_quiz(); - $this->assertEquals(1, quiz_settings::count_records()); + $this->assertEquals(1, seb_quiz_settings::count_records()); $this->assertEquals(1, template::count_records()); $this->change_site(); $this->restore_quiz($backupid); // Should see additional setting record, but no new template record. - $this->assertEquals(2, quiz_settings::count_records()); + $this->assertEquals(2, seb_quiz_settings::count_records()); $this->assertEquals(1, template::count_records()); } @@ -243,7 +243,7 @@ class backup_restore_test extends \advanced_testcase { $this->create_quiz_with_template(); $backupid = $this->backup_quiz(); - $this->assertEquals(1, quiz_settings::count_records()); + $this->assertEquals(1, seb_quiz_settings::count_records()); $this->assertEquals(1, template::count_records()); $this->template->set('name', 'New name for template'); @@ -253,7 +253,7 @@ class backup_restore_test extends \advanced_testcase { $this->restore_quiz($backupid); // Should see additional setting record, and new template record. - $this->assertEquals(2, quiz_settings::count_records()); + $this->assertEquals(2, seb_quiz_settings::count_records()); $this->assertEquals(2, template::count_records()); } @@ -267,7 +267,7 @@ class backup_restore_test extends \advanced_testcase { $this->create_quiz_with_template(); $backupid = $this->backup_quiz(); - $this->assertEquals(1, quiz_settings::count_records()); + $this->assertEquals(1, seb_quiz_settings::count_records()); $this->assertEquals(1, template::count_records()); $newxml = file_get_contents($CFG->dirroot . '/mod/quiz/accessrule/seb/tests/fixtures/simpleunencrypted.seb'); @@ -278,7 +278,7 @@ class backup_restore_test extends \advanced_testcase { $this->restore_quiz($backupid); // Should see additional setting record, and new template record. - $this->assertEquals(2, quiz_settings::count_records()); + $this->assertEquals(2, seb_quiz_settings::count_records()); $this->assertEquals(2, template::count_records()); } diff --git a/mod/quiz/accessrule/seb/tests/event/events_test.php b/mod/quiz/accessrule/seb/tests/event/events_test.php index d5a579fd901..9bd55004d27 100644 --- a/mod/quiz/accessrule/seb/tests/event/events_test.php +++ b/mod/quiz/accessrule/seb/tests/event/events_test.php @@ -53,7 +53,7 @@ class events_test extends \advanced_testcase { $this->setAdminUser(); $quiz = $this->create_test_quiz($this->course, \quizaccess_seb\settings_provider::USE_SEB_CONFIG_MANUALLY); - $accessmanager = new \quizaccess_seb\access_manager(new quiz_settings($quiz, + $accessmanager = new \quizaccess_seb\seb_access_manager(new quiz_settings($quiz, get_coursemodule_from_id('quiz', $quiz->cmid), $this->course)); // Set up event with data. @@ -103,7 +103,7 @@ class events_test extends \advanced_testcase { $this->setAdminUser(); $quiz = $this->create_test_quiz($this->course, \quizaccess_seb\settings_provider::USE_SEB_CONFIG_MANUALLY); - $accessmanager = new \quizaccess_seb\access_manager(new quiz_settings($quiz, + $accessmanager = new \quizaccess_seb\seb_access_manager(new quiz_settings($quiz, get_coursemodule_from_id('quiz', $quiz->cmid), $this->course)); // Set up event with data. diff --git a/mod/quiz/accessrule/seb/tests/external/validate_quiz_access_test.php b/mod/quiz/accessrule/seb/tests/external/validate_quiz_access_test.php index bb6d1791805..727c8076822 100644 --- a/mod/quiz/accessrule/seb/tests/external/validate_quiz_access_test.php +++ b/mod/quiz/accessrule/seb/tests/external/validate_quiz_access_test.php @@ -20,7 +20,7 @@ defined('MOODLE_INTERNAL') || die(); global $CFG; -use quizaccess_seb\quiz_settings; +use quizaccess_seb\seb_quiz_settings; require_once($CFG->libdir . '/externallib.php'); @@ -164,7 +164,7 @@ class validate_quiz_access_test extends \advanced_testcase { $url = 'https://www.example.com/moodle'; // Create the quiz settings. - $quizsettings = new quiz_settings(0, $settings); + $quizsettings = new seb_quiz_settings(0, $settings); $quizsettings->save(); $fullconfigkey = hash('sha256', $url . $quizsettings->get_config_key()); @@ -188,7 +188,7 @@ class validate_quiz_access_test extends \advanced_testcase { ]); // Create the quiz settings. - $quizsettings = new quiz_settings(0, $settings); + $quizsettings = new seb_quiz_settings(0, $settings); $quizsettings->save(); $result = validate_quiz_keys::execute($this->quiz->cmid, 'https://www.example.com/moodle', 'badconfigkey'); @@ -217,7 +217,7 @@ class validate_quiz_access_test extends \advanced_testcase { ]); // Create the quiz settings. - $quizsettings = new quiz_settings(0, $settings); + $quizsettings = new seb_quiz_settings(0, $settings); $quizsettings->save(); $fullbrowserexamkey = hash('sha256', $url . $validbrowserexamkey); @@ -243,7 +243,7 @@ class validate_quiz_access_test extends \advanced_testcase { ]); // Create the quiz settings. - $quizsettings = new quiz_settings(0, $settings); + $quizsettings = new seb_quiz_settings(0, $settings); $quizsettings->save(); $result = validate_quiz_keys::execute($this->quiz->cmid, 'https://www.example.com/moodle', null, diff --git a/mod/quiz/accessrule/seb/tests/privacy/provider_test.php b/mod/quiz/accessrule/seb/tests/privacy/provider_test.php index c817df65e1c..aeae1b69ccf 100644 --- a/mod/quiz/accessrule/seb/tests/privacy/provider_test.php +++ b/mod/quiz/accessrule/seb/tests/privacy/provider_test.php @@ -31,7 +31,7 @@ use core_privacy\local\request\writer; use core_privacy\tests\request\approved_contextlist; use core_privacy\tests\provider_testcase; use quizaccess_seb\privacy\provider; -use quizaccess_seb\quiz_settings; +use quizaccess_seb\seb_quiz_settings; defined('MOODLE_INTERNAL') || die(); @@ -62,7 +62,7 @@ class provider_test extends provider_testcase { $template = $this->create_template(); - $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); // Modify settings so usermodified is updated. This is the user data we are testing for. $quizsettings->set('requiresafeexambrowser', \quizaccess_seb\settings_provider::USE_SEB_TEMPLATE); @@ -126,7 +126,7 @@ class provider_test extends provider_testcase { $index = '1'; // Get first data returned from the quizsettings table metadata. $data = $writer->get_data([ get_string('pluginname', 'quizaccess_seb'), - quiz_settings::TABLE, + seb_quiz_settings::TABLE, $index, ]); $this->assertNotEmpty($data); @@ -142,7 +142,7 @@ class provider_test extends provider_testcase { $index = '2'; // There should not be more than one instance with data. $data = $writer->get_data([ get_string('pluginname', 'quizaccess_seb'), - quiz_settings::TABLE, + seb_quiz_settings::TABLE, $index, ]); $this->assertEmpty($data); @@ -180,11 +180,11 @@ class provider_test extends provider_testcase { 'quizaccess_seb', [$this->user->id]); // Test data exists. - $this->assertNotEmpty(quiz_settings::get_record(['quizid' => $this->quiz->id])); + $this->assertNotEmpty(seb_quiz_settings::get_record(['quizid' => $this->quiz->id])); // Test data is deleted. provider::delete_data_for_users($approveduserlist); - $record = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $record = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $this->assertEmpty($record->get('usermodified')); $template = \quizaccess_seb\template::get_record(['id' => $record->get('templateid')]); @@ -202,11 +202,11 @@ class provider_test extends provider_testcase { 'quizaccess_seb', [$context->id]); // Test data exists. - $this->assertNotEmpty(quiz_settings::get_record(['quizid' => $this->quiz->id])); + $this->assertNotEmpty(seb_quiz_settings::get_record(['quizid' => $this->quiz->id])); // Test data is deleted. provider::delete_data_for_user($approvedcontextlist); - $record = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $record = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $this->assertEmpty($record->get('usermodified')); $template = \quizaccess_seb\template::get_record(['id' => $record->get('templateid')]); @@ -222,7 +222,7 @@ class provider_test extends provider_testcase { $context = \context_module::instance($this->quiz->cmid); // Test data exists. - $record = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $record = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $template = \quizaccess_seb\template::get_record(['id' => $record->get('templateid')]); $this->assertNotEmpty($record->get('usermodified')); $this->assertNotEmpty($template->get('usermodified')); @@ -230,7 +230,7 @@ class provider_test extends provider_testcase { // Test data is deleted. provider::delete_data_for_all_users_in_context($context); - $record = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $record = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $template = \quizaccess_seb\template::get_record(['id' => $record->get('templateid')]); $this->assertEmpty($record->get('usermodified')); $this->assertEmpty($template->get('usermodified')); diff --git a/mod/quiz/accessrule/seb/tests/quiz_settings_test.php b/mod/quiz/accessrule/seb/tests/quiz_settings_test.php index 94f8e2c2465..22432ef5b54 100644 --- a/mod/quiz/accessrule/seb/tests/quiz_settings_test.php +++ b/mod/quiz/accessrule/seb/tests/quiz_settings_test.php @@ -16,12 +16,15 @@ namespace quizaccess_seb; +use context_module; +use moodle_url; + defined('MOODLE_INTERNAL') || die(); require_once(__DIR__ . '/test_helper_trait.php'); /** - * PHPUnit tests for mod_quiz\quiz_settings class. + * PHPUnit tests for seb_quiz_settings class. * * @package quizaccess_seb * @author Andrew Madden @@ -66,7 +69,7 @@ class quiz_settings_test extends \advanced_testcase { ]); // Obtain the existing record that is created when using a generator. - $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); // Update the settings with values from the test function. $quizsettings->from_record($settings); @@ -100,7 +103,7 @@ class quiz_settings_test extends \advanced_testcase { ]); // Obtain the existing record that is created when using a generator. - $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); // Update the settings with values from the test function. $quizsettings->from_record($settings); @@ -145,7 +148,7 @@ class quiz_settings_test extends \advanced_testcase { public function test_config_key_is_created_from_quiz_settings() { $settings = $this->get_test_settings(); - $quizsettings = new quiz_settings(0, $settings); + $quizsettings = new seb_quiz_settings(0, $settings); $configkey = $quizsettings->get_config_key(); $this->assertEquals("65ff7a3b8aec80e58fbe2e7968826c33cbf0ac444a748055ebe665829cbf4201", $configkey @@ -158,7 +161,7 @@ class quiz_settings_test extends \advanced_testcase { public function test_config_key_is_updated_from_quiz_settings() { $settings = $this->get_test_settings(); - $quizsettings = new quiz_settings(0, $settings); + $quizsettings = new seb_quiz_settings(0, $settings); $configkey = $quizsettings->get_config_key(); $this->assertEquals("65ff7a3b8aec80e58fbe2e7968826c33cbf0ac444a748055ebe665829cbf4201", $configkey); @@ -178,7 +181,7 @@ class quiz_settings_test extends \advanced_testcase { * @dataProvider filter_rules_provider */ public function test_filter_rules_added_to_config(\stdClass $settings, string $expectedxml) { - $quizsettings = new quiz_settings(0, $settings); + $quizsettings = new seb_quiz_settings(0, $settings); $config = $quizsettings->get_config(); $this->assertEquals($expectedxml, $config); } @@ -187,7 +190,7 @@ class quiz_settings_test extends \advanced_testcase { * Test that browser keys are validated and retrieved as an array instead of string. */ public function test_browser_exam_keys_are_retrieved_as_array() { - $quizsettings = new quiz_settings(); + $quizsettings = new seb_quiz_settings(); $quizsettings->set('allowedbrowserexamkeys', "one two,three\nfour"); $retrievedkeys = $quizsettings->get('allowedbrowserexamkeys'); $this->assertEquals(['one', 'two', 'three', 'four'], $retrievedkeys); @@ -202,7 +205,7 @@ class quiz_settings_test extends \advanced_testcase { * @dataProvider bad_browser_exam_key_provider */ public function test_browser_exam_keys_validation_errors($bek, $expectederrorstring) { - $quizsettings = new quiz_settings(); + $quizsettings = new seb_quiz_settings(); $quizsettings->set('allowedbrowserexamkeys', $bek); $quizsettings->validate(); $errors = $quizsettings->get_errors(); @@ -220,7 +223,7 @@ class quiz_settings_test extends \advanced_testcase { . "allowWlanstartURL$url" . "sendBrowserExamKeybrowserWindowWebView3\n"; $itemid = $this->create_module_test_file($xml, $this->quiz->cmid); - $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_UPLOAD_CONFIG); $quizsettings->save(); $config = $quizsettings->get_config(); @@ -231,7 +234,7 @@ class quiz_settings_test extends \advanced_testcase { * Test test_no_config_file_uploaded */ public function test_no_config_file_uploaded() { - $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_UPLOAD_CONFIG); $cmid = $quizsettings->get('cmid'); $this->expectException(\moodle_exception::class); @@ -279,7 +282,7 @@ class quiz_settings_test extends \advanced_testcase { $this->assertStringContainsString("allowQuit", $template->get('content')); $this->assertStringContainsString("hashedQuitPasswordpassword", $template->get('content')); - $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_TEMPLATE); $quizsettings->set('templateid', $template->get('id')); $quizsettings->set('allowuserquitseb', 1); @@ -318,7 +321,7 @@ class quiz_settings_test extends \advanced_testcase { $this->assertStringNotContainsString("allowQuit", $template->get('content')); $this->assertStringNotContainsString("hashedQuitPasswordpassword", $template->get('content')); - $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_TEMPLATE); $quizsettings->set('templateid', $template->get('id')); $quizsettings->set('allowuserquitseb', 1); @@ -347,7 +350,7 @@ class quiz_settings_test extends \advanced_testcase { $xml = $this->get_config_xml(true, 'password'); $this->create_module_test_file($xml, $this->quiz->cmid); - $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_UPLOAD_CONFIG); $quizsettings->set('allowuserquitseb', 0); $quizsettings->set('quitpassword', ''); @@ -384,7 +387,7 @@ class quiz_settings_test extends \advanced_testcase { $xml = $this->get_config_xml(); $this->create_module_test_file($xml, $this->quiz->cmid); - $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_UPLOAD_CONFIG); $quizsettings->set('allowuserquitseb', 1); $quizsettings->set('quitpassword', ''); @@ -424,7 +427,7 @@ class quiz_settings_test extends \advanced_testcase { $template = $this->create_template($xml); - $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_TEMPLATE); $quizsettings->set('templateid', $template->get('id')); @@ -446,7 +449,7 @@ class quiz_settings_test extends \advanced_testcase { . "sendBrowserExamKey\n"; $itemid = $this->create_module_test_file($xml, $this->quiz->cmid); - $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_UPLOAD_CONFIG); $this->assertEmpty($quizsettings->get('linkquitseb')); @@ -460,7 +463,7 @@ class quiz_settings_test extends \advanced_testcase { * Test template id set correctly. */ public function test_templateid_set_correctly_when_save_settings() { - $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $this->assertEquals(0, $quizsettings->get('templateid')); $template = $this->create_template(); @@ -468,12 +471,12 @@ class quiz_settings_test extends \advanced_testcase { // Initially set to USE_SEB_TEMPLATE with a template id. $this->save_settings_with_optional_template($quizsettings, settings_provider::USE_SEB_TEMPLATE, $templateid); - $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $this->assertEquals($templateid, $quizsettings->get('templateid')); // Case for USE_SEB_NO, ensure template id reverts to 0. $this->save_settings_with_optional_template($quizsettings, settings_provider::USE_SEB_NO); - $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $this->assertEquals(0, $quizsettings->get('templateid')); // Reverting back to USE_SEB_TEMPLATE. @@ -481,7 +484,7 @@ class quiz_settings_test extends \advanced_testcase { // Case for USE_SEB_CONFIG_MANUALLY, ensure template id reverts to 0. $this->save_settings_with_optional_template($quizsettings, settings_provider::USE_SEB_CONFIG_MANUALLY); - $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $this->assertEquals(0, $quizsettings->get('templateid')); // Reverting back to USE_SEB_TEMPLATE. @@ -489,7 +492,7 @@ class quiz_settings_test extends \advanced_testcase { // Case for USE_SEB_CLIENT_CONFIG, ensure template id reverts to 0. $this->save_settings_with_optional_template($quizsettings, settings_provider::USE_SEB_CLIENT_CONFIG); - $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $this->assertEquals(0, $quizsettings->get('templateid')); // Reverting back to USE_SEB_TEMPLATE. @@ -499,19 +502,19 @@ class quiz_settings_test extends \advanced_testcase { $xml = file_get_contents(__DIR__ . '/fixtures/unencrypted.seb'); $this->create_module_test_file($xml, $this->quiz->cmid); $this->save_settings_with_optional_template($quizsettings, settings_provider::USE_SEB_UPLOAD_CONFIG); - $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $this->assertEquals(0, $quizsettings->get('templateid')); // Case for USE_SEB_TEMPLATE, ensure template id is correct. $this->save_settings_with_optional_template($quizsettings, settings_provider::USE_SEB_TEMPLATE, $templateid); - $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $this->assertEquals($templateid, $quizsettings->get('templateid')); } /** * Helper function in tests to set USE_SEB_TEMPLATE and a template id on the quiz settings. * - * @param quiz_settings $quizsettings Given quiz settings instance. + * @param seb_quiz_settings $quizsettings Given quiz settings instance. * @param int $savetype Type of SEB usage. * @param int $templateid Template ID. */ @@ -695,13 +698,13 @@ class quiz_settings_test extends \advanced_testcase { * Test that config and config key are null when expected. */ public function test_generates_config_values_as_null_when_expected() { - $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $this->assertNotNull($quizsettings->get_config()); $this->assertNotNull($quizsettings->get_config_key()); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_NO); $quizsettings->save(); - $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $this->assertNull($quizsettings->get_config()); $this->assertNull($quizsettings->get_config()); @@ -709,20 +712,20 @@ class quiz_settings_test extends \advanced_testcase { $xml = file_get_contents(__DIR__ . '/fixtures/unencrypted.seb'); $this->create_module_test_file($xml, $this->quiz->cmid); $quizsettings->save(); - $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $this->assertNotNull($quizsettings->get_config()); $this->assertNotNull($quizsettings->get_config_key()); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_CLIENT_CONFIG); $quizsettings->save(); - $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $this->assertNull($quizsettings->get_config()); $this->assertNull($quizsettings->get_config_key()); $template = $this->create_template(); $templateid = $template->get('id'); $this->save_settings_with_optional_template($quizsettings, settings_provider::USE_SEB_TEMPLATE, $templateid); - $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $this->assertNotNull($quizsettings->get_config()); $this->assertNotNull($quizsettings->get_config_key()); } @@ -731,7 +734,7 @@ class quiz_settings_test extends \advanced_testcase { * Test that quizsettings cache exists after creation. */ public function test_quizsettings_cache_exists_after_creation() { - $expected = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $expected = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $this->assertEquals($expected->to_record(), \cache::make('quizaccess_seb', 'quizsettings')->get($this->quiz->id)); } @@ -741,30 +744,30 @@ class quiz_settings_test extends \advanced_testcase { public function test_quizsettings_cache_purged_after_deletion() { $this->assertNotEmpty(\cache::make('quizaccess_seb', 'quizsettings')->get($this->quiz->id)); - $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->delete(); $this->assertFalse(\cache::make('quizaccess_seb', 'quizsettings')->get($this->quiz->id)); } /** - * Test that we can get mod_quiz\quiz_settings by quiz id. + * Test that we can get seb_quiz_settings by quiz id. */ public function test_get_quiz_settings_by_quiz_id() { - $expected = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $expected = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); - $this->assertEquals($expected->to_record(), quiz_settings::get_by_quiz_id($this->quiz->id)->to_record()); + $this->assertEquals($expected->to_record(), seb_quiz_settings::get_by_quiz_id($this->quiz->id)->to_record()); // Check that data is getting from cache. $expected->set('showsebtaskbar', 0); - $this->assertNotEquals($expected->to_record(), quiz_settings::get_by_quiz_id($this->quiz->id)->to_record()); + $this->assertNotEquals($expected->to_record(), seb_quiz_settings::get_by_quiz_id($this->quiz->id)->to_record()); // Now save and check that cached as been updated. $expected->save(); - $this->assertEquals($expected->to_record(), quiz_settings::get_by_quiz_id($this->quiz->id)->to_record()); + $this->assertEquals($expected->to_record(), seb_quiz_settings::get_by_quiz_id($this->quiz->id)->to_record()); // Returns false for non existing quiz. - $this->assertFalse(quiz_settings::get_by_quiz_id(7777777)); + $this->assertFalse(seb_quiz_settings::get_by_quiz_id(7777777)); } /** @@ -780,7 +783,7 @@ class quiz_settings_test extends \advanced_testcase { public function test_config_cache_purged_after_deletion() { $this->assertNotEmpty(\cache::make('quizaccess_seb', 'config')->get($this->quiz->id)); - $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->delete(); $this->assertFalse(\cache::make('quizaccess_seb', 'config')->get($this->quiz->id)); @@ -790,21 +793,21 @@ class quiz_settings_test extends \advanced_testcase { * Test that we can get SEB config by quiz id. */ public function test_get_config_by_quiz_id() { - $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $expected = $quizsettings->get_config(); - $this->assertEquals($expected, quiz_settings::get_config_by_quiz_id($this->quiz->id)); + $this->assertEquals($expected, seb_quiz_settings::get_config_by_quiz_id($this->quiz->id)); // Check that data is getting from cache. $quizsettings->set('showsebtaskbar', 0); - $this->assertNotEquals($quizsettings->get_config(), quiz_settings::get_config_by_quiz_id($this->quiz->id)); + $this->assertNotEquals($quizsettings->get_config(), seb_quiz_settings::get_config_by_quiz_id($this->quiz->id)); // Now save and check that cached as been updated. $quizsettings->save(); - $this->assertEquals($quizsettings->get_config(), quiz_settings::get_config_by_quiz_id($this->quiz->id)); + $this->assertEquals($quizsettings->get_config(), seb_quiz_settings::get_config_by_quiz_id($this->quiz->id)); // Returns null for non existing quiz. - $this->assertNull(quiz_settings::get_config_by_quiz_id(7777777)); + $this->assertNull(seb_quiz_settings::get_config_by_quiz_id(7777777)); } /** @@ -820,7 +823,7 @@ class quiz_settings_test extends \advanced_testcase { public function test_config_key_cache_purged_after_deletion() { $this->assertNotEmpty(\cache::make('quizaccess_seb', 'configkey')->get($this->quiz->id)); - $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->delete(); $this->assertFalse(\cache::make('quizaccess_seb', 'configkey')->get($this->quiz->id)); @@ -830,21 +833,21 @@ class quiz_settings_test extends \advanced_testcase { * Test that we can get SEB config key by quiz id. */ public function test_get_config_key_by_quiz_id() { - $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $expected = $quizsettings->get_config_key(); - $this->assertEquals($expected, quiz_settings::get_config_key_by_quiz_id($this->quiz->id)); + $this->assertEquals($expected, seb_quiz_settings::get_config_key_by_quiz_id($this->quiz->id)); // Check that data is getting from cache. $quizsettings->set('showsebtaskbar', 0); - $this->assertNotEquals($quizsettings->get_config_key(), quiz_settings::get_config_key_by_quiz_id($this->quiz->id)); + $this->assertNotEquals($quizsettings->get_config_key(), seb_quiz_settings::get_config_key_by_quiz_id($this->quiz->id)); // Now save and check that cached as been updated. $quizsettings->save(); - $this->assertEquals($quizsettings->get_config_key(), quiz_settings::get_config_key_by_quiz_id($this->quiz->id)); + $this->assertEquals($quizsettings->get_config_key(), seb_quiz_settings::get_config_key_by_quiz_id($this->quiz->id)); // Returns null for non existing quiz. - $this->assertNull(quiz_settings::get_config_key_by_quiz_id(7777777)); + $this->assertNull(seb_quiz_settings::get_config_key_by_quiz_id(7777777)); } } diff --git a/mod/quiz/accessrule/seb/tests/rule_test.php b/mod/quiz/accessrule/seb/tests/rule_test.php index b36c20b24c9..88184f08591 100644 --- a/mod/quiz/accessrule/seb/tests/rule_test.php +++ b/mod/quiz/accessrule/seb/tests/rule_test.php @@ -285,7 +285,7 @@ class rule_test extends \advanced_testcase { $this->quiz->seb_requiresafeexambrowser = settings_provider::USE_SEB_NO; quizaccess_seb::save_settings($this->quiz); - $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $this->assertEquals(settings_provider::USE_SEB_CONFIG_MANUALLY, $quizsettings->get('requiresafeexambrowser')); } @@ -388,7 +388,7 @@ class rule_test extends \advanced_testcase { $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); // Set quiz setting to require seb and save BEK. - $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_UPLOAD_CONFIG); $xml = file_get_contents(__DIR__ . '/fixtures/unencrypted.seb'); $this->create_module_test_file($xml, $this->quiz->cmid); @@ -414,7 +414,7 @@ class rule_test extends \advanced_testcase { $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); // Set quiz setting to require seb and save BEK. - $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_TEMPLATE); $quizsettings->set('templateid', $this->create_template()->get('id')); $quizsettings->save(); @@ -442,7 +442,7 @@ class rule_test extends \advanced_testcase { $this->setUser($user); // Set quiz setting to require seb. - $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); // Set up dummy request. $FULLME = 'https://example.com/moodle/mod/quiz/attempt.php?attemptid=123&page=4'; @@ -463,7 +463,7 @@ class rule_test extends \advanced_testcase { $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); // Set quiz setting to require seb and save BEK. - $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_TEMPLATE); $quizsettings->set('templateid', $this->create_template()->get('id')); $quizsettings->save(); @@ -490,7 +490,7 @@ class rule_test extends \advanced_testcase { $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); // Set quiz setting to require seb and save BEK. - $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_UPLOAD_CONFIG); $xml = file_get_contents(__DIR__ . '/fixtures/unencrypted.seb'); $this->create_module_test_file($xml, $this->quiz->cmid); @@ -522,7 +522,7 @@ class rule_test extends \advanced_testcase { // Set quiz setting to require seb and save BEK. $browserexamkey = hash('sha256', 'testkey'); - $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_CLIENT_CONFIG); // Doesn't check config key. $quizsettings->set('allowedbrowserexamkeys', $browserexamkey); $quizsettings->save(); @@ -548,7 +548,7 @@ class rule_test extends \advanced_testcase { // Set quiz setting to require seb and save BEK. $browserexamkey = hash('sha256', 'testkey'); - $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_UPLOAD_CONFIG); $quizsettings->set('allowedbrowserexamkeys', $browserexamkey); $xml = file_get_contents(__DIR__ . '/fixtures/unencrypted.seb'); @@ -643,7 +643,7 @@ class rule_test extends \advanced_testcase { // Set quiz setting to require seb and save BEK. $browserexamkey = hash('sha256', 'testkey'); - $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_CLIENT_CONFIG); // Doesn't check config key. $quizsettings->set('allowedbrowserexamkeys', $browserexamkey); $quizsettings->save(); @@ -666,7 +666,7 @@ class rule_test extends \advanced_testcase { // Set quiz setting to require seb and save BEK. $browserexamkey = hash('sha256', 'testkey'); - $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_UPLOAD_CONFIG); $quizsettings->set('allowedbrowserexamkeys', $browserexamkey); $xml = file_get_contents(__DIR__ . '/fixtures/unencrypted.seb'); @@ -698,7 +698,7 @@ class rule_test extends \advanced_testcase { // Set quiz setting to require seb and save BEK. $browserexamkey = hash('sha256', 'testkey'); - $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_TEMPLATE); $quizsettings->set('allowedbrowserexamkeys', $browserexamkey); $quizsettings->set('templateid', $this->create_template()->get('id')); @@ -730,7 +730,7 @@ class rule_test extends \advanced_testcase { $this->setUser($user); // Set quiz setting to require seb. - $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_CLIENT_CONFIG); // Doesn't check config key. $quizsettings->save(); @@ -752,7 +752,7 @@ class rule_test extends \advanced_testcase { $this->setUser($user); // Set quiz setting to require seb. - $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_CLIENT_CONFIG); // Doesn't check config key. $quizsettings->save(); @@ -795,7 +795,7 @@ class rule_test extends \advanced_testcase { $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); // Set quiz setting to require seb. - $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_UPLOAD_CONFIG); // Doesn't check basic header. $xml = file_get_contents(__DIR__ . '/fixtures/unencrypted.seb'); $this->create_module_test_file($xml, $this->quiz->cmid); @@ -824,7 +824,7 @@ class rule_test extends \advanced_testcase { $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); // Set quiz setting to require seb. - $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_TEMPLATE); $quizsettings->set('templateid', $this->create_template()->get('id')); $quizsettings->save(); @@ -853,7 +853,7 @@ class rule_test extends \advanced_testcase { $this->setUser($user); // Set quiz setting to not require seb. - $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_NO); $quizsettings->save(); @@ -1044,7 +1044,7 @@ class rule_test extends \advanced_testcase { $method = $reflection->getMethod('get_action_buttons'); $method->setAccessible(true); - $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); // Should see link when using manually. $this->assertStringContainsString($this->get_seb_launch_link(), $method->invoke($this->make_rule())); @@ -1161,7 +1161,7 @@ class rule_test extends \advanced_testcase { $this->setAdminUser(); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); - $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $user = $this->getDataGenerator()->create_user(); $roleid = $this->getDataGenerator()->create_role(); @@ -1268,7 +1268,7 @@ class rule_test extends \advanced_testcase { $this->setAdminUser(); $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); - $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $quizsettings->save(); // Set access for Moodle session. $SESSION->quizaccess_seb_access = [$this->quiz->cmid => true]; diff --git a/mod/quiz/accessrule/seb/tests/settings_provider_test.php b/mod/quiz/accessrule/seb/tests/settings_provider_test.php index 7f63c36393d..bf6c7f6b014 100644 --- a/mod/quiz/accessrule/seb/tests/settings_provider_test.php +++ b/mod/quiz/accessrule/seb/tests/settings_provider_test.php @@ -111,7 +111,7 @@ class settings_provider_test extends \advanced_testcase { * Test that settings types to be added to quiz settings, are part of quiz_settings persistent class. */ public function test_setting_elements_are_part_of_quiz_settings_table() { - $dbsettings = (array) (new quiz_settings())->to_record(); + $dbsettings = (array) (new seb_quiz_settings())->to_record(); $settingelements = settings_provider::get_seb_config_elements(); $settingelements = (array) $this->strip_all_prefixes((object) $settingelements); @@ -704,7 +704,7 @@ class settings_provider_test extends \advanced_testcase { $template = $this->create_template(); - $settings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $settings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $settings->set('templateid', $template->get('id')); $settings->set('requiresafeexambrowser', settings_provider::USE_SEB_TEMPLATE); $settings->save(); @@ -731,7 +731,7 @@ class settings_provider_test extends \advanced_testcase { // Setup conflicting permissions. $template = $this->create_template(); - $settings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $settings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $settings->set('templateid', $template->get('id')); $settings->set('requiresafeexambrowser', settings_provider::USE_SEB_TEMPLATE); $settings->save(); @@ -794,7 +794,7 @@ class settings_provider_test extends \advanced_testcase { $template = $this->create_template(); - $settings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $settings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $settings->set('templateid', $template->get('id')); $settings->set('requiresafeexambrowser', settings_provider::USE_SEB_TEMPLATE); $settings->save(); @@ -989,7 +989,7 @@ class settings_provider_test extends \advanced_testcase { settings_provider::save_filemanager_sebconfigfile_draftarea($draftitemid, $this->quiz->cmid); - $settings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $settings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $settings->set('requiresafeexambrowser', settings_provider::USE_SEB_UPLOAD_CONFIG); $settings->save(); @@ -1167,7 +1167,7 @@ class settings_provider_test extends \advanced_testcase { // Create a template. $template = $this->create_template(); - $settings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $settings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $settings->set('templateid', $template->get('id')); $settings->set('requiresafeexambrowser', settings_provider::USE_SEB_TEMPLATE); $settings->save(); @@ -1197,7 +1197,7 @@ class settings_provider_test extends \advanced_testcase { $xml = file_get_contents(__DIR__ . '/fixtures/unencrypted.seb'); $draftitemid = $this->create_test_draftarea_file($xml); settings_provider::save_filemanager_sebconfigfile_draftarea($draftitemid, $this->quiz->cmid); - $settings = quiz_settings::get_record(['quizid' => $this->quiz->id]); + $settings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); $settings->set('requiresafeexambrowser', settings_provider::USE_SEB_UPLOAD_CONFIG); $settings->save(); diff --git a/mod/quiz/accessrule/seb/tests/template_test.php b/mod/quiz/accessrule/seb/tests/template_test.php index dbc8f62a8bd..c3105607e4d 100644 --- a/mod/quiz/accessrule/seb/tests/template_test.php +++ b/mod/quiz/accessrule/seb/tests/template_test.php @@ -108,7 +108,7 @@ class template_test extends \advanced_testcase { $template->save(); $this->assertTrue($template->can_delete()); - $DB->insert_record(quiz_settings::TABLE, (object) [ + $DB->insert_record(seb_quiz_settings::TABLE, (object) [ 'quizid' => 1, 'cmid' => 1, 'templateid' => $template->get('id'), diff --git a/mod/quiz/accessrule/seb/tests/test_helper_trait.php b/mod/quiz/accessrule/seb/tests/test_helper_trait.php index aca36de4408..7ae25d5df5e 100644 --- a/mod/quiz/accessrule/seb/tests/test_helper_trait.php +++ b/mod/quiz/accessrule/seb/tests/test_helper_trait.php @@ -25,7 +25,7 @@ use mod_quiz\local\access_rule_base; use mod_quiz\quiz_attempt; -use quizaccess_seb\access_manager; +use quizaccess_seb\seb_access_manager; use quizaccess_seb\settings_provider; defined('MOODLE_INTERNAL') || die(); @@ -239,10 +239,10 @@ trait quizaccess_seb_test_helper_trait { /** * Get access manager for testing. * - * @return \quizaccess_seb\access_manager + * @return \quizaccess_seb\seb_access_manager */ protected function get_access_manager() { - return new access_manager(new mod_quiz\quiz_settings($this->quiz, + return new seb_access_manager(new mod_quiz\quiz_settings($this->quiz, get_coursemodule_from_id('quiz', $this->quiz->cmid), $this->course)); } diff --git a/mod/quiz/classes/quiz_attempt.php b/mod/quiz/classes/quiz_attempt.php index 69ac1b64007..ff91e247501 100644 --- a/mod/quiz/classes/quiz_attempt.php +++ b/mod/quiz/classes/quiz_attempt.php @@ -292,7 +292,7 @@ class quiz_attempt { } /** - * Get the {@see quiz_settings} object for this quiz. + * Get the {@see seb_quiz_settings} object for this quiz. * * @return quiz_settings */ diff --git a/mod/quiz/upgrade.txt b/mod/quiz/upgrade.txt index fc861f29635..1ada6364d99 100644 --- a/mod/quiz/upgrade.txt +++ b/mod/quiz/upgrade.txt @@ -54,6 +54,8 @@ This files describes API changes in the quiz code. - quiz_review_nav_panel => mod_quiz\output\navigation_panel_review - quiz_attempt => mod_quiz\quiz_attempt - quiz => mod_quiz\quiz_settings + - quizaccess_seb\quiz_settings => quizaccess_seb\seb_quiz_settings + - quizaccess_seb\access_manager => quizaccess_seb\seb_access_manager * The following classes have been deprecated: - mod_quiz_overdue_attempt_updater - merged into mod_quiz\task\update_overdue_attempts From a02497f94267093f6124da03ce178cb8c3cd20e0 Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Tue, 20 Dec 2022 15:52:53 +0000 Subject: [PATCH 29/30] MDL-76614 quiz: clean up view.php script --- mod/quiz/view.php | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/mod/quiz/view.php b/mod/quiz/view.php index 7691856833b..28c88c85ead 100644 --- a/mod/quiz/view.php +++ b/mod/quiz/view.php @@ -40,20 +40,20 @@ $q = optional_param('q', 0, PARAM_INT); // Quiz ID. if ($id) { if (!$cm = get_coursemodule_from_id('quiz', $id)) { - throw new \moodle_exception('invalidcoursemodule'); + throw new moodle_exception('invalidcoursemodule'); } - if (!$course = $DB->get_record('course', array('id' => $cm->course))) { - throw new \moodle_exception('coursemisconf'); + if (!$course = $DB->get_record('course', ['id' => $cm->course])) { + throw new moodle_exception('coursemisconf'); } } else { - if (!$quiz = $DB->get_record('quiz', array('id' => $q))) { - throw new \moodle_exception('invalidquizid', 'quiz'); + if (!$quiz = $DB->get_record('quiz', ['id' => $q])) { + throw new moodle_exception('invalidquizid', 'quiz'); } - if (!$course = $DB->get_record('course', array('id' => $quiz->course))) { - throw new \moodle_exception('invalidcourseid'); + if (!$course = $DB->get_record('course', ['id' => $quiz->course])) { + throw new moodle_exception('invalidcourseid'); } if (!$cm = get_coursemodule_from_instance("quiz", $quiz->id, $course->id)) { - throw new \moodle_exception('invalidcoursemodule'); + throw new moodle_exception('invalidcoursemodule'); } } @@ -78,7 +78,7 @@ $quiz = $quizobj->get_quiz(); quiz_view($quiz, $course, $cm, $context); // Initialize $PAGE, compute blocks. -$PAGE->set_url('/mod/quiz/view.php', array('id' => $cm->id)); +$PAGE->set_url('/mod/quiz/view.php', ['id' => $cm->id]); // Create view object which collects all the information the renderer will need. $viewobj = new view_page(); @@ -108,7 +108,7 @@ if ($unfinishedattempt = quiz_get_user_attempt_unfinished($quiz->id, $USER->id)) $numattempts = count($attempts); $viewobj->attempts = $attempts; -$viewobj->attemptobjs = array(); +$viewobj->attemptobjs = []; foreach ($attempts as $attempt) { $viewobj->attemptobjs[] = new quiz_attempt($attempt, $quiz, $cm, $course, false); } @@ -179,8 +179,8 @@ $viewobj->mygradeoverridden = $mygradeoverridden; $viewobj->gradebookfeedback = $gradebookfeedback; $viewobj->lastfinishedattempt = $lastfinishedattempt; $viewobj->canedit = has_capability('mod/quiz:manage', $context); -$viewobj->editurl = new moodle_url('/mod/quiz/edit.php', array('cmid' => $cm->id)); -$viewobj->backtocourseurl = new moodle_url('/course/view.php', array('id' => $course->id)); +$viewobj->editurl = new moodle_url('/mod/quiz/edit.php', ['cmid' => $cm->id]); +$viewobj->backtocourseurl = new moodle_url('/course/view.php', ['id' => $course->id]); $viewobj->startattempturl = $quizobj->start_attempt_url(); if ($accessmanager->is_preflight_check_required($unfinishedattemptid)) { @@ -205,9 +205,9 @@ if ($item && grade_floats_different($item->gradepass, 0)) { $viewobj->infomessages[] = get_string('gradetopassoutof', 'quiz', $a); } -// Determine wheter a start attempt button should be displayed. +// Determine whether a start attempt button should be displayed. $viewobj->quizhasquestions = $quizobj->has_questions(); -$viewobj->preventmessages = array(); +$viewobj->preventmessages = []; if (!$viewobj->quizhasquestions) { $viewobj->buttontext = ''; From 7e4972a4a8d14387cae7e1afe255c81363f6a85f Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Mon, 9 Jan 2023 17:48:26 +0000 Subject: [PATCH 30/30] fixup! MDL-76614 quiz: deprecate class moodle_quiz_exception --- mod/quiz/deprecatedlib.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mod/quiz/deprecatedlib.php b/mod/quiz/deprecatedlib.php index 69982742a76..2fa17695f14 100644 --- a/mod/quiz/deprecatedlib.php +++ b/mod/quiz/deprecatedlib.php @@ -187,6 +187,8 @@ class moodle_quiz_exception extends moodle_exception { * @deprecated since Moodle 4.2. Please just use moodle_exception. */ public function __construct($quizobj, $errorcode, $a = null, $link = '', $debuginfo = null) { + debugging('Class moodle_quiz_exception is deprecated. ' . + 'Please use a standard moodle_exception instead.', DEBUG_DEVELOPER); if (!$link) { $link = $quizobj->view_url(); }