diff --git a/admin/tool/recyclebin/tests/course_bin_test.php b/admin/tool/recyclebin/tests/course_bin_test.php
index 821ceedf7492a..853a88c0c5569 100644
--- a/admin/tool/recyclebin/tests/course_bin_test.php
+++ b/admin/tool/recyclebin/tests/course_bin_test.php
@@ -16,6 +16,9 @@
namespace tool_recyclebin;
+use mod_quiz\quiz_attempt;
+use stdClass;
+
/**
* Recycle bin course tests.
*
@@ -237,7 +240,7 @@ public function test_coursemodule_restore_with_userdata($settings) {
$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());
}
@@ -300,17 +303,17 @@ private function create_quiz_attempt($quiz, $student) {
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();
$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 8ff878c55d335..3832d4d16b011 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;
@@ -379,7 +381,7 @@ public function test_question_attempt_steps_date_restore() {
// 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());
@@ -393,7 +395,7 @@ public function test_question_attempt_steps_date_restore() {
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 @@ public function test_question_attempt_steps_date_restore() {
$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 = [];
@@ -419,7 +421,7 @@ public function test_question_attempt_steps_date_restore() {
// 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 19de3ddc505d1..23325c85bed42 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/accessmanager.php b/mod/quiz/accessmanager.php
index f1db4ba549b5d..df1c42fe9f106 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/accessmanager_form.php b/mod/quiz/accessmanager_form.php
index e2e6fc5e24186..df1c42fe9f106 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 d849512207673..5c4c8c7455c3c 100644
--- a/mod/quiz/accessrule/accessrulebase.php
+++ b/mod/quiz/accessrule/accessrulebase.php
@@ -15,322 +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+.
*/
-
-
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 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 mod_quiz_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,
- 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.
- }
+debugging('This file is no longer required in Moodle 4.2+. Please do not include/require it.', DEBUG_DEVELOPER);
- /**
- * 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 probalby to read the code of {@link 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
- * 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();
- }
-}
+require_once($CFG->dirroot . '/mod/quiz/locallib.php');
diff --git a/mod/quiz/accessrule/delaybetweenattempts/rule.php b/mod/quiz/accessrule/delaybetweenattempts/rule.php
index c5153aa0b2190..f2cf9d1f1fefa 100644
--- a/mod/quiz/accessrule/delaybetweenattempts/rule.php
+++ b/mod/quiz/accessrule/delaybetweenattempts/rule.php
@@ -14,30 +14,19 @@
// 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;
+use mod_quiz\quiz_settings;
/**
* 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) {
+ 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 812bc9228ac6f..72c902813af75 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 @@ public function test_just_first_delay() {
$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 @@ public function test_just_second_delay() {
$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 @@ public function test_just_both_delays() {
$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 @@ public function test_with_close_date() {
$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 @@ public function test_time_limit_and_overdue() {
$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 fa666a1e2e823..e0d78c914d7ac 100644
--- a/mod/quiz/accessrule/ipaddress/rule.php
+++ b/mod/quiz/accessrule/ipaddress/rule.php
@@ -14,30 +14,19 @@
// 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;
+use mod_quiz\quiz_settings;
/**
* 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) {
+ 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 c5ff7a6eec808..9a4d73df39d08 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 @@ public function test_ipaddress_access_rule() {
// 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 @@ public function test_ipaddress_access_rule() {
}
$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 e69faf74bfb1c..3c87006e4d705 100644
--- a/mod/quiz/accessrule/numattempts/rule.php
+++ b/mod/quiz/accessrule/numattempts/rule.php
@@ -14,30 +14,19 @@
// 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;
+use mod_quiz\quiz_settings;
/**
* 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) {
+ 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 c500b0a792ebd..ada155122436d 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 @@ public function test_num_attempts_access_rule() {
$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 511be30f145e7..aaa3fe0053e78 100644
--- a/mod/quiz/accessrule/offlineattempts/rule.php
+++ b/mod/quiz/accessrule/offlineattempts/rule.php
@@ -14,29 +14,21 @@
// 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
- */
-
-
-defined('MOODLE_INTERNAL') || die();
-
-require_once($CFG->dirroot . '/mod/quiz/accessrule/accessrulebase.php');
+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.
*
+ * @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) {
+ 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.
@@ -63,7 +55,7 @@ public function is_preflight_check_required($attemptid) {
}
}
- 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/offlineattempts/tests/rule_test.php b/mod/quiz/accessrule/offlineattempts/tests/rule_test.php
index ea314b37d5aca..85e08089c3e57 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 @@ public function test_offlineattempts_access_rule() {
$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 2bb70dac93afb..59ff513d10345 100644
--- a/mod/quiz/accessrule/openclosedate/rule.php
+++ b/mod/quiz/accessrule/openclosedate/rule.php
@@ -14,30 +14,19 @@
// 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;
+use mod_quiz\quiz_settings;
/**
* 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) {
+ 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 5cfd2801db03b..afa7940abd418 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 @@ public function test_no_dates() {
$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 @@ public function test_start_date() {
$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 @@ public function test_close_date() {
$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 @@ public function test_both_dates() {
$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 @@ public function test_close_date_with_overdue() {
$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 778e39028916d..956fc7b83653b 100644
--- a/mod/quiz/accessrule/password/rule.php
+++ b/mod/quiz/accessrule/password/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_password plugin.
- *
- * @package quizaccess
- * @subpackage password
- * @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\form\preflight_check_form;
+use mod_quiz\local\access_rule_base;
+use mod_quiz\quiz_settings;
/**
* 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) {
+ public static function make(quiz_settings $quizobj, $timenow, $canignoretimelimits) {
if (empty($quizobj->get_quiz()->password)) {
return null;
}
@@ -54,7 +44,7 @@ public function is_preflight_check_required($attemptid) {
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/password/tests/rule_test.php b/mod/quiz/accessrule/password/tests/rule_test.php
index 0c5b6eace19c9..fb4c779e7069d 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 @@ public function test_password_access_rule() {
$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/backup/moodle2/backup_quizaccess_seb_subplugin.class.php b/mod/quiz/accessrule/seb/backup/moodle2/backup_quizaccess_seb_subplugin.class.php
index bc57ed834d6a7..0d8ba022e666e 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 @@ protected function define_quiz_subplugin_structure() {
$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 @@ protected function define_quiz_subplugin_structure() {
$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 @@ protected function define_quiz_subplugin_structure() {
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 928fb42943e77..1cbdd677f0089 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 @@ public function process_quizaccess_seb_quizsettings($data) {
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 @@ public function process_quizaccess_seb_template($data) {
}
// 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 22aba91ec9f5c..f9e919d1a560a 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 2375e18ec6384..def7a16b3f428 100644
--- a/mod/quiz/accessrule/seb/classes/external/validate_quiz_keys.php
+++ b/mod/quiz/accessrule/seb/classes/external/validate_quiz_keys.php
@@ -25,12 +25,10 @@
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;
+use quizaccess_seb\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');
/**
@@ -97,7 +95,7 @@ public static function execute(string $cmid, string $url, ?string $configkey = n
$result = ['configkey' => true, 'browserexamkey' => true];
- $accessmanager = new access_manager(quiz::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 2ef33cece4d9a..65acf56a87515 100644
--- a/mod/quiz/accessrule/seb/classes/helper.php
+++ b/mod/quiz/accessrule/seb/classes/helper.php
@@ -121,7 +121,7 @@ public static function get_seb_config_content(string $cmid) : string {
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 266481d2f9d15..d6a33b36ffcaa 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\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 @@ public static function export_user_data(approved_contextlist $contextlist) {
$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 94%
rename from mod/quiz/accessrule/seb/classes/access_manager.php
rename to mod/quiz/accessrule/seb/classes/seb_access_manager.php
index 58f47b1020529..d195155b1902c 100644
--- a/mod/quiz/accessrule/seb/classes/access_manager.php
+++ b/mod/quiz/accessrule/seb/classes/seb_access_manager.php
@@ -29,7 +29,7 @@
namespace quizaccess_seb;
use context_module;
-use quiz;
+use mod_quiz\quiz_settings;
defined('MOODLE_INTERNAL') || die();
@@ -39,7 +39,7 @@
* @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';
@@ -47,10 +47,10 @@ 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 */
+ /** @var seb_quiz_settings $quizsettings A quiz settings persistent object containing plugin settings */
private $quizsettings;
/** @var context_module $context Context of this quiz activity. */
@@ -62,13 +62,13 @@ 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());
- $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());
}
/**
@@ -219,9 +219,9 @@ public function get_valid_config_key(): ?string {
/**
* 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/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 923fdb33d6d18..b702acfc9bce6 100644
--- a/mod/quiz/accessrule/seb/classes/quiz_settings.php
+++ b/mod/quiz/accessrule/seb/classes/seb_quiz_settings.php
@@ -43,7 +43,7 @@
* @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 @@ protected static function define_properties() : array {
* 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 @@ private function process_quit_password_settings() {
}
/**
- * Sets the quitURL if found in the 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 40805bfab1ab9..c0551ed711dec 100644
--- a/mod/quiz/accessrule/seb/classes/settings_provider.php
+++ b/mod/quiz/accessrule/seb/classes/settings_provider.php
@@ -395,7 +395,7 @@ protected static function lock_seb_elements(\mod_quiz_mod_form $quizform, \Moodl
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 @@ public static function is_conflicting_permissions(\context $context) {
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 708bf2aa9e69d..c92262ea9c739 100644
--- a/mod/quiz/accessrule/seb/classes/template.php
+++ b/mod/quiz/accessrule/seb/classes/template.php
@@ -125,7 +125,7 @@ public function can_delete() : bool {
$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 0000000000000..896867ba502f4
--- /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 1772a818237d0..3c4e02aa7f009 100644
--- a/mod/quiz/accessrule/seb/rule.php
+++ b/mod/quiz/accessrule/seb/rule.php
@@ -14,6 +14,13 @@
// 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 mod_quiz\quiz_attempt;
+use quizaccess_seb\seb_access_manager;
+use quizaccess_seb\seb_quiz_settings;
+use quizaccess_seb\settings_provider;
+use quizaccess_seb\event\access_prevented;
+
/**
* Implementation of the quizaccess_seb plugin.
*
@@ -23,36 +30,19 @@
* @copyright 2019 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
+class quizaccess_seb extends access_rule_base {
-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 {
-
- /** @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;
/**
* 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.
+ * @param seb_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, seb_access_manager $accessmanager) {
parent::__construct($quizobj, $timenow);
$this->accessmanager = $accessmanager;
}
@@ -61,14 +51,14 @@ public function __construct(quiz $quizobj, int $timenow, access_manager $accessm
* 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 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) {
- $accessmanager = new access_manager($quizobj);
+ public static function make(\mod_quiz\quiz_settings $quizobj, $timenow, $canignoretimelimits) {
+ $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;
@@ -120,7 +110,7 @@ public static function validate_settings_form_fields(array $errors,
$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);
@@ -186,9 +176,9 @@ public static function save_settings($quiz) {
$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);
@@ -218,7 +208,7 @@ public static function save_settings($quiz) {
* 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();
@@ -228,7 +218,7 @@ public static function delete_settings($quiz) {
/**
* 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/accessrule/seb/tests/access_manager_test.php b/mod/quiz/accessrule/seb/tests/access_manager_test.php
index 3a7743add7049..610c2c3f5e002 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 @@
* @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 @@ public function test_access_manager_quizsettings_null() {
$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 @@ public function test_access_keys_validate_with_config_key() {
$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 @@ public function test_access_keys_validate_with_provided_config_key() {
$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 @@ public function test_config_key_not_checked_if_client_requirement_is_selected()
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 @@ public function test_no_browser_exam_keys_cause_check_to_be_successful() {
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 @@ public function test_access_keys_fail_if_browser_exam_key_header_does_not_exist(
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 @@ public function test_browser_exam_keys_match_header_hash() {
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 @@ public function test_browser_exam_keys_match_header_hash() {
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 @@ public function test_get_seb_use_type() {
// 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 @@ public function test_get_seb_use_type() {
// 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 @@ public function should_validate_basic_header_data_provider() {
* @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 @@ public function should_validate_config_key_data_provider() {
* @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 @@ public function should_validate_browser_exam_key_data_provider() {
* @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 @@ public function test_access_manager_uses_cached_config_key() {
$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 @@ public function test_valid_config_key_is_null_if_no_settings() {
$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 aa8abe3ed9587..15ff6376fc639 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 @@ public function setUp(): void {
/**
* 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 @@ protected function change_site() {
* @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 @@ protected function validate_backup_restore(\cm_info $newcm) {
*/
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 @@ public function test_backup_restore_no_seb() {
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 @@ public function test_backup_restore_manual_config() {
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 @@ public function test_backup_restore_template_config() {
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 @@ public function test_restore_template_to_a_different_site_when_the_same_template
$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 @@ public function test_restore_template_to_a_different_site_when_the_same_content_
$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 @@ public function test_restore_template_to_a_different_site_when_the_same_content_
$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 @@ public function test_restore_template_to_a_different_site_when_the_same_name_but
$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 @@ public function test_restore_template_to_a_different_site_when_the_same_name_but
$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 f96c951d97422..9bd55004d2772 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 @@ public function test_event_access_prevented() {
$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\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 @@ public function test_event_access_prevented_with_keys() {
$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\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 bb6d17918059b..727c80768228b 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 @@
global $CFG;
-use quizaccess_seb\quiz_settings;
+use quizaccess_seb\seb_quiz_settings;
require_once($CFG->libdir . '/externallib.php');
@@ -164,7 +164,7 @@ public function test_config_key_valid() {
$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 @@ public function test_config_key_not_valid() {
]);
// 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 @@ public function test_browser_exam_key_valid() {
]);
// 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 @@ public function test_browser_exam_key_not_valid() {
]);
// 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 c817df65e1c92..aeae1b69ccf3b 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\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 @@ public function setup_test_data() {
$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 @@ public function test_export_user_data() {
$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 @@ public function test_export_user_data() {
$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 @@ public function test_delete_data_for_users() {
'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 @@ public function test_delete_data_for_user() {
'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 @@ public function test_delete_data_for_all_users_in_context() {
$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 @@ public function test_delete_data_for_all_users_in_context() {
// 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 57335a77f8de5..22432ef5b549a 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 quiz_settings class.
+ * PHPUnit tests for seb_quiz_settings class.
*
* @package quizaccess_seb
* @author Andrew Madden
@@ -66,7 +69,7 @@ public function test_config_is_created_from_quiz_settings() {
]);
// 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 @@ public function test_config_is_updated_from_quiz_settings() {
]);
// 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 @@ public function test_config_is_updated_from_quiz_settings() {
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 @@ public function test_config_key_is_created_from_quiz_settings() {
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 @@ public function test_config_key_is_updated_from_quiz_settings() {
* @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 @@ public function test_filter_rules_added_to_config(\stdClass $settings, string $e
* 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 @@ public function test_browser_exam_keys_are_retrieved_as_array() {
* @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 @@ public function test_config_file_uploaded_converted_to_config() {
. "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 @@ public function test_config_file_uploaded_converted_to_config() {
* 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 @@ public function test_using_seb_template_override_settings_when_they_set_in_templ
$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 @@ public function test_using_seb_template_override_settings_when_not_set_in_templa
$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 @@ public function test_using_own_config_settings_are_not_overridden_if_set() {
$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 @@ public function test_using_own_config_settings_are_not_overridden_if_not_set() {
$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 @@ public function test_template_has_quit_url_set() {
$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 @@ public function test_config_file_uploaded_has_quit_url_set() {
. "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 @@ public function test_config_file_uploaded_has_quit_url_set() {
* 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 @@ public function test_templateid_set_correctly_when_save_settings() {
// 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 @@ public function test_templateid_set_correctly_when_save_settings() {
// 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 @@ public function test_templateid_set_correctly_when_save_settings() {
// 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 @@ public function test_templateid_set_correctly_when_save_settings() {
$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 @@ public function filter_rules_provider() : array {
* 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 @@ public function test_generates_config_values_as_null_when_expected() {
$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 @@ public function test_generates_config_values_as_null_when_expected() {
* 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 @@ public function test_quizsettings_cache_exists_after_creation() {
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 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 @@ public function test_config_cache_exists_after_creation() {
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 @@ public function test_config_cache_purged_after_deletion() {
* 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 @@ public function test_config_key_cache_exists_after_creation() {
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 @@ public function test_config_key_cache_purged_after_deletion() {
* 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 b36c20b24c9ab..88184f085911a 100644
--- a/mod/quiz/accessrule/seb/tests/rule_test.php
+++ b/mod/quiz/accessrule/seb/tests/rule_test.php
@@ -285,7 +285,7 @@ public function test_settings_are_not_saved_if_conflicting_permissions() {
$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 @@ public function test_access_prevented_if_config_key_invalid_uploaded_config() {
$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 @@ public function test_access_prevented_if_config_key_invalid_uploaded_template()
$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 @@ public function test_access_allowed_if_config_key_valid() {
$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 @@ public function test_access_allowed_if_config_key_valid_uploaded_config() {
$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 @@ public function test_access_allowed_if_config_key_valid_template() {
$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 @@ public function test_access_allowed_if_browser_exam_keys_valid() {
// 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 @@ public function test_access_allowed_if_browser_exam_keys_valid_use_uploaded_file
// 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 @@ public function test_access_prevented_if_browser_exam_keys_are_invalid() {
// 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 @@ public function test_access_prevented_if_browser_exam_keys_are_invalid_use_uploa
// 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 @@ public function test_access_prevented_if_browser_exam_keys_are_invalid_use_templ
// 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 @@ public function test_access_allowed_if_using_client_config_basic_header_is_valid
$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 @@ public function test_access_prevented_if_using_client_configuration_and_basic_he
$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 @@ public function test_access_allowed_if_using_client_configuration_and_basic_head
$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 @@ public function test_access_allowed_if_using_client_configuration_and_basic_head
$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 @@ public function test_access_allowed_if_seb_not_required() {
$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 @@ public function test_get_get_action_buttons_shows_launch_and_download_config_lin
$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 @@ public function test_description_shows_download_config_link_when_required() {
$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 @@ public function test_current_attempt_finished() {
$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 7f63c36393d41..bf6c7f6b014d5 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 @@ public function settings_capability_data_provider() {
* 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 @@ public function test_get_requiresafeexambrowser_options_with_conflicting_permiss
$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 @@ public function test_form_elements_are_frozen_if_conflicting_permissions() {
// 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 @@ public function test_form_elements_are_locked_when_quiz_attempted_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();
@@ -989,7 +989,7 @@ public function test_get_module_context_sebconfig_file() {
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 @@ public function test_is_conflicting_permissions_for_manage_templates() {
// 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 @@ public function test_is_conflicting_permissions_for_upload_seb_file() {
$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 dbc8f62a8bd2a..c3105607e4db4 100644
--- a/mod/quiz/accessrule/seb/tests/template_test.php
+++ b/mod/quiz/accessrule/seb/tests/template_test.php
@@ -108,7 +108,7 @@ public function test_cannot_delete_template_when_assigned_to_quiz() {
$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 f09c2a7a51e43..7ae25d5df5e7a 100644
--- a/mod/quiz/accessrule/seb/tests/test_helper_trait.php
+++ b/mod/quiz/accessrule/seb/tests/test_helper_trait.php
@@ -23,7 +23,9 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-use quizaccess_seb\access_manager;
+use mod_quiz\local\access_rule_base;
+use mod_quiz\quiz_attempt;
+use quizaccess_seb\seb_access_manager;
use quizaccess_seb\settings_provider;
defined('MOODLE_INTERNAL') || die();
@@ -189,7 +191,7 @@ protected function attempt_quiz($quiz, $user) {
$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);
@@ -200,7 +202,7 @@ protected function attempt_quiz($quiz, $user) {
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'],
@@ -210,7 +212,7 @@ protected function attempt_quiz($quiz, $user) {
$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();
@@ -237,21 +239,21 @@ public function create_template(string $xml = null) {
/**
* 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 \quiz($this->quiz,
+ return new seb_access_manager(new mod_quiz\quiz_settings($this->quiz,
get_coursemodule_from_id('quiz', $this->quiz->cmid), $this->course));
}
/**
* 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(
- 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 1857dbcd7e5a2..6aa636b6b8245 100644
--- a/mod/quiz/accessrule/securewindow/rule.php
+++ b/mod/quiz/accessrule/securewindow/rule.php
@@ -14,29 +14,18 @@
// 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;
+use mod_quiz\quiz_settings;
/**
* 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,
@@ -52,7 +41,7 @@ class quizaccess_securewindow extends quiz_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 0b0c3e3c60f4d..cfc5117e46fd4 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();
@@ -32,7 +32,7 @@
* @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 {
@@ -42,7 +42,7 @@ public function test_securewindow_access_rule() {
$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 ea06139880130..610c384a4a57a 100644
--- a/mod/quiz/accessrule/timelimit/rule.php
+++ b/mod/quiz/accessrule/timelimit/rule.php
@@ -14,31 +14,23 @@
// 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
- */
-
-
-defined('MOODLE_INTERNAL') || die();
-
-require_once($CFG->dirroot . '/mod/quiz/accessrule/accessrulebase.php');
-
+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. 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) {
+ public static function make(quiz_settings $quizobj, $timenow, $canignoretimelimits) {
if (empty($quizobj->get_quiz()->timelimit) || $canignoretimelimits) {
return null;
@@ -74,7 +66,7 @@ public function is_preflight_check_required($attemptid) {
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/timelimit/tests/rule_test.php b/mod/quiz/accessrule/timelimit/tests/rule_test.php
index 36a075bc8413b..ab04271c2ced7 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 @@ public function test_time_limit_access_rule() {
$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 @@ public function test_time_limit_access_rule_with_time_close($timetoclose, $timel
$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/accessrule/upgrade.txt b/mod/quiz/accessrule/upgrade.txt
index f883b6e114397..26a866c73e9d5 100644
--- a/mod/quiz/accessrule/upgrade.txt
+++ b/mod/quiz/accessrule/upgrade.txt
@@ -2,6 +2,17 @@ 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.
+* 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 ===
* New static method delete_settings for access rules, which is called when a
diff --git a/mod/quiz/addrandom.php b/mod/quiz/addrandom.php
index 3839ca47c068f..8d57c0d6149a5 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 @@
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 d16121fc0178b..df1c42fe9f106 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/attempt.php b/mod/quiz/attempt.php
index ecd85256505a4..163b8277ca46f 100644
--- a/mod/quiz/attempt.php
+++ b/mod/quiz/attempt.php
@@ -22,6 +22,10 @@
* @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;
+use mod_quiz\quiz_attempt;
+
require_once(__DIR__ . '/../../config.php');
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
@@ -57,7 +61,7 @@
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());
}
}
@@ -82,6 +86,7 @@
// 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) {
@@ -107,7 +112,7 @@
// 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.
@@ -121,7 +126,7 @@
\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 54475f1981461..90919b9f7ee06 100644
--- a/mod/quiz/attemptlib.php
+++ b/mod/quiz/attemptlib.php
@@ -14,2978 +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
- */
-
-defined('MOODLE_INTERNAL') || die();
-
-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.
- *
- * 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 quiz_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 = 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 {@link 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 quiz_access_manager and instance of the quiz_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
- * 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,
- 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_quiz_exception($this, 'questionnotloaded', $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;
- }
-}
-
-
-/**
- * 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 = 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 quiz_access_manager and instance of the quiz_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 mod_quiz_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) {
- 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 mod_quiz_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) {
- $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 mod_quiz_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) {
- 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 mod_quiz_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,
- $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 {@link mod_quiz_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.
- * 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 mod_quiz_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 {@link mod_quiz_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
- * 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,
- * 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 mod_quiz_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;
- }
-}
-
-
-/**
- * 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 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.
- *
- * @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 quiz_nav_section_heading(format_string($heading));
- }
- }
-
- $qa = $this->attemptobj->get_question_attempt($slot);
- $showcorrectness = $this->options->correctness && $qa->has_marks();
-
- $button = new quiz_nav_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 mod_quiz_renderer $output the quiz renderer to use.
- * @return string HTML to output.
- */
- public function render_before_button_bits(mod_quiz_renderer $output) {
- return '';
- }
-
- abstract public function render_end_bits(mod_quiz_renderer $output);
-
- /**
- * Render the restart preview button.
- *
- * @param mod_quiz_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.
+ * File only retained to prevent fatal errors in code that tries to require/include this.
*
- * @copyright 2008 Tim Hunt
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- * @since Moodle 2.0
+ * @todo MDL-76612 delete this file as part of Moodle 4.6 development.
+ * @deprecated This file is no longer required in Moodle 4.2+.
*/
-class quiz_attempt_nav_panel extends quiz_nav_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(mod_quiz_renderer $output) {
- return html_writer::tag('div', get_string('navnojswarning', 'quiz'),
- array('id' => 'quiznojswarning'));
- }
-
- public function render_end_bits(mod_quiz_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 {@link quiz_nav_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 {
- public function get_question_url($slot) {
- return $this->attemptobj->review_url($slot, -1, $this->showall, $this->page);
- }
+defined('MOODLE_INTERNAL') || die();
- public function render_end_bits(mod_quiz_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;
- }
-}
+debugging('This file is no longer required in Moodle 4.2+. Please do not include/require it.', DEBUG_DEVELOPER);
diff --git a/mod/quiz/autosave.ajax.php b/mod/quiz/autosave.ajax.php
index c2f38179f4837..6600650f6b818 100644
--- a/mod/quiz/autosave.ajax.php
+++ b/mod/quiz/autosave.ajax.php
@@ -45,7 +45,7 @@
// 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 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/access_manager.php b/mod/quiz/classes/access_manager.php
new file mode 100644
index 0000000000000..7e4ddb9e0a386
--- /dev/null
+++ b/mod/quiz/classes/access_manager.php
@@ -0,0 +1,586 @@
+.
+
+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\output\renderer;
+use moodle_page;
+use moodle_url;
+use MoodleQuickForm;
+use stdClass;
+
+/**
+ * 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
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since Moodle 2.2
+ */
+class access_manager {
+ /** @var quiz_settings the quiz settings object. */
+ protected $quizobj;
+
+ /** @var int the time to be considered as 'now'. */
+ protected $timenow;
+
+ /** @var access_rule_base instances of the active rules for this quiz. */
+ protected $rules = [];
+
+ /**
+ * Create an instance for a particular quiz.
+ *
+ * @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_settings $quizobj, int $timenow, bool $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_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_settings $quizobj, int $timenow, bool $canignoretimelimits): array {
+
+ $rules = [];
+ foreach (self::get_rule_classes() as $ruleclass) {
+ $rule = $ruleclass::make($quizobj, $timenow, $canignoretimelimits);
+ if ($rule) {
+ $rules[$ruleclass] = $rule;
+ }
+ }
+
+ $superceededrules = [];
+ foreach ($rules as $rule) {
+ $superceededrules += $rule->get_superceded_rules();
+ }
+
+ foreach ($superceededrules as $superceededrule) {
+ unset($rules['quizaccess_' . $superceededrule]);
+ }
+
+ return $rules;
+ }
+
+ /**
+ * Get that names of all the installed rule classes.
+ *
+ * @return array of class names.
+ */
+ protected static function get_rule_classes(): array {
+ 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): void {
+
+ 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(): array {
+ $options = ['-' => 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 {@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.
+ * @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, 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);
+ }
+
+ 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 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(stdClass $quiz): void {
+
+ 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 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(stdClass $quiz): void {
+
+ 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 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(int $quizid, array $rules, string $basefields): array {
+ $allfields = $basefields;
+ $alljoins = '{quiz} quiz';
+ $allparams = ['quizid' => $quizid];
+
+ foreach ($rules as $rule) {
+ [$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 ['', []];
+ }
+
+ return ["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(int $quizid): array {
+ global $DB;
+
+ $rules = self::get_rule_classes();
+ [$sql, $params] = self::get_load_sql($quizid, $rules, '');
+
+ if ($sql) {
+ $data = (array) $DB->get_record_sql($sql, $params);
+ } else {
+ $data = [];
+ }
+
+ 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 stdClass mdl_quiz row with extra fields.
+ */
+ public static function load_quiz_and_settings(int $quizid): stdClass {
+ global $DB;
+
+ $rules = self::get_rule_classes();
+ [$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;
+ }
+
+ /**
+ * Get an array of the class names of all the active rules.
+ *
+ * Mainly useful for debugging.
+ *
+ * @return array
+ */
+ public function get_active_rule_names(): array {
+ $classnames = [];
+ 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(array $messages, $new): array {
+ 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(): array {
+ $result = [];
+ foreach ($this->rules as $rule) {
+ $result = $this->accumulate_messages($result, $rule->description());
+ }
+ return $result;
+ }
+
+ /**
+ * 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 $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 array an array of reason why access is not allowed. An empty array
+ * (== false) if access should be allowed.
+ */
+ 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));
+ }
+ 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 array An array of reason why access is not allowed, or an empty array
+ * (== false) if access should be allowed.
+ */
+ public function prevent_access(): array {
+ $reasons = [];
+ foreach ($this->rules as $rule) {
+ $reasons = $this->accumulate_messages($reasons, $rule->prevent_access());
+ }
+ return $reasons;
+ }
+
+ /**
+ * 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.
+ */
+ public function is_preflight_check_required(?int $attemptid): bool {
+ 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 preflight_check_form the 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__preflight_check_form'])) {
+ $method = 'get';
+ }
+ return new preflight_check_form($url->out_omit_querystring(),
+ ['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(?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.
+ */
+ public function current_attempt_finished(): void {
+ 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 $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(int $numprevattempts, $lastattempt): bool {
+ 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(moodle_page $page): void {
+ foreach ($this->rules as $rule) {
+ $rule->setup_attempt_page($page);
+ }
+ }
+
+ /**
+ * Compute when the attempt must be submitted.
+ *
+ * @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(stdClass $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 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(stdClass $attempt, int $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;
+ }
+
+ /**
+ * 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(): bool {
+ foreach ($this->rules as $rule) {
+ if ($rule->attempt_must_be_in_popup()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 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(): array {
+ $options = [];
+ 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 renderer $output the quiz renderer.
+ * @param string $message optional message to output while redirecting.
+ */
+ 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);
+ die();
+ } else {
+ redirect($this->quizobj->view_url(), $message);
+ }
+ }
+
+ /**
+ * Make some text into a link to review the quiz, if that is appropriate.
+ *
+ * @param stdClass $attempt the attempt object
+ * @param mixed $nolongerused not used any more.
+ * @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, renderer $output): string {
+
+ // If the attempt is still open, don't link.
+ if (in_array($attempt->state, [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 errors
+ * @since Moodle 3.1
+ */
+ 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);
+ }
+ }
+ return $errors;
+ }
+}
diff --git a/mod/quiz/classes/admin/browser_security_setting.php b/mod/quiz/classes/admin/browser_security_setting.php
index e7be3f35c728f..bea427eba714c 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 @@ public function load_choices() {
}
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 3502a92b37813..43a7f7c72ee58 100644
--- a/mod/quiz/classes/completion/custom_completion.php
+++ b/mod/quiz/classes/completion/custom_completion.php
@@ -20,10 +20,8 @@
use context_module;
use core_completion\activity_custom_completion;
-use grade_grade;
-use grade_item;
-use quiz;
-use quiz_access_manager;
+use mod_quiz\quiz_settings;
+use mod_quiz\access_manager;
/**
* Activity custom completion subclass for the quiz activity.
@@ -71,8 +69,8 @@ protected function check_passing_grade_or_all_attempts(): bool {
}
$lastfinishedattempt = end($attempts);
$context = context_module::instance($this->cm->id);
- $quizobj = quiz::create($this->cm->instance, $this->userid);
- $accessmanager = new quiz_access_manager(
+ $quizobj = quiz_settings::create($this->cm->instance, $this->userid);
+ $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 b1b5ecd17aee5..26bf39486fcdb 100644
--- a/mod/quiz/classes/external.php
+++ b/mod/quiz/classes/external.php
@@ -25,6 +25,9 @@
*/
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;
@@ -111,8 +114,8 @@ public static function get_quizzes_by_courses($courseids = array()) {
$quizdetails['hasfeedback'] = (!empty($hasfeedback)) ? 1 : 0;
$timenow = time();
- $quizobj = quiz::create($quiz->id, $USER->id);
- $accessmanager = new quiz_access_manager($quizobj, $timenow, has_capability('mod/quiz:ignoretimelimits',
+ $quizobj = quiz_settings::create($quiz->id, $USER->id);
+ $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.
@@ -304,7 +307,6 @@ public static function view_quiz_parameters() {
* @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;
@@ -365,10 +367,9 @@ public static function get_user_attempts_parameters() {
* @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();
@@ -710,7 +711,6 @@ public static function start_attempt_parameters() {
* @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;
@@ -728,11 +728,11 @@ public static function start_attempt($quizid, $preflightdata = array(), $forcene
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()) {
- 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.
@@ -766,7 +766,7 @@ public static function start_attempt($quizid, $preflightdata = array(), $forcene
$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.
@@ -775,9 +775,9 @@ public static function start_attempt($quizid, $preflightdata = array(), $forcene
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;
@@ -812,7 +812,6 @@ public static function start_attempt_returns() {
* @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) {
@@ -825,7 +824,7 @@ protected static function validate_attempt($params, $checkaccessrules = true, $f
// 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());
}
// General capabilities check.
@@ -843,15 +842,15 @@ protected static function validate_attempt($params, $checkaccessrules = true, $f
$messages = $accessmanager->prevent_access();
if (!$ispreviewuser && $messages) {
- throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'attempterror');
+ throw new moodle_exception('attempterror', 'quiz', $attemptobj->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).
@@ -863,7 +862,7 @@ protected static function validate_attempt($params, $checkaccessrules = true, $f
$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']);
@@ -872,19 +871,19 @@ protected static function validate_attempt($params, $checkaccessrules = true, $f
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());
}
}
@@ -1047,7 +1046,6 @@ public static function get_attempt_data_parameters() {
* @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;
@@ -1371,8 +1369,6 @@ public static function process_attempt_returns() {
* @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) {
@@ -1382,13 +1378,13 @@ protected static function validate_attempt_review($params) {
$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);
}
@@ -1416,8 +1412,6 @@ public static function get_attempt_review_parameters() {
* @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;
@@ -1547,7 +1541,7 @@ public static function view_attempt($attemptid, $page, $preflightdata = array())
// 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();
@@ -1713,7 +1707,6 @@ public static function get_quiz_feedback_for_grade_parameters() {
* @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;
@@ -1784,7 +1777,6 @@ public static function get_quiz_access_information_parameters() {
* @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;
@@ -1807,10 +1799,10 @@ public static function get_quiz_access_information($quizid) {
$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 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();
@@ -1868,7 +1860,6 @@ public static function get_attempt_access_information_parameters() {
* @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;
@@ -1883,20 +1874,20 @@ public static function get_attempt_access_information($quizid, $attemptid = 0) {
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) {
- throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'notyourattempt');
+ throw new moodle_exception('notyourattempt', 'quiz', $attemptobj->view_url());
}
$attempttocheck = $attemptobj->get_attempt();
}
// 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 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);
@@ -1913,7 +1904,7 @@ public static function get_attempt_access_information($quizid, $attemptid = 0) {
$numattempts = count($attempts);
if (!$attempttocheck) {
- $attempttocheck = $unfinishedattempt ? $unfinishedattempt : $lastfinishedattempt;
+ $attempttocheck = $unfinishedattempt ?: $lastfinishedattempt;
}
$result = array();
@@ -1974,7 +1965,6 @@ public static function get_quiz_required_qtypes_parameters() {
* @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;
@@ -1988,7 +1978,7 @@ public static function get_quiz_required_qtypes($quizid) {
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/form/add_random_form.php b/mod/quiz/classes/form/add_random_form.php
new file mode 100644
index 0000000000000..2498abb0e2efb
--- /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/classes/form/edit_override_form.php b/mod/quiz/classes/form/edit_override_form.php
new file mode 100644
index 0000000000000..b830ce73a4704
--- /dev/null
+++ b/mod/quiz/classes/form/edit_override_form.php
@@ -0,0 +1,301 @@
+.
+
+namespace mod_quiz\form;
+
+use cm_info;
+use context;
+use context_module;
+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_module 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 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(moodle_url $submiturl,
+ cm_info $cm, stdClass $quiz, context_module $context,
+ bool $groupmode, ?stdClass $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 = [];
+ $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', ['cmid' => $cm->id]);
+ throw new \moodle_exception('groupsnone', 'quiz', $link);
+ }
+
+ $groupchoices = [];
+ 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 = [];
+ $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', ['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'), ['optional' => true]);
+ $mform->addHelpButton('timelimit', 'timelimit', 'quiz');
+ $mform->setDefault('timelimit', $this->quiz->timelimit);
+
+ // Number of attempts.
+ $attemptoptions = ['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 = [];
+ $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', '', [' '], 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 = ['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/classes/form/preflight_check_form.php b/mod/quiz/classes/form/preflight_check_form.php
new file mode 100644
index 0000000000000..a734d137fecc3
--- /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/classes/local/access_rule_base.php b/mod/quiz/classes/local/access_rule_base.php
new file mode 100644
index 0000000000000..9ab7b85c92b19
--- /dev/null
+++ b/mod/quiz/classes/local/access_rule_base.php
@@ -0,0 +1,356 @@
+.
+
+namespace mod_quiz\local;
+
+use mod_quiz\form\preflight_check_form;
+use mod_quiz_mod_form;
+use moodle_page;
+use MoodleQuickForm;
+use mod_quiz\quiz_settings;
+use stdClass;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot . '/mod/quiz/locallib.php');
+
+
+/**
+ * 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
+ * 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.
+ *
+ * @package mod_quiz
+ * @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_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_settings $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_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_settings $quizobj, $timenow, $canignoretimelimits) {
+ return null;
+ }
+
+ /**
+ * 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.
+ */
+ 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;
+ }
+
+ /**
+ * 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
+ * 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 {@see 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 {@see 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.
+ }
+
+ /**
+ * 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() {
+ return '';
+ }
+
+ /**
+ * 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, 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 $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.
+ */
+ public function is_finished($numprevattempts, $lastattempt) {
+ return false;
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * 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 [];
+ }
+
+ /**
+ * 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 ['ipaddress', 'password'].
+ */
+ public function get_superceded_rules() {
+ return [];
+ }
+
+ /**
+ * Add any fields that this rule requires to the quiz settings form. This
+ * 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.
+ */
+ 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 {@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.
+ * @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;
+ }
+
+ /**
+ * 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 [];
+ }
+
+ /**
+ * Save any submitted settings when the quiz settings form is submitted. This
+ * 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.
+ */
+ public static function save_settings($quiz) {
+ // By default do nothing.
+ }
+
+ /**
+ * Delete any rule-specific settings when the quiz is deleted. This is called
+ * 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
+ */
+ 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 {@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
+ * 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 ['', '', []];
+ }
+
+ /**
+ * 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 [];
+ }
+}
diff --git a/mod/quiz/classes/local/reports/attempts_report_options.php b/mod/quiz/classes/local/reports/attempts_report_options.php
index b66872845fe99..5d8f92383d561 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 993e6385e3e2f..d55d01312726f 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 @@
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_join;
use qubaid_list;
use question_engine_data_mapper;
-use quiz_attempt;
use stdClass;
/**
diff --git a/mod/quiz/classes/output/edit_renderer.php b/mod/quiz/classes/output/edit_renderer.php
index e30a992ca9c6b..25ae62954afea 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/links_to_other_attempts.php b/mod/quiz/classes/output/links_to_other_attempts.php
new file mode 100644
index 0000000000000..3fede7129254e
--- /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.
+ * (The third option is used by {@see quiz_attempt::links_to_other_redos()}.)
+ */
+ public $links = [];
+}
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 0000000000000..1fd6fbfc80a31
--- /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/classes/output/navigation_panel_base.php b/mod/quiz/classes/output/navigation_panel_base.php
new file mode 100644
index 0000000000000..6de96a68b1ad4
--- /dev/null
+++ b/mod/quiz/classes/output/navigation_panel_base.php
@@ -0,0 +1,200 @@
+.
+
+namespace mod_quiz\output;
+
+use mod_quiz\quiz_attempt;
+use moodle_url;
+use question_attempt;
+use question_display_options;
+use question_state;
+use renderable;
+use user_picture;
+
+/**
+ * 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.
+ *
+ * @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;
+
+ /**
+ * 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;
+ $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;
+ }
+
+ /**
+ * 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);
+ }
+
+ // 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 to do output above the question buttons.
+ *
+ * @param renderer $output the quiz renderer to use.
+ * @return string HTML to output.
+ */
+ public function render_before_button_bits(renderer $output) {
+ 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);
+
+ /**
+ * 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)));
+ }
+
+ /**
+ * 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) {
+ 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/navigation_panel_review.php b/mod/quiz/classes/output/navigation_panel_review.php
new file mode 100644
index 0000000000000..d07de8e417222
--- /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/classes/output/navigation_question_button.php b/mod/quiz/classes/output/navigation_question_button.php
new file mode 100644
index 0000000000000..e07a23d73d240
--- /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/navigation_section_heading.php b/mod/quiz/classes/output/navigation_section_heading.php
new file mode 100644
index 0000000000000..e70c79fd39a94
--- /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/question_chooser.php b/mod/quiz/classes/output/question_chooser.php
index f58ffe114e2d7..06e101ab0a5dd 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.
diff --git a/mod/quiz/classes/output/renderer.php b/mod/quiz/classes/output/renderer.php
new file mode 100644
index 0000000000000..d5a06c8187d95
--- /dev/null
+++ b/mod/quiz/classes/output/renderer.php
@@ -0,0 +1,1468 @@
+.
+
+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 mod_quiz\quiz_attempt;
+use moodle_url;
+use plugin_renderer_base;
+use popup_action;
+use question_display_options;
+use mod_quiz\quiz_settings;
+use renderable;
+use single_button;
+use stdClass;
+
+/**
+ * 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 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 string HTML to display.
+ */
+ 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 string HTML to display.
+ */
+ 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,
+ ["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 array updated version of the $summarydata array.
+ */
+ 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, ['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
+ * @return string HTML to display.
+ */
+ 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', [
+ '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, ['class' => 'cell', 'scope' => 'row']) .
+ html_writer::tag('td', $content, ['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 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
+ */
+ 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 int $page current page number
+ * @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) {
+ 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', ['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', ['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')]);
+ $output .= html_writer::end_tag('div');
+ $output .= html_writer::end_tag('div');
+ $output .= html_writer::end_tag('form');
+
+ return $output;
+ }
+
+ /**
+ * Returns either a link 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',
+ [$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']);
+
+ } else {
+ return html_writer::link($url, get_string('finishreview', 'quiz'),
+ ['class' => 'mod_quiz-next-nav']);
+ }
+ }
+
+ /**
+ * 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.
+ *
+ * @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, ['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) {
+
+ $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 URL to restart the attempt.
+ */
+ public function restart_preview_button($url) {
+ return $this->single_button($url, get_string('startnewpreview', 'quiz'));
+ }
+
+ /**
+ * Outputs the navigation block panel
+ *
+ * @param navigation_panel_base $panel
+ */
+ public function navigation_panel(navigation_panel_base $panel) {
+
+ $output = '';
+ $userpicture = $panel->user_picture();
+ if ($userpicture) {
+ $fullname = fullname($userpicture->user);
+ if ($userpicture->size) {
+ $fullname = html_writer::div($fullname);
+ }
+ $output .= html_writer::tag('div', $this->render($userpicture) . $fullname,
+ ['id' => 'user-picture', 'class' => 'clearfix']);
+ }
+ $output .= $panel->render_before_button_bits($this);
+
+ $bcc = $panel->get_button_container_class();
+ $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),
+ ['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 navigation_question_button $button
+ * @return string HTML fragment.
+ */
+ protected function render_navigation_question_button(navigation_question_button $button) {
+ $classes = ['qnbutton', $button->stateclass, $button->navmethod, 'btn'];
+ $extrainfo = [];
+
+ 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, ['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', '', ['class' => 'thispageholder']) .
+ 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];
+
+ 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 navigation_section_heading $heading the heading.
+ * @return string HTML fragment.
+ */
+ protected function render_navigation_section_heading(navigation_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 = [];
+ 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 \mod_quiz\quiz_settings $quizobj
+ * @param preflight_check_form $mform
+ * @return string
+ */
+ 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());
+ $output .= $this->heading(format_string($quizobj->get_quiz_name(), true,
+ ["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
+ );
+ }
+
+ /**
+ * Outputs 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',
+ ['action' => new moodle_url($attemptobj->processattempt_url(),
+ ['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));
+ }
+
+ $navmethod = $attemptobj->get_quiz()->navmethod;
+ $output .= $this->attempt_navigation_buttons($page, $attemptobj->is_last_page($page), $navmethod);
+
+ // 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', ['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', ['class' => 'submitbtns']);
+ if ($page > 0 && $navmethod == 'free') {
+ $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']);
+ $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', ['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 = ['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));
+ }
+
+ /**
+ * 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 = [$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',
+ [$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.
+ * @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 = [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');
+ $table->align[] = 'left';
+ $table->size[] = '';
+ }
+ $tablewidth = count($table->align);
+ $table->data = [];
+
+ // 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[] = [$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', ['src' => $this->image_url('i/flagged'),
+ 'alt' => get_string('flagged', 'question'), 'class' => 'questionflag icon-post']);
+ }
+ if ($attemptobj->can_navigate_to($slot)) {
+ $row = [html_writer::link($attemptobj->attempt_url($slot),
+ $attemptobj->get_question_number($slot) . $flag),
+ $attemptobj->get_question_status($slot, $displayoptions->correctness)];
+ } else {
+ $row = [$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.
+ return html_writer::table($table);
+ }
+
+ /**
+ * Creates any controls 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 = [
+ '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->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',
+ ['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|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.
+ */
+ 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();
+ }
+
+ $this->page->requires->js_call_amd('mod_quiz/preflightcheck', 'init',
+ ['.quizstartbuttondiv [type=submit]', get_string('startattempt', 'quiz'),
+ '#mod_quiz_preflight_form', $popupjsoptions]);
+
+ return $this->render($button) . ($preflightcheckform ? $preflightcheckform->render() : '');
+ }
+
+ /**
+ * 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', ['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) {
+ $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 enrolled on 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,
+ ['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 stdClass $quiz the quiz settings.
+ * @param context_module $context the quiz context.
+ * @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 = [];
+ $table->align = [];
+ $table->size = [];
+ 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 = [];
+
+ // 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[] = '';
+ }
+ }
+
+ // 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) {
+ 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 description of the current state of an attempt.
+ *
+ * @param quiz_attempt $attemptobj the attempt
+ * @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())),
+ ['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']);
+
+ case quiz_attempt::ABANDONED:
+ return get_string('stateabandoned', 'quiz');
+
+ default:
+ throw new coding_exception('Unexpected attempt state');
+ }
+ }
+
+ /**
+ * Generates data pertaining to quiz results
+ *
+ * @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 = '';
+ 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'),
+ ['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'),
+ ['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,
+ ['class' => 'noreviewmessage']);
+ }
+
+ /**
+ * 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
+ * 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', [
+ '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 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 of 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 of the graph.
+ */
+ public function graph(moodle_url $url, $title) {
+ $graph = html_writer::empty_tag('img', ['src' => $url, 'alt' => $title]);
+
+ return $this->heading($title, 3) . html_writer::tag('div', $graph, ['class' => 'graph']);
+ }
+
+ /**
+ * Output the connection warning messages, which are initially hidden, and
+ * only revealed by JavaScript if necessary.
+ */
+ public function connection_warning() {
+ $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,
+ ['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.
+ *
+ * @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);
+ }
+
+ /**
+ * 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);
+ }
+
+ /**
+ * 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/classes/output/view_page.php b/mod/quiz/classes/output/view_page.php
new file mode 100644
index 0000000000000..ec437bfafa7df
--- /dev/null
+++ b/mod/quiz/classes/output/view_page.php
@@ -0,0 +1,98 @@
+.
+
+namespace mod_quiz\output;
+
+use mod_quiz\access_manager;
+use mod_quiz\form\preflight_check_form;
+use mod_quiz\quiz_attempt;
+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 quiz_attempt[] $attemptobjs 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;
+}
diff --git a/mod/quiz/classes/privacy/legacy_quizaccess_polyfill.php b/mod/quiz/classes/privacy/legacy_quizaccess_polyfill.php
index b70c651fb0450..786befdb84f81 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 82b4e9e58f868..c0ca490e9ab22 100644
--- a/mod/quiz/classes/privacy/provider.php
+++ b/mod/quiz/classes/privacy/provider.php
@@ -34,6 +34,7 @@
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();
@@ -276,7 +277,7 @@ public static function export_user_data(approved_contextlist $contextlist) {
$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());
@@ -353,7 +354,7 @@ public static function delete_data_for_all_users_in_context(\context $context) {
return;
}
- $quizobj = \quiz::create($cm->instance);
+ $quizobj = \mod_quiz\quiz_settings::create($cm->instance);
$quiz = $quizobj->get_quiz();
// Handle the 'quizaccess' subplugin.
@@ -392,7 +393,7 @@ public static function delete_data_for_user(approved_contextlist $contextlist) {
}
// 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();
@@ -440,7 +441,7 @@ public static function delete_data_for_users(approved_userlist $userlist) {
return;
}
- $quizobj = \quiz::create($cm->instance);
+ $quizobj = \mod_quiz\quiz_settings::create($cm->instance);
$quiz = $quizobj->get_quiz();
$userids = $userlist->get_userids();
@@ -526,7 +527,7 @@ protected static function export_quiz_attempts(approved_contextlist $contextlist
// 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/privacy/quizaccess_provider.php b/mod/quiz/classes/privacy/quizaccess_provider.php
index e18d582f29801..385e80d0c9ad0 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 f28000c274cd2..fd6cdbf4ef8aa 100644
--- a/mod/quiz/classes/question/bank/qbank_helper.php
+++ b/mod/quiz/classes/question/bank/qbank_helper.php
@@ -23,8 +23,6 @@
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');
/**
* Helper class for question bank and its associated data.
diff --git a/mod/quiz/classes/question/qubaids_for_quiz.php b/mod/quiz/classes/question/qubaids_for_quiz.php
index e42a650a8384f..5583349d06136 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 @@ public function __construct(int $quizid, bool $includepreviews = true, bool $onl
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 b3c102ca83573..9248627ca0614 100644
--- a/mod/quiz/classes/question/qubaids_for_users_attempts.php
+++ b/mod/quiz/classes/question/qubaids_for_users_attempts.php
@@ -16,10 +16,11 @@
namespace mod_quiz\question;
+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.
@@ -54,14 +55,14 @@ public function __construct($quizid, $userid, $status = 'finished', $includeprev
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 0000000000000..ff91e2475019a
--- /dev/null
+++ b/mod/quiz/classes/quiz_attempt.php
@@ -0,0 +1,2283 @@
+.
+
+namespace mod_quiz;
+
+use action_link;
+use block_contents;
+use cm_info;
+use coding_exception;
+use context_module;
+use 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 stdClass;
+
+/**
+ * This class represents one user's attempt 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_settings 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 {@see 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 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.
+ */
+ public function __construct($attempt, $quiz, $cm, $course, $loadquestions = true) {
+ $this->attempt = $attempt;
+ $this->quizobj = new quiz_settings($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', ['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(['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(['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.
+ */
+ public static function state_name($state) {
+ return quiz_attempt_state_name($state);
+ }
+
+ /**
+ * This method can be called later if the object was constructed with $loadquestions = 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.
+ */
+ 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 that represent the layout.
+ */
+ protected function determine_layout() {
+
+ // 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 = [];
+ 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, change 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 ==========================================================.
+
+ /**
+ * Get the raw quiz settings object.
+ *
+ * @return stdClass
+ */
+ public function get_quiz() {
+ return $this->quizobj->get_quiz();
+ }
+
+ /**
+ * Get the {@see seb_quiz_settings} object for this quiz.
+ *
+ * @return quiz_settings
+ */
+ public function get_quizobj() {
+ return $this->quizobj;
+ }
+
+ /**
+ * Git the id of the course this quiz belongs to.
+ *
+ * @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();
+ }
+
+ /**
+ * Get the quiz id.
+ *
+ * @return int the quiz id.
+ */
+ public function get_quizid() {
+ return $this->quizobj->get_quizid();
+ }
+
+ /**
+ * 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();
+ }
+
+ /**
+ * 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();
+ }
+
+ /**
+ * Get the course_module for this quiz.
+ *
+ * @return stdClass|cm_info 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();
+ }
+
+ /**
+ * 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();
+ }
+
+ /**
+ * 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();
+ }
+
+ /**
+ * 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.
+ */
+ public function get_access_manager($timenow) {
+ return $this->quizobj->get_access_manager($timenow);
+ }
+
+ /**
+ * Get the id of this attempt.
+ *
+ * @return int the attempt id.
+ */
+ public function get_attemptid() {
+ return $this->attempt->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;
+ }
+
+ /**
+ * Get the raw quiz attempt object.
+ *
+ * @return stdClass the row from the quiz_attempts table.
+ */
+ public function get_attempt() {
+ return $this->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;
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * Get the id of the user this attempt belongs to.
+ * @return int user id.
+ */
+ public function get_userid() {
+ return $this->attempt->userid;
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * Is this attempt a preview?
+ *
+ * @return bool true if it is.
+ */
+ public function is_preview() {
+ return $this->attempt->preview;
+ }
+
+ /**
+ * Does this attempt belong to the current user?
+ *
+ * @return bool true => own attempt/preview. false => reviewing someone else's.
+ */
+ public function is_own_attempt() {
+ global $USER;
+ return $this->attempt->userid == $USER->id;
+ }
+
+ /**
+ * 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() &&
+ $this->is_preview_user() && $this->attempt->preview;
+ }
+
+ /**
+ * Is the current user allowed to review this attempt. This applies when
+ * {@see 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 submitted 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 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. 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.
+ */
+ 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. 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) {
+ $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 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.
+ 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 standard exception thrown.
+ $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;
+ }
+
+ return $this->get_navigation_method() == QUIZ_NAVMETHOD_FREE;
+ }
+
+ /**
+ * 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);
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * 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 = [];
+ 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 = [];
+ 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 = [];
+ 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|null 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 {@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.
+ */
+ 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.
+ */
+ 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);
+ }
+
+
+ /**
+ * 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;
+ }
+
+ /**
+ * 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 = [];
+ 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 ============================================.
+
+ /**
+ * 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();
+ }
+
+ /**
+ * 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 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)) {
+ $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;
+ }
+
+ /**
+ * 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 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);
+ }
+
+ /**
+ * 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());
+ }
+
+ /**
+ * 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', ['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() {
+ 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;
+ }
+
+ /**
+ * 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.
+ * @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 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);
+ }
+
+ /**
+ * 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(), ['forcenew' => true]),
+ get_string('startnewpreview', 'quiz'));
+ } else {
+ return '';
+ }
+ }
+
+ /**
+ * 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.
+ * @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 {@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.
+ * @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 {@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.
+ * @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 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.
+ * @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 bool true if the file can be accessed.
+ */
+ 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 {@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.
+ *
+ * @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, ['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, ['slot' => $qa->get_slot()]);
+ $links->links[$index] = new action_link($url, $index,
+ new popup_action('click', $url, 'reviewquestion',
+ ['width' => 450, 'height' => 650]),
+ ['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);
+ }
+
+ /**
+ * 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
+ * {@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 {@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) {
+ 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 randomly selected pick from the available question.
+ *
+ * @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, ['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 = [
+ 'context' => $this->get_quizobj()->get_context(),
+ 'courseid' => $this->get_courseid(),
+ 'objectid' => $this->attempt->id,
+ 'relateduserid' => $this->attempt->userid,
+ '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 =========================================================.
+
+ /**
+ * Get a URL for a particular question on a particular page of the quiz.
+ * Used by {@see attempt_url()} and {@see review_url()}.
+ *
+ * @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
+ * 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,
+ ['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, ['id' => $this->get_attemptid()]);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Trigger the attempt_viewed event.
+ *
+ * @since Moodle 3.1
+ */
+ public function fire_attempt_viewed_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(),
+ '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 = [
+ '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_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 = [
+ '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_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/quiz_settings.php b/mod/quiz/classes/quiz_settings.php
new file mode 100644
index 0000000000000..de053228c96f5
--- /dev/null
+++ b/mod/quiz/classes/quiz_settings.php
@@ -0,0 +1,562 @@
+.
+
+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.
+ *
+ * @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. */
+ 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', ['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
+ * {@see 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;
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * 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)) {
+ $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());
+ }
+
+ /**
+ * Have any questions been added to this quiz yet?
+ *
+ * @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);
+ }
+
+ /**
+ * Get a particular question in this quiz, by its id.
+ *
+ * @param int $id the question id.
+ * @return stdClass the question object with that id.
+ */
+ public function get_question($id) {
+ return $this->questions[$id];
+ }
+
+ /**
+ * 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.
+ */
+ public function get_questions($questionids = null) {
+ if (is_null($questionids)) {
+ $questionids = array_keys($this->questions);
+ }
+ $questions = [];
+ 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',
+ ['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 ============================================.
+
+ /**
+ * 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() {
+ return new moodle_url('/mod/quiz/view.php', ['id' => $this->cm->id]);
+ }
+
+ /**
+ * 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() {
+ 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 moodle_url the URL of that attempt.
+ */
+ public function attempt_url($attemptid, $page = 0) {
+ $params = ['attempt' => $attemptid, 'cmid' => $this->get_cmid()];
+ if ($page) {
+ $params['page'] = $page;
+ }
+ return new moodle_url('/mod/quiz/attempt.php', $params);
+ }
+
+ /**
+ * 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 = ['cmid' => $this->cm->id, 'sesskey' => sesskey()];
+ if ($page) {
+ $params['page'] = $page;
+ }
+ return new moodle_url('/mod/quiz/startattempt.php', $params);
+ }
+
+ /**
+ * 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', ['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', ['attempt' => $attemptid, 'cmid' => $this->get_cmid()]);
+ }
+
+ // 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 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 = [];
+
+ // To control if we need to look in categories for questions.
+ $qcategories = [];
+
+ 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 = [];
+ 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 c2ab857f4699d..6cea4d504f68f 100644
--- a/mod/quiz/classes/structure.php
+++ b/mod/quiz/classes/structure.php
@@ -39,7 +39,7 @@
* @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 @@ public static function create() {
/**
* 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/classes/task/quiz_notify_attempt_manual_grading_completed.php b/mod/quiz/classes/task/quiz_notify_attempt_manual_grading_completed.php
index 445cdd102b913..62531792b45b3 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 @@
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 54fa14e96bb6a..e83c08f344e74 100644
--- a/mod/quiz/classes/task/update_overdue_attempts.php
+++ b/mod/quiz/classes/task/update_overdue_attempts.php
@@ -24,8 +24,13 @@
*/
namespace mod_quiz\task;
+use mod_quiz\quiz_attempt;
+use moodle_exception;
+use moodle_recordset;
+
defined('MOODLE_INTERNAL') || die();
+global $CFG;
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
/**
@@ -39,27 +44,112 @@
*/
class update_overdue_attempts extends \core\task\scheduled_task {
- public function get_name() {
+ public function get_name(): string {
return get_string('updateoverdueattemptstask', 'mod_quiz');
}
/**
- *
* 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(int $timenow, int $processto): array {
+ 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);
+ }
+
+ /**
+ * 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, sorted by courseid then quizid.
+ */
+ public function get_list_of_overdue_attempts(int $processto): moodle_recordset {
+ 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 51509e7a95c6b..bf6091bab8787 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();
-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 {
+debugging('This file is no longer required in Moodle 4.2+. Please do not include/require it.', DEBUG_DEVELOPER);
- // 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/locallib.php');
+require_once($CFG->dirroot . '/mod/quiz/deprecatedlib.php');
diff --git a/mod/quiz/db/renamedclasses.php b/mod/quiz/db/renamedclasses.php
index 4904067348e60..4c9b218c1dcfa 100644
--- a/mod/quiz/db/renamedclasses.php
+++ b/mod/quiz/db/renamedclasses.php
@@ -51,4 +51,19 @@
'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',
+ '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',
+ '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',
+ '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',
+ 'quiz_attempt' => 'mod_quiz\quiz_attempt',
+ 'quiz' => 'mod_quiz\quiz_settings',
];
diff --git a/mod/quiz/deprecatedlib.php b/mod/quiz/deprecatedlib.php
index 07c39ea8cfc88..2fa17695f146f 100644
--- a/mod/quiz/deprecatedlib.php
+++ b/mod/quiz/deprecatedlib.php
@@ -22,6 +22,10 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
+use mod_quiz\access_manager;
+use mod_quiz\quiz_settings;
+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.
*
@@ -68,8 +72,8 @@ 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(),
+ $quizobj = quiz_settings::create($quiz->id, $userid);
+ $accessmanager = new access_manager($quizobj, time(),
has_capability('mod/quiz:ignoretimelimits', $context, $userid, false));
return $accessmanager->is_finished(count($attempts), $lastfinishedattempt);
@@ -132,3 +136,62 @@ 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-76612 Final deprecation in Moodle 4.6
+ */
+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);
+ }
+}
+
+/**
+ * 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_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.
+ * 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) {
+ debugging('Class moodle_quiz_exception is deprecated. ' .
+ 'Please use a standard moodle_exception instead.', DEBUG_DEVELOPER);
+ if (!$link) {
+ $link = $quizobj->view_url();
+ }
+ parent::__construct($errorcode, 'quiz', $link, $a, $debuginfo);
+ }
+}
diff --git a/mod/quiz/edit.php b/mod/quiz/edit.php
index b25d7ebfb0f14..667289850c3f4 100644
--- a/mod/quiz/edit.php
+++ b/mod/quiz/edit.php
@@ -40,10 +40,10 @@
* @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');
-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
@@ -63,7 +63,7 @@
// 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 4d20fde23a7b9..e7fe66d199062 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 @@
$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/grade.php b/mod/quiz/grade.php
index 970aa6f5543c7..c7889735261f6 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 8aaa204b151c8..a18327cb8cb2c 100644
--- a/mod/quiz/lib.php
+++ b/mod/quiz/lib.php
@@ -28,14 +28,17 @@
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;
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.
@@ -190,7 +193,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) {
@@ -581,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) {
@@ -659,7 +657,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
*/
@@ -691,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));
@@ -1185,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.
@@ -1236,7 +1234,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);
@@ -1965,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());
@@ -2062,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.
@@ -2459,7 +2457,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 = [
@@ -2473,7 +2470,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/locallib.php b/mod/quiz/locallib.php
index 00d8b14161a13..8dc47ffc7407b 100644
--- a/mod/quiz/locallib.php
+++ b/mod/quiz/locallib.php
@@ -31,16 +31,15 @@
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');
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;
+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
@@ -82,7 +81,7 @@
*
* @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.
@@ -90,7 +89,7 @@
*
* @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) {
@@ -146,17 +145,16 @@ 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
* @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.
- * @throws moodle_exception
+ * to force the choice of a particular variant. Intended for testing
+ * purposes only.
* @return object modified attempt object
*/
function quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $timenow,
@@ -341,7 +339,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.
@@ -586,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) {
@@ -1355,7 +1353,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) {
@@ -2437,16 +2435,15 @@ 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 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
* @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, quiz_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();
@@ -2486,7 +2483,7 @@ function quiz_validate_new_attempt(quiz $quizobj, quiz_access_manager $accessman
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());
}
}
@@ -2522,7 +2519,7 @@ function quiz_validate_new_attempt(quiz $quizobj, quiz_access_manager $accessman
/**
* 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
@@ -2534,7 +2531,7 @@ function quiz_validate_new_attempt(quiz $quizobj, quiz_access_manager $accessman
* @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;
@@ -2695,8 +2692,7 @@ function($carry, $slottag) use ($slottags, $tagsbyid, $tagsbyname) {
*
* @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 {
diff --git a/mod/quiz/mod_form.php b/mod/quiz/mod_form.php
index 772b6ddfd7c03..a9fec29231b8b 100644
--- a/mod/quiz/mod_form.php
+++ b/mod/quiz/mod_form.php
@@ -28,6 +28,7 @@
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 @@ protected function definition() {
// 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 @@ public function data_preprocessing(&$toform) {
// 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 @@ public function validation($data, $files) {
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/override_form.php b/mod/quiz/override_form.php
index 04f33033bbfcd..4fcf92cd6307a 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 117ab70fafc64..fab6698ac3d77 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 @@
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 3450be23ee58a..d8e419d8bdcec 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 @@
}
// 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 d419a0d241085..8bb12c6ab584c 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/processattempt.php b/mod/quiz/processattempt.php
index 50dba84d6ee7c..4a871af17ce38 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');
@@ -71,7 +73,7 @@
// 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 +83,7 @@
// 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/renderer.php b/mod/quiz/renderer.php
index 0055f5b734317..49b549d0cf87e 100644
--- a/mod/quiz/renderer.php
+++ b/mod/quiz/renderer.php
@@ -15,1487 +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\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);
- }
-
- /**
- * outputs the link the other attempts.
- *
- * @param mod_quiz_links_to_other_attempts $links
- */
- protected function render_mod_quiz_links_to_other_attempts(
- mod_quiz_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);
- }
-
- public function start_attempt_page(quiz $quizobj, mod_quiz_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 quiz_access_manager $accessmanager Instance of quiz_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 mod_quiz_view_object $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 mod_quiz_view_object $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 {
- $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 mod_quiz_view_object $viewobj the information required to display the view page.
- * @return string HTML to output.
- */
- public function view_page_buttons(mod_quiz_view_object $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 mod_quiz_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,
- $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 ' .
- 'quiz_access_manager::get_preflight_check_form, not a warning message string.');
- }
-
- $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 mod_quiz_view_object $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 mod_quiz_view_object $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 mod_quiz_view_object $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 mod_quiz_view_object $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'));
- }
-}
-
-
-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.
- */
- public $links = array();
-}
-
-
-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 quiz_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 mod_quiz_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;
- }
- }
-}
+// 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/repaginate.php b/mod/quiz/repaginate.php
index 08ce25f84f157..4ccb37da7055c 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 @@
$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/grading/report.php b/mod/quiz/report/grading/report.php
index c43532be94de3..9192a8179e531 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 681ee0f4ce48f..f1ab1da042bd8 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 4469aa77c4be9..83c41a4dda6a8 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 309a78d430a9d..1d0e583d56b80 100644
--- a/mod/quiz/report/overview/tests/report_test.php
+++ b/mod/quiz/report/overview/tests/report_test.php
@@ -18,9 +18,9 @@
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\quiz_settings;
use mod_quiz\local\reports\attempts_report;
use quiz_overview_options;
use quiz_overview_report;
@@ -122,7 +122,7 @@ public function test_report_sql(?string $isdownloading): void {
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 @@ public function test_delete_selected_attempts(): void {
$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 @@ public function test_regrade_question() {
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 da0e996af7477..53ba5ed622b29 100644
--- a/mod/quiz/report/reportlib.php
+++ b/mod/quiz/report/reportlib.php
@@ -26,9 +26,7 @@
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;
@@ -98,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/responses/first_or_all_responses_table.php b/mod/quiz/report/responses/first_or_all_responses_table.php
index 0f96e7b082652..b9787cf8af3f5 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 92eb5ba828e66..033de2a931104 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 cb16d9ad7598f..909c706241cdf 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 6d5906be8655f..ba3001f0bcff8 100644
--- a/mod/quiz/report/statistics/classes/task/recalculate.php
+++ b/mod/quiz/report/statistics/classes/task/recalculate.php
@@ -16,8 +16,8 @@
namespace quiz_statistics\task;
-use quiz_attempt;
-use quiz;
+use mod_quiz\quiz_attempt;
+use mod_quiz\quiz_settings;
use quiz_statistics_report;
defined('MOODLE_INTERNAL') || die();
@@ -60,7 +60,7 @@ public function execute() {
$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/report/statistics/statisticslib.php b/mod/quiz/report/statistics/statisticslib.php
index 832e14be64b46..383044ab6438d 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 5c9e2ef9440ec..0c907c1f421d7 100644
--- a/mod/quiz/review.php
+++ b/mod/quiz/review.php
@@ -25,6 +25,9 @@
* @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;
+use mod_quiz\quiz_attempt;
require_once(__DIR__ . '/../../config.php');
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
@@ -76,7 +79,7 @@
}
} 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.
@@ -254,10 +257,11 @@
$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/reviewquestion.php b/mod/quiz/reviewquestion.php
index f49306b97744e..9bdb904dc000e 100644
--- a/mod/quiz/reviewquestion.php
+++ b/mod/quiz/reviewquestion.php
@@ -71,7 +71,7 @@
}
} 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/startattempt.php b/mod/quiz/startattempt.php
index b71771ab0c8fa..b4a132f7ccc03 100644
--- a/mod/quiz/startattempt.php
+++ b/mod/quiz/startattempt.php
@@ -26,6 +26,9 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
+use mod_quiz\quiz_attempt;
+use mod_quiz\quiz_settings;
+
require_once(__DIR__ . '/../../config.php');
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
@@ -41,7 +44,7 @@
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/summary.php b/mod/quiz/summary.php
index 496ed8e960caa..f3c22ac05dfb9 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');
@@ -44,7 +46,7 @@
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());
}
}
@@ -60,6 +62,7 @@
// 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 @@
$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/tests/attempt_test.php b/mod/quiz/tests/attempt_test.php
index 0b78eae2cba78..7dbf1128c848e 100644
--- a/mod/quiz/tests/attempt_test.php
+++ b/mod/quiz/tests/attempt_test.php
@@ -17,8 +17,7 @@
namespace mod_quiz;
use question_engine;
-use quiz;
-use quiz_attempt;
+use mod_quiz\quiz_settings;
defined('MOODLE_INTERNAL') || die();
@@ -53,7 +52,7 @@ protected function create_quiz_and_attempt_with_layout($layout, $navmethod = QUI
$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());
@@ -291,7 +290,7 @@ public function test_is_participant() {
$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);
@@ -339,7 +338,7 @@ public function test_quiz_prepare_and_start_new_attempt() {
$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 788b3f577f9a4..987195e81076e 100644
--- a/mod/quiz/tests/attempt_walkthrough_from_csv_test.php
+++ b/mod/quiz/tests/attempt_walkthrough_from_csv_test.php
@@ -17,8 +17,7 @@
namespace mod_quiz;
use question_engine;
-use quiz;
-use quiz_attempt;
+use mod_quiz\quiz_settings;
defined('MOODLE_INTERNAL') || die();
@@ -246,7 +245,7 @@ protected function walkthrough_attempts(array $steps): array {
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 cbeb26db77b31..b30236c97f55b 100644
--- a/mod/quiz/tests/attempt_walkthrough_test.php
+++ b/mod/quiz/tests/attempt_walkthrough_test.php
@@ -18,8 +18,7 @@
use question_bank;
use question_engine;
-use quiz;
-use quiz_attempt;
+use mod_quiz\quiz_settings;
defined('MOODLE_INTERNAL') || die();
@@ -68,7 +67,7 @@ public function test_quiz_attempt_walkthrough() {
// 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());
@@ -189,7 +188,7 @@ public function test_quiz_attempt_walkthrough_submit_time_recorded_correctly_whe
// 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);
@@ -235,7 +234,7 @@ public function test_quiz_attempt_walkthrough_close_time_extended_at_last_minute
// 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);
@@ -308,7 +307,7 @@ public function test_quiz_with_random_question_attempt_walkthrough() {
$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());
@@ -409,7 +408,7 @@ public function test_quiz_with_question_with_variants_attempt_walkthrough($varia
// 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 52abb66289597..1de3efb8732c0 100644
--- a/mod/quiz/tests/attempts_test.php
+++ b/mod/quiz/tests/attempts_test.php
@@ -16,9 +16,11 @@
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;
+use mod_quiz\quiz_settings;
defined('MOODLE_INTERNAL') || die();
@@ -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 @@ public function test_bulk_update_functions() {
// 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 @@ public function test_bulk_update_functions() {
// 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');
@@ -562,7 +560,7 @@ public function test_quiz_create_attempt_handling_errors() {
// 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 f2f700de81449..621ee50a3d87c 100644
--- a/mod/quiz/tests/backup/restore_date_test.php
+++ b/mod/quiz/tests/backup/restore_date_test.php
@@ -50,7 +50,7 @@ public function test_restore_dates() {
// 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/behat/behat_mod_quiz.php b/mod/quiz/tests/behat/behat_mod_quiz.php
index 737421976d1d1..aa402076f6115 100644
--- a/mod/quiz/tests/behat/behat_mod_quiz.php
+++ b/mod/quiz/tests/behat/behat_mod_quiz.php
@@ -31,6 +31,7 @@
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 0420ffad60646..bf4c6085c5d96 100644
--- a/mod/quiz/tests/custom_completion_test.php
+++ b/mod/quiz/tests/custom_completion_test.php
@@ -24,8 +24,7 @@
use grade_item;
use mod_quiz\completion\custom_completion;
use question_engine;
-use quiz;
-use quiz_attempt;
+use mod_quiz\quiz_settings;
defined('MOODLE_INTERNAL') || die();
@@ -113,7 +112,7 @@ private function setup_quiz_for_testing_completion(array $completionoptions): ar
* @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 5229e87b96b17..c998654197a0b 100644
--- a/mod/quiz/tests/event/events_test.php
+++ b/mod/quiz/tests/event/events_test.php
@@ -25,15 +25,10 @@
namespace mod_quiz\event;
-use quiz;
-use quiz_attempt;
+use mod_quiz\quiz_attempt;
+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 @@ protected function prepare_quiz() {
$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 41f4b903e3a02..5b6de7293b959 100644
--- a/mod/quiz/tests/external/external_test.php
+++ b/mod/quiz/tests/external/external_test.php
@@ -27,10 +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 quiz;
-use quiz_attempt;
+use moodle_exception;
+use mod_quiz\quiz_settings;
defined('MOODLE_INTERNAL') || die();
@@ -148,7 +149,7 @@ private function create_quiz_with_questions($startattempt = false, $finishattemp
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',
@@ -340,7 +341,7 @@ public function test_view_quiz() {
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 @@ public function test_view_quiz() {
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 @@ public function test_view_quiz() {
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);
}
@@ -544,8 +545,8 @@ public function test_get_user_best_grade() {
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([
@@ -699,7 +700,7 @@ public function test_get_combined_review_options() {
$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',
@@ -846,7 +847,7 @@ public function test_start_attempt() {
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 @@ public function test_start_attempt() {
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 @@ public function test_validate_attempt() {
'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 @@ public function test_validate_attempt() {
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 @@ public function test_validate_attempt() {
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 @@ public function test_validate_attempt() {
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 @@ public function test_validate_attempt() {
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 @@ public function test_validate_attempt_review() {
$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 @@ public function test_validate_attempt_review() {
$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 @@ public function test_view_attempt() {
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);
}
@@ -1865,7 +1866,7 @@ public function test_get_attempt_access_information() {
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',
@@ -2026,7 +2027,7 @@ public function test_sequential_navigation_view_attempt() {
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 @@ public function test_sequential_navigation_get_attempt_data() {
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 @@ public function test_sequential_navigation_get_attempt_data() {
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());
}
@@ -2086,9 +2087,9 @@ public function test_sequential_navigation_get_attempt_data() {
/**
* 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');
@@ -2111,7 +2112,7 @@ private function prepare_sequential_quiz(): quiz {
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));
@@ -2123,14 +2124,19 @@ private function prepare_sequential_quiz(): quiz {
/**
* Create question attempt
*
- * @param quiz $quizobj
+ * @param quiz_settings $quizobj
* @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 {
+ 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/tests/generator/lib.php b/mod/quiz/tests/generator/lib.php
index cccdaf2f9df0d..416000b123934 100644
--- a/mod/quiz/tests/generator/lib.php
+++ b/mod/quiz/tests/generator/lib.php
@@ -14,6 +14,9 @@
// 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;
+
defined('MOODLE_INTERNAL') || die();
/**
@@ -115,7 +118,7 @@ public function create_instance($record = null, array $options = null) {
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 a9ed6e1d0b1b6..a4826dd884551 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;
+use mod_quiz\quiz_settings;
defined('MOODLE_INTERNAL') || die();
@@ -200,7 +197,7 @@ private function setup_quiz_for_testing_completion(array $completionoptions) {
* @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());
@@ -457,11 +454,11 @@ public function test_quiz_get_user_attempts() {
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());
@@ -967,7 +964,7 @@ public function test_quiz_core_calendar_provide_event_action_already_finished()
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();
@@ -1022,7 +1019,7 @@ public function test_quiz_core_calendar_provide_event_action_already_finished_fo
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();
@@ -1248,7 +1245,7 @@ public function test_mod_quiz_inplace_editable(int $slotnumber, string $newvalue
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/locallib_test.php b/mod/quiz/tests/locallib_test.php
index a5c1809461aac..b73248fc98449 100644
--- a/mod/quiz/tests/locallib_test.php
+++ b/mod/quiz/tests/locallib_test.php
@@ -24,7 +24,7 @@
*/
namespace mod_quiz;
-use quiz_attempt;
+use mod_quiz\output\renderer;
use mod_quiz\question\display_options;
defined('MOODLE_INTERNAL') || die();
@@ -517,7 +517,7 @@ public function test_quiz_override_summary() {
$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/tests/privacy/provider_test.php b/mod/quiz/tests/privacy/provider_test.php
index a7292e7170e9f..1f74e996d6c57 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\writer;
use mod_quiz\privacy\provider;
use mod_quiz\privacy\helper;
+use mod_quiz\quiz_attempt;
defined('MOODLE_INTERNAL') || die();
@@ -188,7 +189,7 @@ public function test_user_with_data() {
$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 @@ public function test_user_with_data() {
$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());
}
/**
@@ -246,7 +247,7 @@ public function test_user_with_preview() {
// 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());
@@ -258,7 +259,7 @@ public function test_user_with_preview() {
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 @@ public function test_user_with_preview() {
$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 @@ public function test_wrong_context() {
* Create a test quiz for the specified course.
*
* @param \stdClass $course
- * @return array
+ * @return \stdClass
*/
protected function create_test_quiz($course) {
global $DB;
@@ -429,7 +430,7 @@ protected function attempt_quiz($quiz, $user) {
$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());
@@ -441,7 +442,7 @@ protected function attempt_quiz($quiz, $user) {
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 @@ protected function attempt_quiz($quiz, $user) {
$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/privacy_legacy_quizaccess_polyfill_test.php b/mod/quiz/tests/privacy_legacy_quizaccess_polyfill_test.php
index a908760957d83..f34b01a04583d 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 @@ public function test_export_quizaccess_user_data() {
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 @@ public function test_delete_quizaccess_for_context() {
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 @@ protected static function _export_quizaccess_user_data($quiz, $user) {
/**
* 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 @@ protected static function _delete_quizaccess_data_for_all_users_in_context($quiz
/**
* 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 71d3474dbbff0..456d9a0b6493a 100644
--- a/mod/quiz/tests/qbank_helper_test.php
+++ b/mod/quiz/tests/qbank_helper_test.php
@@ -78,7 +78,7 @@ public function test_reference_records() {
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 @@ public function test_get_question_structure() {
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 44c189dfb9e03..544e4a3e285e7 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,8 +29,7 @@
use context_module;
use mod_quiz\task\quiz_notify_attempt_manual_grading_completed;
use question_engine;
-use quiz;
-use quiz_attempt;
+use mod_quiz\quiz_settings;
use stdClass;
defined('MOODLE_INTERNAL') || die();
@@ -62,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. */
@@ -118,7 +117,7 @@ public function setUp(): void {
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 1d315d1d25cab..35e027df16ec6 100644
--- a/mod/quiz/tests/quiz_question_helper_test_trait.php
+++ b/mod/quiz/tests/quiz_question_helper_test_trait.php
@@ -13,6 +13,8 @@
//
// 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.
@@ -105,7 +107,7 @@ protected function attempt_quiz(\stdClass $quiz, \stdClass $user, $attemptnumber
$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 0e4726d74821b..be24aa3609f3e 100644
--- a/mod/quiz/tests/quiz_question_restore_test.php
+++ b/mod/quiz/tests/quiz_question_restore_test.php
@@ -259,7 +259,7 @@ public function test_pre_4_quiz_restore_for_regular_questions() {
// 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 @@ public function test_pre_4_quiz_restore_for_random_questions() {
// 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 @@ public function test_pre_4_quiz_restore_for_random_question_tags() {
// 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 c59250c28d960..dbfe6331346c8 100644
--- a/mod/quiz/tests/quiz_question_version_test.php
+++ b/mod/quiz/tests/quiz_question_version_test.php
@@ -68,7 +68,7 @@ public function test_quiz_questions_for_changed_versions() {
$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 @@ public function test_quiz_question_attempts_with_changed_version() {
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 2877b19cf4e57..ce6e4fb2f216e 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 @@ public function test_cannot_review_message() {
$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 @@ public function test_cannot_review_message() {
$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 51422068c56af..d0a98367ff8e0 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 @@ private function get_quiz_object() {
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 2c177f7def167..b5bcfb7aa82e4 100644
--- a/mod/quiz/tests/reportlib_test.php
+++ b/mod/quiz/tests/reportlib_test.php
@@ -16,12 +16,9 @@
namespace mod_quiz;
-use quiz_attempt;
-
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 5a1c83d28ab97..1e8fcc975ff42 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 @@ protected function prepare_quiz_data() {
* 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 @@ protected function create_test_quiz($layout) {
}
}
- $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 386d0dcc2d3d5..e9fdfe6428f46 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 @@ public function test_restore_random_question_by_tag() {
// 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 9c27c08735be5..1ada6364d99e7 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
@@ -35,14 +39,42 @@ 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
-
- As part of the clean-up, the following files are no longer required, and if you try to
+ - 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
+ - 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
+ - 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
+ - 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
+ - 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:
- 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
+ - mod/quiz/accessmanager_form.php
+ - mod/quiz/cronlib.php
+ - mod/quiz/override_form.php
+ - mod/quiz/accessrule/accessrulebase.php
+ - mod/quiz/renderer.php - actually, no debugging ouput for this one because of how renderer factories work.
+ - mod/quiz/attemptlib.php
=== 4.1 ===
diff --git a/mod/quiz/view.php b/mod/quiz/view.php
index f4e4647f2fe68..28c88c85eadd7 100644
--- a/mod/quiz/view.php
+++ b/mod/quiz/view.php
@@ -23,6 +23,11 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
+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');
@@ -35,20 +40,20 @@
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');
}
}
@@ -64,8 +69,8 @@
// 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,
+$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();
@@ -73,10 +78,10 @@
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 mod_quiz_view_object();
+$viewobj = new view_page();
$viewobj->accessmanager = $accessmanager;
$viewobj->canreviewmine = $canreviewmine || $canpreview;
@@ -103,7 +108,7 @@
$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);
}
@@ -147,7 +152,7 @@
$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.
@@ -174,8 +179,8 @@
$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)) {
@@ -200,9 +205,9 @@
$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 = '';
diff --git a/question/bank/statistics/tests/helper_test.php b/question/bank/statistics/tests/helper_test.php
index f80084e91d967..e05d70dabb698 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 quiz;
+use mod_quiz\quiz_attempt;
+use mod_quiz\quiz_settings;
use question_engine;
-use quiz_attempt;
/**
* Tests for question statistics.
@@ -171,7 +171,7 @@ private function submit_quiz(object $quiz, array $answers): void {
// 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 f337fc31caac8..841b968fc97f5 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.
*
@@ -51,7 +53,7 @@ public function setup(): void {
$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);
@@ -75,7 +77,7 @@ public function setup(): void {
$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 3994c82d22492..aa9f1e62bcd6d 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.
*
@@ -43,7 +45,7 @@ public function test_question_usage() {
$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);
@@ -68,7 +70,7 @@ public function test_question_usage() {
$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('/'));
diff --git a/question/type/questionbase.php b/question/type/questionbase.php
index 0febd2fc4669f..cbee8a562b79b 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() {