first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
+586
View File
@@ -0,0 +1,586 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz;
use core_component;
use mod_quiz\form\preflight_check_form;
use mod_quiz\local\access_rule_base;
use mod_quiz\output\renderer;
use mod_quiz\question\display_options;
use mod_quiz_mod_form;
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 &lt;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, $attempt->timefinish));
} 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;
}
}
@@ -0,0 +1,44 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\admin;
use mod_quiz\access_manager;
/**
* Admin settings class for the quiz browser security option.
*
* Just so we can lazy-load the choices.
*
* @package mod_quiz
* @category admin
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class browser_security_setting extends \admin_setting_configselect_with_advanced {
public function load_choices() {
global $CFG;
if (is_array($this->choices)) {
return true;
}
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
$this->choices = access_manager::get_browser_security_choices();
return true;
}
}
@@ -0,0 +1,42 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\admin;
/**
* Admin settings class for the quiz grading method.
*
* Just so we can lazy-load the choices.
*
* @package mod_quiz
* @category admin
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class grade_method_setting extends \admin_setting_configselect_with_advanced {
public function load_choices() {
global $CFG;
if (is_array($this->choices)) {
return true;
}
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
$this->choices = quiz_get_grading_options();
return true;
}
}
@@ -0,0 +1,42 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\admin;
/**
* Admin settings class for the quiz overdue attempt handling method.
*
* Just so we can lazy-load the choices.
*
* @package mod_quiz
* @category admin
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class overdue_handling_setting extends \admin_setting_configselect_with_advanced {
public function load_choices() {
global $CFG;
if (is_array($this->choices)) {
return true;
}
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
$this->choices = quiz_get_overdue_handling_options();
return true;
}
}
+169
View File
@@ -0,0 +1,169 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\admin;
/**
* Admin settings class for the quiz review options.
*
* @package mod_quiz
* @category admin
* @copyright 2008 Tim Hunt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class review_setting extends \admin_setting {
/**
* @var integer should match the constants defined in
* {@see display_options}. Copied for performance reasons.
*/
const DURING = 0x10000;
/**
* @var integer should match the constants defined in
* {@see display_options}. Copied for performance reasons.
*/
const IMMEDIATELY_AFTER = 0x01000;
/**
* @var integer should match the constants defined in
* {@see display_options}. Copied for performance reasons.
*/
const LATER_WHILE_OPEN = 0x00100;
/**
* @var integer should match the constants defined in
* {@see display_options}. Copied for performance reasons.
*/
const AFTER_CLOSE = 0x00010;
/**
* @var boolean|null forced checked / disabled attributes for the during time.
*/
protected $duringstate;
/**
* This should match {@link mod_quiz_mod_form::$reviewfields} but copied
* here because generating the admin tree needs to be fast.
* @return array
*/
public static function fields() {
return [
'attempt' => get_string('theattempt', 'quiz'),
'correctness' => get_string('whethercorrect', 'question'),
'maxmarks' => get_string('maxmarks', 'quiz'),
'marks' => get_string('marks', 'question'),
'specificfeedback' => get_string('specificfeedback', 'question'),
'generalfeedback' => get_string('generalfeedback', 'question'),
'rightanswer' => get_string('rightanswer', 'question'),
'overallfeedback' => get_string('overallfeedback', 'quiz'),
];
}
/**
* Constructor.
*
* @param string $name unique ascii name, either 'mysetting' for settings that in config,
* or 'myplugin/mysetting' for ones in config_plugins.
* @param string $visiblename localised name
* @param string $description localised long description
* @param mixed $defaultsetting string or array depending on implementation
* @param bool|null $duringstate
*/
public function __construct($name, $visiblename, $description,
$defaultsetting, $duringstate = null) {
$this->duringstate = $duringstate;
parent::__construct($name, $visiblename, $description, $defaultsetting);
}
/**
* Return the combination that means all times.
* @return int all times.
*/
public static function all_on() {
return self::DURING | self::IMMEDIATELY_AFTER | self::LATER_WHILE_OPEN |
self::AFTER_CLOSE;
}
/**
* Get an array of the names of all the possible times.
* @return array an array of time constant => lang string.
*/
protected static function times() {
return [
self::DURING => get_string('reviewduring', 'quiz'),
self::IMMEDIATELY_AFTER => get_string('reviewimmediately', 'quiz'),
self::LATER_WHILE_OPEN => get_string('reviewopen', 'quiz'),
self::AFTER_CLOSE => get_string('reviewclosed', 'quiz'),
];
}
protected function normalise_data($data) {
$times = self::times();
$value = 0;
foreach ($times as $timemask => $name) {
if ($timemask == self::DURING && !is_null($this->duringstate)) {
if ($this->duringstate) {
$value += $timemask;
}
} else if (!empty($data[$timemask])) {
$value += $timemask;
}
}
return $value;
}
public function get_setting() {
return $this->config_read($this->name);
}
public function write_setting($data) {
if (is_array($data) || empty($data)) {
$data = $this->normalise_data($data);
}
$this->config_write($this->name, $data);
return '';
}
public function output_html($data, $query = '') {
if (is_array($data) || empty($data)) {
$data = $this->normalise_data($data);
}
$return = '<div class="group"><input type="hidden" name="' .
$this->get_full_name() . '[' . self::DURING . ']" value="0" />';
foreach (self::times() as $timemask => $namestring) {
$id = $this->get_id(). '_' . $timemask;
$state = '';
if ($data & $timemask) {
$state = 'checked="checked" ';
}
if ($timemask == self::DURING && !is_null($this->duringstate)) {
$state = 'disabled="disabled" ';
if ($this->duringstate) {
$state .= 'checked="checked" ';
}
}
$return .= '<span><input type="checkbox" name="' .
$this->get_full_name() . '[' . $timemask . ']" value="1" id="' . $id .
'" ' . $state . '/> <label for="' . $id . '">' .
$namestring . "</label></span>\n";
}
$return .= "</div>\n";
return format_admin_setting($this, $this->visiblename, $return,
$this->description, true, '', get_string('everythingon', 'quiz'), $query);
}
}
@@ -0,0 +1,42 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\admin;
/**
* Admin settings class for the choices for how to display the user's image.
*
* Just so we can lazy-load the choices.
*
* @package mod_quiz
* @category admin
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class user_image_setting extends \admin_setting_configselect_with_advanced {
public function load_choices() {
global $CFG;
if (is_array($this->choices)) {
return true;
}
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
$this->choices = quiz_get_user_image_options();
return true;
}
}
@@ -0,0 +1,34 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\adminpresets;
use core_adminpresets\local\setting\adminpresets_admin_setting_configselect_with_advanced;
/**
* Admin settings class for the quiz browser security option.
*
* @package mod_quiz
* @copyright 2021 Pimenko <support@pimenko.com><pimenko.com>
* @author Jordan Kesraoui | Sylvain Revenu | Pimenko based on David Monllaó <david.monllao@urv.cat> code
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class adminpresets_browser_security_setting extends adminpresets_admin_setting_configselect_with_advanced {
public function set_behaviors() {
$this->behaviors['loadchoices'] = &$this->settingdata;
}
}
@@ -0,0 +1,34 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\adminpresets;
use core_adminpresets\local\setting\adminpresets_admin_setting_configselect_with_advanced;
/**
* Admin settings class for the quiz grading method.
*
* @package mod_quiz
* @copyright 2021 Pimenko <support@pimenko.com><pimenko.com>
* @author Jordan Kesraoui | Sylvain Revenu | Pimenko based on David Monllaó <david.monllao@urv.cat> code
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class adminpresets_grade_method_setting extends adminpresets_admin_setting_configselect_with_advanced {
public function set_behaviors() {
$this->behaviors['loadchoices'] = &$this->settingdata;
}
}
@@ -0,0 +1,34 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\adminpresets;
use core_adminpresets\local\setting\adminpresets_admin_setting_configselect_with_advanced;
/**
* Admin settings class for the quiz overdue attempt handling method.
*
* @package mod_quiz
* @copyright 2021 Pimenko <support@pimenko.com><pimenko.com>
* @author Jordan Kesraoui | Sylvain Revenu | Pimenko based on David Monllaó <david.monllao@urv.cat> code
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class adminpresets_overdue_handling_setting extends adminpresets_admin_setting_configselect_with_advanced {
public function set_behaviors() {
$this->behaviors['loadchoices'] = &$this->settingdata;
}
}
@@ -0,0 +1,53 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\adminpresets;
use ReflectionMethod;
use core_adminpresets\local\setting\adminpresets_setting;
/**
* Admin settings class for the quiz review options.
*
* @package mod_quiz
* @copyright 2021 Pimenko <support@pimenko.com><pimenko.com>
* @author Jordan Kesraoui | Sylvain Revenu | Pimenko based on David Monllaó <david.monllao@urv.cat> code
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class adminpresets_review_setting extends adminpresets_setting {
/**
* The setting value is a sum of 'review_setting::times'
*/
protected function set_visiblevalue() {
// Getting the masks descriptions (review_setting protected method).
$reflectiontimes = new ReflectionMethod('mod_quiz\admin\review_setting', 'times');
$times = $reflectiontimes->invoke(null);
$visiblevalue = '';
foreach ($times as $timemask => $namestring) {
// If the value is checked.
if ($this->value & $timemask) {
$visiblevalue .= $namestring . ', ';
}
}
$visiblevalue = rtrim($visiblevalue, ', ');
$this->visiblevalue = $visiblevalue;
}
}
@@ -0,0 +1,34 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\adminpresets;
use core_adminpresets\local\setting\adminpresets_admin_setting_configselect_with_advanced;
/**
* Admin settings class for the choices for how to display the user's image.
*
* @package mod_quiz
* @copyright 2021 Pimenko <support@pimenko.com><pimenko.com>
* @author Jordan Kesraoui | Sylvain Revenu | Pimenko based on David Monllaó <david.monllao@urv.cat> code
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class adminpresets_user_image_setting extends adminpresets_admin_setting_configselect_with_advanced {
public function set_behaviors() {
$this->behaviors['loadchoices'] = &$this->settingdata;
}
}
@@ -0,0 +1,65 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Activity base class.
*
* @package mod_quiz
* @copyright 2017 onwards Ankit Agarwal <ankit.agrr@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_quiz\analytics\indicator;
defined('MOODLE_INTERNAL') || die();
/**
* Activity base class.
*
* @package mod_quiz
* @copyright 2017 onwards Ankit Agarwal <ankit.agrr@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class activity_base extends \core_analytics\local\indicator\community_of_inquiry_activity {
/**
* feedback_check_grades
*
* @return bool
*/
protected function feedback_check_grades() {
// We need the grade to be released to the student to consider that feedback has been provided.
return true;
}
/**
* feedback_viewed_events
*
* @return string[]
*/
protected function feedback_viewed_events() {
return ['\mod_quiz\event\course_module_viewed'];
}
/**
* Returns the name of the field that controls activity availability.
*
* @return null|string
*/
protected function get_timeclose_field() {
return 'timeclose';
}
}
@@ -0,0 +1,79 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Cognitive depth indicator - quiz.
*
* @package mod_quiz
* @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_quiz\analytics\indicator;
defined('MOODLE_INTERNAL') || die();
/**
* Cognitive depth indicator - quiz.
*
* @package mod_quiz
* @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class cognitive_depth extends activity_base {
/**
* Returns the name.
*
* If there is a corresponding '_help' string this will be shown as well.
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
return new \lang_string('indicator:cognitivedepth', 'mod_quiz');
}
public function get_indicator_type() {
return self::INDICATOR_COGNITIVE;
}
public function get_cognitive_depth_level(\cm_info $cm) {
return self::COGNITIVE_LEVEL_5;
}
/**
* feedback_submitted_events
*
* @return string[]
*/
protected function feedback_submitted_events() {
return ['\mod_quiz\event\attempt_submitted'];
}
/**
* feedback_replied
*
* @param \cm_info $cm
* @param int $contextid
* @param int $userid
* @param int $after
* @return bool
*/
protected function feedback_replied(\cm_info $cm, $contextid, $userid, $after = false) {
// No level 4.
return false;
}
}
@@ -0,0 +1,56 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Social breadth indicator - quiz.
*
* @package mod_quiz
* @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_quiz\analytics\indicator;
defined('MOODLE_INTERNAL') || die();
/**
* Social breadth indicator - quiz.
*
* @package mod_quiz
* @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class social_breadth extends activity_base {
/**
* Returns the name.
*
* If there is a corresponding '_help' string this will be shown as well.
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
return new \lang_string('indicator:socialbreadth', 'mod_quiz');
}
public function get_indicator_type() {
return self::INDICATOR_SOCIAL;
}
public function get_social_breadth_level(\cm_info $cm) {
return self::SOCIAL_LEVEL_2;
}
}
+115
View File
@@ -0,0 +1,115 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Cache data source for the quiz overrides.
*
* @package mod_quiz
* @copyright 2021 Shamim Rezaie <shamim@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
declare(strict_types=1);
namespace mod_quiz\cache;
use cache_definition;
/**
* Class quiz_overrides
*
* @package mod_quiz
* @copyright 2021 Shamim Rezaie <shamim@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class overrides implements \cache_data_source {
/** @var overrides the singleton instance of this class. */
protected static $instance = null;
/**
* Returns an instance of the data source class that the cache can use for loading data using the other methods
* specified by this interface.
*
* @param cache_definition $definition
* @return stdClass
*/
public static function get_instance_for_cache(cache_definition $definition): overrides {
if (is_null(self::$instance)) {
self::$instance = new overrides();
}
return self::$instance;
}
/**
* Loads the data for the key provided ready formatted for caching.
*
* @param string|int $key The key to load.
* @return mixed What ever data should be returned, or false if it can't be loaded.
* @throws \coding_exception
*/
public function load_for_cache($key) {
global $DB;
// Ignore getting data if this is a cache invalidation - {@see \cache_helper::purge_by_event()}.
if ($key == 'lastinvalidation') {
return null;
}
[$quizid, $ug, $ugid] = explode('_', $key);
$quizid = (int) $quizid;
switch ($ug) {
case 'u':
$userid = (int) $ugid;
$override = $DB->get_record(
'quiz_overrides',
['quiz' => $quizid, 'userid' => $userid],
'timeopen, timeclose, timelimit, attempts, password'
);
break;
case 'g':
$groupid = (int) $ugid;
$override = $DB->get_record(
'quiz_overrides',
['quiz' => $quizid, 'groupid' => $groupid],
'timeopen, timeclose, timelimit, attempts, password'
);
break;
default:
throw new \coding_exception('Invalid cache key');
}
// Return null instead of false, because false will not be cached.
return $override ?: null;
}
/**
* Loads several keys for the cache.
*
* @param array $keys An array of keys each of which will be string|int.
* @return array An array of matching data items.
*/
public function load_many_for_cache(array $keys) {
$results = [];
foreach ($keys as $key) {
$results[] = $this->load_for_cache($key);
}
return $results;
}
}
@@ -0,0 +1,163 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace mod_quiz\completion;
use context_module;
use core_completion\activity_custom_completion;
use mod_quiz\quiz_settings;
use mod_quiz\access_manager;
/**
* Activity custom completion subclass for the quiz activity.
*
* Class for defining mod_quiz's custom completion rules and fetching the completion statuses
* of the custom completion rules for a given quiz instance and a user.
*
* @package mod_quiz
* @copyright 2021 Shamim Rezaie <shamim@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class custom_completion extends activity_custom_completion {
/**
* Check passing grade (or no attempts left) requirement for completion.
*
* @return bool True if the passing grade (or no attempts left) requirement is disabled or met.
*/
protected function check_passing_grade_or_all_attempts(): bool {
global $CFG;
require_once($CFG->libdir . '/gradelib.php');
$completionpassorattempts = $this->cm->customdata['customcompletionrules']['completionpassorattemptsexhausted'];
if (empty($completionpassorattempts['completionpassgrade'])) {
return true;
}
if ($this->completionstate &&
isset($this->completionstate['passgrade']) &&
$this->completionstate['passgrade'] == COMPLETION_COMPLETE_PASS) {
return true;
}
// If a passing grade is required and exhausting all available attempts is not accepted for completion,
// then this quiz is not complete.
if (empty($completionpassorattempts['completionattemptsexhausted'])) {
return false;
}
// Check if all attempts are used up.
$attempts = quiz_get_user_attempts($this->cm->instance, $this->userid, 'finished', true);
if (!$attempts) {
return false;
}
$lastfinishedattempt = end($attempts);
$context = context_module::instance($this->cm->id);
$quizobj = quiz_settings::create((int) $this->cm->instance, $this->userid);
$accessmanager = new access_manager(
$quizobj,
time(),
has_capability('mod/quiz:ignoretimelimits', $context, $this->userid, false)
);
return $accessmanager->is_finished(count($attempts), $lastfinishedattempt);
}
/**
* Check minimum attempts requirement for completion.
*
* @return bool True if minimum attempts requirement is disabled or met.
*/
protected function check_min_attempts() {
$minattempts = $this->cm->customdata['customcompletionrules']['completionminattempts'];
if (!$minattempts) {
return true;
}
// Check if the user has done enough attempts.
$attempts = quiz_get_user_attempts($this->cm->instance, $this->userid, 'finished', true);
return $minattempts <= count($attempts);
}
/**
* Fetches the completion state for a given completion rule.
*
* @param string $rule The completion rule.
* @return int The completion state.
*/
public function get_state(string $rule): int {
$this->validate_rule($rule);
switch ($rule) {
case 'completionpassorattemptsexhausted':
$status = static::check_passing_grade_or_all_attempts();
break;
case 'completionminattempts':
$status = static::check_min_attempts();
break;
}
return empty($status) ? COMPLETION_INCOMPLETE : COMPLETION_COMPLETE;
}
/**
* Fetch the list of custom completion rules that this module defines.
*
* @return array
*/
public static function get_defined_custom_rules(): array {
return [
'completionpassorattemptsexhausted',
'completionminattempts',
];
}
/**
* Returns an associative array of the descriptions of custom completion rules.
*
* @return array
*/
public function get_custom_rule_descriptions(): array {
$minattempts = $this->cm->customdata['customcompletionrules']['completionminattempts'] ?? 0;
$description['completionminattempts'] = get_string('completiondetail:minattempts', 'mod_quiz', $minattempts);
// Completion pass grade is now part of core. Only show the following if it's combined with min attempts.
$completionpassorattempts = $this->cm->customdata['customcompletionrules']['completionpassorattemptsexhausted'] ?? [];
if (!empty($completionpassorattempts['completionattemptsexhausted'])) {
$description['completionpassorattemptsexhausted'] = get_string('completiondetail:passorexhaust', 'mod_quiz');
}
return $description;
}
/**
* Returns an array of all completion rules, in the order they should be displayed to users.
*
* @return array
*/
public function get_sort_order(): array {
return [
'completionview',
'completionminattempts',
'completionusegrade',
'completionpassgrade',
'completionpassorattemptsexhausted',
];
}
}
+70
View File
@@ -0,0 +1,70 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Contains the class for fetching the important dates in mod_quiz for a given module instance and a user.
*
* @package mod_quiz
* @copyright 2021 Shamim Rezaie <shamim@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
declare(strict_types=1);
namespace mod_quiz;
use core\activity_dates;
/**
* Class for fetching the important dates in mod_quiz for a given module instance and a user.
*
* @copyright 2021 Shamim Rezaie <shamim@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class dates extends activity_dates {
/**
* Returns a list of important dates in mod_quiz
*
* @return array
*/
protected function get_dates(): array {
$timeopen = $this->cm->customdata['timeopen'] ?? null;
$timeclose = $this->cm->customdata['timeclose'] ?? null;
$now = time();
$dates = [];
if ($timeopen) {
$openlabelid = $timeopen > $now ? 'activitydate:opens' : 'activitydate:opened';
$dates[] = [
'dataid' => 'timeopen',
'label' => get_string($openlabelid, 'core_course'),
'timestamp' => (int) $timeopen,
];
}
if ($timeclose) {
$closelabelid = $timeclose > $now ? 'activitydate:closes' : 'activitydate:closed';
$dates[] = [
'dataid' => 'timeclose',
'label' => get_string($closelabelid, 'core_course'),
'timestamp' => (int) $timeclose,
];
}
return $dates;
}
}
@@ -0,0 +1,110 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The mod_quiz attempt abandoned event.
*
* @package mod_quiz
* @copyright 2013 Adrian Greeve <adrian@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_quiz\event;
defined('MOODLE_INTERNAL') || die();
/**
* The mod_quiz attempt abandoned event class.
*
* @property-read array $other {
* Extra information about event.
*
* - int submitterid: id of submitter (null when trigged by CLI script).
* - int quizid: (optional) id of the quiz.
* }
*
* @package mod_quiz
* @since Moodle 2.6
* @copyright 2013 Adrian Greeve <adrian@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class attempt_abandoned extends \core\event\base {
/**
* Init method.
*/
protected function init() {
$this->data['objecttable'] = 'quiz_attempts';
$this->data['crud'] = 'u';
$this->data['edulevel'] = self::LEVEL_PARTICIPATING;
}
/**
* Returns description of what happened.
*
* @return string
*/
public function get_description() {
return "The user with id '$this->relateduserid' has had their attempt with id '$this->objectid' marked as abandoned " .
"for the quiz with course module id '$this->contextinstanceid'.";
}
/**
* Returns localised general event name.
*
* @return string
*/
public static function get_name() {
return get_string('eventquizattemptabandoned', 'mod_quiz');
}
/**
* Returns relevant URL.
*
* @return \moodle_url
*/
public function get_url() {
return new \moodle_url('/mod/quiz/review.php', ['attempt' => $this->objectid]);
}
/**
* Custom validation.
*
* @throws \coding_exception
* @return void
*/
protected function validate_data() {
parent::validate_data();
if (!isset($this->relateduserid)) {
throw new \coding_exception('The \'relateduserid\' must be set.');
}
if (!array_key_exists('submitterid', $this->other)) {
throw new \coding_exception('The \'submitterid\' value must be set in other.');
}
}
public static function get_objectid_mapping() {
return ['db' => 'quiz_attempts', 'restore' => 'quiz_attempt'];
}
public static function get_other_mapping() {
$othermapped = [];
$othermapped['submitterid'] = ['db' => 'user', 'restore' => 'user'];
$othermapped['quizid'] = ['db' => 'quiz', 'restore' => 'quiz'];
return $othermapped;
}
}
@@ -0,0 +1,131 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The mod_quiz attempt auto-saved event.
*
* @package mod_quiz
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_quiz\event;
/**
* The mod_quiz attempt auto-saved event class.
*
* @property-read array $other {
* Extra information about event.
*
* - int quizid: the id of the quiz.
* - int page: the page number of attempt.
* }
*
* @package mod_quiz
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class attempt_autosaved extends \core\event\base {
/**
* Init method.
*/
protected function init() {
$this->data['objecttable'] = 'quiz_attempts';
$this->data['crud'] = 'u';
$this->data['edulevel'] = self::LEVEL_PARTICIPATING;
}
/**
* Returns localised general event name.
*
* @return string
*/
public static function get_name() {
return get_string('eventattemptautosaved', 'mod_quiz');
}
/**
* Returns description of what happened.
*
* @return string
*/
public function get_description() {
$pagenumber = $this->other['page'] + 1;
return "The user with id '$this->userid' is working on page " .
"'{$pagenumber}' of the attempt " .
"with id '$this->objectid' for the quiz with course module id '$this->contextinstanceid', " .
"and their latest responses have been saved automatically.";
}
/**
* Returns relevant URL.
*
* @return \moodle_url
*/
public function get_url() {
return new \moodle_url('/mod/quiz/review.php', [
'attempt' => $this->objectid,
'page' => $this->other['page']
]);
}
/**
* Custom validation.
*
* @throws \coding_exception
* @return void
*/
protected function validate_data() {
parent::validate_data();
if (!isset($this->relateduserid)) {
throw new \coding_exception('The \'relateduserid\' must be set.');
}
if (!isset($this->other['quizid'])) {
throw new \coding_exception('The \'quizid\' value must be set in other.');
}
if (!isset($this->other['page'])) {
throw new \coding_exception('The \'page\' value must be set in other.');
}
}
/**
* This is used when restoring course logs where it is required that we
* map the information in 'other' to it's new value in the new course.
*
* @return array List of mapping of other ids.
*/
public static function get_objectid_mapping() {
return ['db' => 'quiz_attempts', 'restore' => 'quiz_attempt'];
}
/**
* This is used when restoring course logs where it is required that we
* map the information in 'other' to it's new value in the new course.
*
* @return array List of mapping of other ids.
*/
public static function get_other_mapping() {
$othermapped = [];
$othermapped['quizid'] = ['db' => 'quiz', 'restore' => 'quiz'];
return $othermapped;
}
}
@@ -0,0 +1,113 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The mod_quiz attempt became overdue event.
*
* @package mod_quiz
* @copyright 2013 Adrian Greeve <adrian@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_quiz\event;
defined('MOODLE_INTERNAL') || die();
/**
* The mod_quiz attempt became overdue event class.
*
* Please note that the name of this event is not following the event naming convention.
* Its name should not be used as a reference for other events to be created.
*
* @property-read array $other {
* Extra information about event.
*
* - int submitterid: id of submitter (null when trigged by CLI script).
* - int quizid: (optional) the id of the quiz.
* }
*
* @package mod_quiz
* @since Moodle 2.6
* @copyright 2013 Adrian Greeve <adrian@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class attempt_becameoverdue extends \core\event\base {
/**
* Init method.
*/
protected function init() {
$this->data['objecttable'] = 'quiz_attempts';
$this->data['crud'] = 'u';
$this->data['edulevel'] = self::LEVEL_PARTICIPATING;
}
/**
* Returns description of what happened.
*
* @return string
*/
public function get_description() {
return "The quiz attempt with id '$this->objectid' belonging to the quiz with course module id '$this->contextinstanceid' " .
"for the user with id '$this->relateduserid' became overdue.";
}
/**
* Returns localised general event name.
*
* @return string
*/
public static function get_name() {
return get_string('eventquizattempttimelimitexceeded', 'mod_quiz');
}
/**
* Returns relevant URL.
*
* @return \moodle_url
*/
public function get_url() {
return new \moodle_url('/mod/quiz/review.php', ['attempt' => $this->objectid]);
}
/**
* Custom validation.
*
* @throws \coding_exception
* @return void
*/
protected function validate_data() {
parent::validate_data();
if (!isset($this->relateduserid)) {
throw new \coding_exception('The \'relateduserid\' must be set.');
}
if (!array_key_exists('submitterid', $this->other)) {
throw new \coding_exception('The \'submitterid\' value must be set in other.');
}
}
public static function get_objectid_mapping() {
return ['db' => 'quiz_attempts', 'restore' => 'quiz_attempt'];
}
public static function get_other_mapping() {
$othermapped = [];
$othermapped['submitterid'] = ['db' => 'user', 'restore' => 'user'];
$othermapped['quizid'] = ['db' => 'quiz', 'restore' => 'quiz'];
return $othermapped;
}
}
+109
View File
@@ -0,0 +1,109 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The mod_quiz attempt deleted event.
*
* @package mod_quiz
* @copyright 2014 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_quiz\event;
defined('MOODLE_INTERNAL') || die();
/**
* The mod_quiz attempt deleted event class.
*
* @property-read array $other {
* Extra information about event.
*
* - int quizid: the id of the quiz.
* }
*
* @package mod_quiz
* @since Moodle 2.7
* @copyright 2014 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class attempt_deleted extends \core\event\base {
/**
* Init method.
*/
protected function init() {
$this->data['objecttable'] = 'quiz_attempts';
$this->data['crud'] = 'd';
$this->data['edulevel'] = self::LEVEL_TEACHING;
}
/**
* Returns localised general event name.
*
* @return string
*/
public static function get_name() {
return get_string('eventattemptdeleted', 'mod_quiz');
}
/**
* Returns description of what happened.
*
* @return string
*/
public function get_description() {
return "The user with id '$this->userid' deleted the attempt with id '$this->objectid' belonging to the quiz " .
"with course module id '$this->contextinstanceid' for the user with id '$this->relateduserid'.";
}
/**
* Returns relevant URL.
*
* @return \moodle_url
*/
public function get_url() {
return new \moodle_url('/mod/quiz/report.php', ['id' => $this->contextinstanceid]);
}
/**
* Custom validation.
*
* @throws \coding_exception
* @return void
*/
protected function validate_data() {
parent::validate_data();
if (!isset($this->relateduserid)) {
throw new \coding_exception('The \'relateduserid\' must be set.');
}
if (!isset($this->other['quizid'])) {
throw new \coding_exception('The \'quizid\' value must be set in other.');
}
}
public static function get_objectid_mapping() {
return ['db' => 'quiz_attempts', 'restore' => 'quiz_attempt'];
}
public static function get_other_mapping() {
$othermapped = [];
$othermapped['quizid'] = ['db' => 'quiz', 'restore' => 'quiz'];
return $othermapped;
}
}
@@ -0,0 +1,69 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\event;
/**
* The mod_quiz attempt manual grading complete event.
*
* @package mod_quiz
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class attempt_manual_grading_completed extends \core\event\base {
protected function init() {
$this->data['objecttable'] = 'quiz_attempts';
$this->data['crud'] = 'u';
$this->data['edulevel'] = self::LEVEL_TEACHING;
}
public function get_description() {
return "The attempt with id '$this->objectid' for the user with id '$this->relateduserid' " .
"for the quiz with course module id '$this->contextinstanceid' is now fully graded. Sending notification.";
}
public static function get_name() {
return get_string('eventattemptmanualgradingcomplete', 'mod_quiz');
}
public function get_url() {
return new \moodle_url('/mod/quiz/review.php', ['attempt' => $this->objectid]);
}
protected function validate_data() {
parent::validate_data();
if (!isset($this->relateduserid)) {
throw new \coding_exception('The \'relateduserid\' must be set.');
}
if (!isset($this->other['quizid'])) {
throw new \coding_exception('The \'quizid\' value must be set in other.');
}
}
public static function get_objectid_mapping() {
return ['db' => 'quiz_attempts', 'restore' => 'quiz_attempt'];
}
public static function get_other_mapping() {
$othermapped = [];
$othermapped['quizid'] = ['db' => 'quiz', 'restore' => 'quiz'];
return $othermapped;
}
}
@@ -0,0 +1,110 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The mod_quiz attempt preview started event.
*
* @package mod_quiz
* @copyright 2014 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_quiz\event;
defined('MOODLE_INTERNAL') || die();
/**
* The mod_quiz attempt preview started event class.
*
* @property-read array $other {
* Extra information about event.
*
* - int quizid: the id of the quiz.
* }
*
* @package mod_quiz
* @since Moodle 2.7
* @copyright 2014 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class attempt_preview_started extends \core\event\base {
/**
* Init method.
*/
protected function init() {
$this->data['objecttable'] = 'quiz_attempts';
$this->data['crud'] = 'c';
$this->data['edulevel'] = self::LEVEL_TEACHING;
}
/**
* Returns localised general event name.
*
* @return string
*/
public static function get_name() {
return get_string('eventattemptpreviewstarted', 'mod_quiz');
}
/**
* Returns description of what happened.
*
* @return string
*/
public function get_description() {
return "The user with id '$this->relateduserid' has had their attempt with id '$this->objectid' previewed by " .
"the user with id '$this->userid' for the quiz with course module id '$this->contextinstanceid'.";
}
/**
* Returns relevant URL.
*
* @return \moodle_url
*/
public function get_url() {
return new \moodle_url('/mod/quiz/view.php', ['id' => $this->contextinstanceid]);
}
/**
* Custom validation.
*
* @throws \coding_exception
* @return void
*/
protected function validate_data() {
parent::validate_data();
if (!isset($this->relateduserid)) {
throw new \coding_exception('The \'relateduserid\' must be set.');
}
if (!isset($this->other['quizid'])) {
throw new \coding_exception('The \'quizid\' value must be set in other.');
}
}
public static function get_objectid_mapping() {
return ['db' => 'quiz_attempts', 'restore' => 'quiz_attempt'];
}
public static function get_other_mapping() {
$othermapped = [];
$othermapped['quizid'] = ['db' => 'quiz', 'restore' => 'quiz'];
return $othermapped;
}
}
@@ -0,0 +1,140 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The mod_quiz attempt question restarted event.
*
* @package mod_quiz
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_quiz\event;
/**
* The mod_quiz attempt question restarted event class.
*
* @property-read array $other {
* Extra information about event.
*
* - int quizid: the id of the quiz.
* - int page: the page number of attempt.
* }
*
* @package mod_quiz
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class attempt_question_restarted extends \core\event\base {
/**
* Init method.
*/
protected function init() {
$this->data['objecttable'] = 'quiz_attempts';
$this->data['crud'] = 'u';
$this->data['edulevel'] = self::LEVEL_PARTICIPATING;
}
/**
* Returns localised general event name.
*
* @return string
*/
public static function get_name() {
return get_string('eventattemptquestionrestarted', 'mod_quiz');
}
/**
* Returns description of what happened.
*
* @return string
*/
public function get_description() {
$pagenumber = $this->other['page'] + 1;
return "The user with id '$this->userid' has restarted question at slot '{$this->other['slot']}' on page " .
"'{$pagenumber}' of the attempt with id '$this->objectid' belonging to the user " .
"with id '$this->relateduserid' for the quiz with course module id '$this->contextinstanceid', " .
"and the new question id is '{$this->other['newquestionid']}'.";
}
/**
* Returns relevant URL.
*
* @return \moodle_url
*/
public function get_url() {
return new \moodle_url('/mod/quiz/review.php', [
'attempt' => $this->objectid,
'page' => $this->other['page']
]);
}
/**
* Custom validation.
*
* @throws \coding_exception
* @return void
*/
protected function validate_data() {
parent::validate_data();
if (!isset($this->relateduserid)) {
throw new \coding_exception('The \'relateduserid\' must be set.');
}
if (!isset($this->other['quizid'])) {
throw new \coding_exception('The \'quizid\' value must be set in other.');
}
if (!isset($this->other['page'])) {
throw new \coding_exception('The \'page\' value must be set in other.');
}
if (!isset($this->other['slot'])) {
throw new \coding_exception('The \'slot\' value must be set in other.');
}
if (!isset($this->other['newquestionid'])) {
throw new \coding_exception('The \'newquestionid\' value must be set in other.');
}
}
/**
* This is used when restoring course logs where it is required that we
* map the information in 'other' to it's new value in the new course.
*
* @return array List of mapping of other ids.
*/
public static function get_objectid_mapping() {
return ['db' => 'quiz_attempts', 'restore' => 'quiz_attempt'];
}
/**
* This is used when restoring course logs where it is required that we
* map the information in 'other' to it's new value in the new course.
*
* @return array List of mapping of other ids.
*/
public static function get_other_mapping() {
$othermapped = [];
$othermapped['quizid'] = ['db' => 'quiz', 'restore' => 'quiz'];
$othermapped['newquestionid'] = ['db' => 'question', 'restore' => 'question'];
return $othermapped;
}
}
+122
View File
@@ -0,0 +1,122 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The mod_quiz attempt regraded event.
*
* @package mod_quiz
* @copyright 2020 Russell Boyatt <russell.boyatt@warwick.ac.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_quiz\event;
defined('MOODLE_INTERNAL') || die();
/**
* The mod_quiz attempt regraded event class.
*
* @property-read array $other {
* Extra information about event.
*
* - int quizid: the id of the quiz.
* }
*
* @package mod_quiz
* @copyright 2020 Russell Boyatt <russell.boyatt@warwick.ac.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class attempt_regraded extends \core\event\base {
/**
* Init method.
*/
protected function init() {
$this->data['objecttable'] = 'quiz_attempts';
$this->data['crud'] = 'u';
$this->data['edulevel'] = self::LEVEL_TEACHING;
}
/**
* Returns localised general event name.
*
* @return string
*/
public static function get_name() {
return get_string('eventquizattemptregraded', 'mod_quiz');
}
/**
* Returns description of what happened.
*
* @return string
*/
public function get_description() {
return "The user with id '$this->userid' regraded quiz attempt with id '$this->objectid' by user " .
"with id '$this->relateduserid' for the quiz with course module id '$this->contextinstanceid'.";
}
/**
* Returns relevant URL.
*
* @return \moodle_url
*/
public function get_url() {
return new \moodle_url('/mod/quiz/review.php', ['attempt' => $this->objectid]);
}
/**
* Custom validation.
*
* @throws \coding_exception
* @return void
*/
protected function validate_data() {
parent::validate_data();
if (!isset($this->relateduserid)) {
throw new \coding_exception('The \'relateduserid\' must be set.');
}
if (!isset($this->userid)) {
throw new \coding_exception('The \'userid\' must be set.');
}
if (!isset($this->other['quizid'])) {
throw new \coding_exception('The \'quizid\' value must be set in other.');
}
}
/**
* Get mapping to objects
*
* @return array Array of mappings
*/
public static function get_objectid_mapping() {
return ['db' => 'quiz_attempts', 'restore' => 'quiz_attempt'];
}
/**
* Retrieve other mapping detail for the event.
*
* @return array Array of array mappings
*/
public static function get_other_mapping() {
$othermapped = [];
$othermapped['quizid'] = ['db' => 'quiz', 'restore' => 'quiz'];
return $othermapped;
}
}
@@ -0,0 +1,81 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\event;
use coding_exception;
use core\event\base;
use moodle_url;
/**
* Event fired when a quiz attempt is reopened.
*
* @property-read array $other {
* Extra information about event.
*
* - int submitterid: id of submitter (null when triggered by CLI script).
* - int quizid: (optional) id of the quiz.
* }
*
* @package mod_quiz
* @since Moodle 4.2
* @copyright 2023 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class attempt_reopened extends base {
protected function init() {
$this->data['objecttable'] = 'quiz_attempts';
$this->data['crud'] = 'u';
$this->data['edulevel'] = self::LEVEL_TEACHING;
}
public function get_description(): string {
return "The user with id '$this->relateduserid' has had their attempt with id '$this->objectid'" .
"for the quiz with course module id '$this->contextinstanceid' re-opened by the user with id '$this->userid'.";
}
public static function get_name(): string {
return get_string('eventquizattemptreopened', 'mod_quiz');
}
public function get_url(): moodle_url {
return new moodle_url('/mod/quiz/review.php', ['attempt' => $this->objectid]);
}
protected function validate_data(): void {
parent::validate_data();
if (!isset($this->relateduserid)) {
throw new coding_exception('The \'relateduserid\' must be set.');
}
if (!array_key_exists('submitterid', $this->other)) {
throw new coding_exception('The \'submitterid\' value must be set in other.');
}
}
public static function get_objectid_mapping(): array {
return ['db' => 'quiz_attempts', 'restore' => 'quiz_attempt'];
}
public static function get_other_mapping(): array {
return [
'submitterid' => ['db' => 'user', 'restore' => 'user'],
'quizid' => ['db' => 'quiz', 'restore' => 'quiz'],
];
}
}
+109
View File
@@ -0,0 +1,109 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The mod_quiz attempt reviewed event.
*
* @package mod_quiz
* @copyright 2014 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_quiz\event;
defined('MOODLE_INTERNAL') || die();
/**
* The mod_quiz attempt reviewed event class.
*
* @property-read array $other {
* Extra information about event.
*
* - int quizid: the id of the quiz.
* }
*
* @package mod_quiz
* @since Moodle 2.7
* @copyright 2014 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class attempt_reviewed extends \core\event\base {
/**
* Init method.
*/
protected function init() {
$this->data['objecttable'] = 'quiz_attempts';
$this->data['crud'] = 'r';
$this->data['edulevel'] = self::LEVEL_PARTICIPATING;
}
/**
* Returns localised general event name.
*
* @return string
*/
public static function get_name() {
return get_string('eventattemptreviewed', 'mod_quiz');
}
/**
* Returns description of what happened.
*
* @return string
*/
public function get_description() {
return "The user with id '$this->userid' has reviewed quiz attempt with id '$this->objectid' by user ".
"with id '$this->relateduserid' for the quiz with course module id '$this->contextinstanceid'.";
}
/**
* Returns relevant URL.
*
* @return \moodle_url
*/
public function get_url() {
return new \moodle_url('/mod/quiz/review.php', ['attempt' => $this->objectid]);
}
/**
* Custom validation.
*
* @throws \coding_exception
* @return void
*/
protected function validate_data() {
parent::validate_data();
if (!isset($this->relateduserid)) {
throw new \coding_exception('The \'relateduserid\' must be set.');
}
if (!isset($this->other['quizid'])) {
throw new \coding_exception('The \'quizid\' value must be set in other.');
}
}
public static function get_objectid_mapping() {
return ['db' => 'quiz_attempts', 'restore' => 'quiz_attempt'];
}
public static function get_other_mapping() {
$othermapped = [];
$othermapped['quizid'] = ['db' => 'quiz', 'restore' => 'quiz'];
return $othermapped;
}
}
+103
View File
@@ -0,0 +1,103 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The mod_quiz attempt started event.
*
* @package mod_quiz
* @copyright 2013 Adrian Greeve <adrian@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_quiz\event;
defined('MOODLE_INTERNAL') || die();
/**
* The mod_quiz attempt started event class.
*
* @property-read array $other {
* Extra information about event.
*
* - int quizid: (optional) the id of the quiz.
* }
*
* @package mod_quiz
* @since Moodle 2.6
* @copyright 2013 Adrian Greeve <adrian@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class attempt_started extends \core\event\base {
/**
* Init method.
*/
protected function init() {
$this->data['objecttable'] = 'quiz_attempts';
$this->data['crud'] = 'c';
$this->data['edulevel'] = self::LEVEL_PARTICIPATING;
}
/**
* Returns description of what happened.
*
* @return string
*/
public function get_description() {
return "The user with id '$this->relateduserid' has started the attempt with id '$this->objectid' for the " .
"quiz with course module id '$this->contextinstanceid'.";
}
/**
* Returns localised general event name.
*
* @return string
*/
public static function get_name() {
return get_string('eventquizattemptstarted', 'mod_quiz');
}
/**
* Returns relevant URL.
*
* @return \moodle_url
*/
public function get_url() {
return new \moodle_url('/mod/quiz/review.php', ['attempt' => $this->objectid]);
}
/**
* Custom validation.
*
* @throws \coding_exception
* @return void
*/
protected function validate_data() {
parent::validate_data();
if (!isset($this->relateduserid)) {
throw new \coding_exception('The \'relateduserid\' must be set.');
}
}
public static function get_objectid_mapping() {
return ['db' => 'quiz_attempts', 'restore' => 'quiz_attempt'];
}
public static function get_other_mapping() {
$othermapped = [];
$othermapped['quizid'] = ['db' => 'quiz', 'restore' => 'quiz'];
return $othermapped;
}
}
@@ -0,0 +1,111 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The mod_quiz attempt submitted event.
*
* @package mod_quiz
* @copyright 2013 Adrian Greeve <adrian@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_quiz\event;
defined('MOODLE_INTERNAL') || die();
/**
* The mod_quiz attempt submitted event class.
*
* @property-read array $other {
* Extra information about event.
*
* - int submitterid: id of submitter (null when trigged by CLI script).
* - int quizid: (optional) the id of the quiz.
* - bool studentisonline: is the student currently interacting with Moodle?
* }
*
* @package mod_quiz
* @since Moodle 2.6
* @copyright 2013 Adrian Greeve <adrian@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class attempt_submitted extends \core\event\base {
/**
* Init method.
*/
protected function init() {
$this->data['objecttable'] = 'quiz_attempts';
$this->data['crud'] = 'u';
$this->data['edulevel'] = self::LEVEL_PARTICIPATING;
}
/**
* Returns description of what happened.
*
* @return string
*/
public function get_description() {
return "The user with id '$this->relateduserid' has submitted the attempt with id '$this->objectid' for the " .
"quiz with course module id '$this->contextinstanceid'.";
}
/**
* Returns localised general event name.
*
* @return string
*/
public static function get_name() {
return get_string('eventquizattemptsubmitted', 'mod_quiz');
}
/**
* Returns relevant URL.
*
* @return \moodle_url
*/
public function get_url() {
return new \moodle_url('/mod/quiz/review.php', ['attempt' => $this->objectid]);
}
/**
* Custom validation.
*
* @throws \coding_exception
* @return void
*/
protected function validate_data() {
parent::validate_data();
if (!isset($this->relateduserid)) {
throw new \coding_exception('The \'relateduserid\' must be set.');
}
if (!array_key_exists('submitterid', $this->other)) {
throw new \coding_exception('The \'submitterid\' value must be set in other.');
}
}
public static function get_objectid_mapping() {
return ['db' => 'quiz_attempts', 'restore' => 'quiz_attempt'];
}
public static function get_other_mapping() {
$othermapped = [];
$othermapped['submitterid'] = ['db' => 'user', 'restore' => 'user'];
$othermapped['quizid'] = ['db' => 'quiz', 'restore' => 'quiz'];
return $othermapped;
}
}
@@ -0,0 +1,112 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The mod_quiz attempt summary viewed event.
*
* @package mod_quiz
* @copyright 2014 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_quiz\event;
defined('MOODLE_INTERNAL') || die();
/**
* The mod_quiz attempt summary viewed event class.
*
* @property-read array $other {
* Extra information about event.
*
* - int quizid: the id of the quiz.
* }
*
* @package mod_quiz
* @since Moodle 2.7
* @copyright 2014 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class attempt_summary_viewed extends \core\event\base {
/**
* Init method.
*
* @return void
*/
protected function init() {
$this->data['objecttable'] = 'quiz_attempts';
$this->data['crud'] = 'r';
$this->data['edulevel'] = self::LEVEL_PARTICIPATING;
}
/**
* Return localised event name.
*
* @return string
*/
public static function get_name() {
return get_string('eventattemptsummaryviewed', 'mod_quiz');
}
/**
* Returns description of what happened.
*
* @return string
*/
public function get_description() {
return "The user with id '$this->userid' has viewed the summary for the attempt with id '$this->objectid' belonging " .
"to the user with id '$this->relateduserid' for the quiz with course module id '$this->contextinstanceid'.";
}
/**
* Get URL related to the action.
*
* @return \moodle_url
*/
public function get_url() {
return new \moodle_url('/mod/quiz/summary.php', ['attempt' => $this->objectid]);
}
/**
* Custom validation.
*
* @throws \coding_exception
* @return void
*/
protected function validate_data() {
parent::validate_data();
if (!isset($this->relateduserid)) {
throw new \coding_exception('The \'relateduserid\' must be set.');
}
if (!isset($this->other['quizid'])) {
throw new \coding_exception('The \'quizid\' must be set in other.');
}
}
public static function get_objectid_mapping() {
return ['db' => 'quiz_attempts', 'restore' => 'quiz_attempt'];
}
public static function get_other_mapping() {
$othermapped = [];
$othermapped['quizid'] = ['db' => 'quiz', 'restore' => 'quiz'];
return $othermapped;
}
}
+130
View File
@@ -0,0 +1,130 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The mod_quiz attempt updated event.
*
* @package mod_quiz
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_quiz\event;
/**
* The mod_quiz attempt updated event class.
*
* @property-read array $other {
* Extra information about event.
*
* - int quizid: the id of the quiz.
* - int page: the page number of attempt.
* }
*
* @package mod_quiz
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class attempt_updated extends \core\event\base {
/**
* Init method.
*/
protected function init() {
$this->data['objecttable'] = 'quiz_attempts';
$this->data['crud'] = 'u';
$this->data['edulevel'] = self::LEVEL_PARTICIPATING;
}
/**
* Returns localised general event name.
*
* @return string
*/
public static function get_name() {
return get_string('eventattemptupdated', 'mod_quiz');
}
/**
* Returns description of what happened.
*
* @return string
*/
public function get_description() {
$pagenumber = $this->other['page'] + 1;
return "The user with id '$this->userid' has updated responses on page '{$pagenumber}' of the attempt " .
"with id '$this->objectid' belonging to the user " .
"with id '$this->relateduserid' for the quiz with course module id '$this->contextinstanceid'.";
}
/**
* Returns relevant URL.
*
* @return \moodle_url
*/
public function get_url() {
return new \moodle_url('/mod/quiz/review.php', [
'attempt' => $this->objectid,
'page' => $this->other['page']
]);
}
/**
* Custom validation.
*
* @throws \coding_exception
* @return void
*/
protected function validate_data() {
parent::validate_data();
if (!isset($this->relateduserid)) {
throw new \coding_exception('The \'relateduserid\' must be set.');
}
if (!isset($this->other['quizid'])) {
throw new \coding_exception('The \'quizid\' value must be set in other.');
}
if (!isset($this->other['page'])) {
throw new \coding_exception('The \'page\' value must be set in other.');
}
}
/**
* This is used when restoring course logs where it is required that we
* map the objectid to it's new value in the new course.
*
* @return array Mapping of object id.
*/
public static function get_objectid_mapping() {
return ['db' => 'quiz_attempts', 'restore' => 'quiz_attempt'];
}
/**
* This is used when restoring course logs where it is required that we
* map the information in 'other' to it's new value in the new course.
*
* @return array List of mapping of other ids.
*/
public static function get_other_mapping() {
$othermapped = [];
$othermapped['quizid'] = ['db' => 'quiz', 'restore' => 'quiz'];
return $othermapped;
}
}
+120
View File
@@ -0,0 +1,120 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The mod_quiz attempt viewed event.
*
* @package mod_quiz
* @copyright 2014 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_quiz\event;
defined('MOODLE_INTERNAL') || die();
/**
* The mod_quiz attempt viewed event class.
*
* @property-read array $other {
* Extra information about event.
*
* - int quizid: the id of the quiz.
* - int page: the page number of attempt.
* }
*
* @package mod_quiz
* @since Moodle 2.7
* @copyright 2014 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class attempt_viewed extends \core\event\base {
/**
* Init method.
*/
protected function init() {
$this->data['objecttable'] = 'quiz_attempts';
$this->data['crud'] = 'r';
$this->data['edulevel'] = self::LEVEL_PARTICIPATING;
}
/**
* Returns localised general event name.
*
* @return string
*/
public static function get_name() {
return get_string('eventattemptviewed', 'mod_quiz');
}
/**
* Returns description of what happened.
*
* @return string
*/
public function get_description() {
$page = isset($this->other['page']) ? $this->other['page'] + 1 : '';
return "The user with id '$this->userid' has viewed page '$page' of the attempt with id " .
"'$this->objectid' belonging to the user with id '$this->relateduserid' for the quiz " .
"with course module id '$this->contextinstanceid'.";
}
/**
* Returns relevant URL.
*
* @return \moodle_url
*/
public function get_url() {
return new \moodle_url('/mod/quiz/review.php', [
'attempt' => $this->objectid,
'page' => isset($this->other['page']) ? $this->other['page'] : 0
]);
}
/**
* Custom validation.
*
* @throws \coding_exception
* @return void
*/
protected function validate_data() {
parent::validate_data();
if (!isset($this->relateduserid)) {
throw new \coding_exception('The \'relateduserid\' must be set.');
}
if (!isset($this->other['quizid'])) {
throw new \coding_exception('The \'quizid\' value must be set in other.');
}
if (!isset($this->other['page'])) {
throw new \coding_exception('The \'page\' value must be set in other.');
}
}
public static function get_objectid_mapping() {
return ['db' => 'quiz_attempts', 'restore' => 'quiz_attempt'];
}
public static function get_other_mapping() {
$othermapped = [];
$othermapped['quizid'] = ['db' => 'quiz', 'restore' => 'quiz'];
return $othermapped;
}
}
@@ -0,0 +1,39 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The mod_quiz instance list viewed event.
*
* @package mod_quiz
* @copyright 2014 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_quiz\event;
defined('MOODLE_INTERNAL') || die();
/**
* The mod_quiz instance list viewed event class.
*
* @package mod_quiz
* @since Moodle 2.7
* @copyright 2014 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class course_module_instance_list_viewed extends \core\event\course_module_instance_list_viewed {
// No code required here as the parent class handles it all.
}
@@ -0,0 +1,53 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The mod_quiz course module viewed event.
*
* @package mod_quiz
* @copyright 2014 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_quiz\event;
defined('MOODLE_INTERNAL') || die();
/**
* The mod_quiz course module viewed event class.
*
* @package mod_quiz
* @since Moodle 2.7
* @copyright 2014 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class course_module_viewed extends \core\event\course_module_viewed {
/**
* Init method.
*
* @return void
*/
protected function init() {
$this->data['crud'] = 'r';
$this->data['edulevel'] = self::LEVEL_PARTICIPATING;
$this->data['objecttable'] = 'quiz';
}
public static function get_objectid_mapping() {
return ['db' => 'quiz', 'restore' => 'quiz'];
}
}
+101
View File
@@ -0,0 +1,101 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The mod_quiz edit page viewed event.
*
* @package mod_quiz
* @copyright 2014 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_quiz\event;
defined('MOODLE_INTERNAL') || die();
/**
* The mod_quiz edit page viewed event class.
*
* @property-read array $other {
* Extra information about event.
*
* - int quizid: the id of the quiz.
* }
*
* @package mod_quiz
* @since Moodle 2.7
* @copyright 2014 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class edit_page_viewed extends \core\event\base {
/**
* Init method.
*/
protected function init() {
$this->data['crud'] = 'r';
$this->data['edulevel'] = self::LEVEL_TEACHING;
}
/**
* Returns localised general event name.
*
* @return string
*/
public static function get_name() {
return get_string('eventeditpageviewed', 'mod_quiz');
}
/**
* Returns description of what happened.
*
* @return string
*/
public function get_description() {
return "The user with id '$this->userid' viewed the edit page for the quiz with " .
"course module id '$this->contextinstanceid'.";
}
/**
* Returns relevant URL.
*
* @return \moodle_url
*/
public function get_url() {
return new \moodle_url('/mod/quiz/edit.php', ['cmid' => $this->contextinstanceid]);
}
/**
* Custom validation.
*
* @throws \coding_exception
* @return void
*/
protected function validate_data() {
parent::validate_data();
if (!isset($this->other['quizid'])) {
throw new \coding_exception('The \'quizid\' value must be set in other.');
}
}
public static function get_other_mapping() {
$othermapped = [];
$othermapped['quizid'] = ['db' => 'quiz', 'restore' => 'quiz'];
return $othermapped;
}
}
@@ -0,0 +1,112 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The mod_quiz group override created event.
*
* @package mod_quiz
* @copyright 2014 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_quiz\event;
defined('MOODLE_INTERNAL') || die();
/**
* The mod_quiz group override created event class.
*
* @property-read array $other {
* Extra information about event.
*
* - int quizid: the id of the quiz.
* - int groupid: the id of the group.
* }
*
* @package mod_quiz
* @since Moodle 2.7
* @copyright 2014 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class group_override_created extends \core\event\base {
/**
* Init method.
*/
protected function init() {
$this->data['objecttable'] = 'quiz_overrides';
$this->data['crud'] = 'c';
$this->data['edulevel'] = self::LEVEL_TEACHING;
}
/**
* Returns localised general event name.
*
* @return string
*/
public static function get_name() {
return get_string('eventoverridecreated', 'mod_quiz');
}
/**
* Returns description of what happened.
*
* @return string
*/
public function get_description() {
return "The user with id '$this->userid' created the override with id '$this->objectid' for the quiz with " .
"course module id '$this->contextinstanceid' for the group with id '{$this->other['groupid']}'.";
}
/**
* Returns relevant URL.
*
* @return \moodle_url
*/
public function get_url() {
return new \moodle_url('/mod/quiz/overrideedit.php', ['id' => $this->objectid]);
}
/**
* Custom validation.
*
* @throws \coding_exception
* @return void
*/
protected function validate_data() {
parent::validate_data();
if (!isset($this->other['quizid'])) {
throw new \coding_exception('The \'quizid\' value must be set in other.');
}
if (!isset($this->other['groupid'])) {
throw new \coding_exception('The \'groupid\' value must be set in other.');
}
}
public static function get_objectid_mapping() {
return ['db' => 'quiz_overrides', 'restore' => 'quiz_override'];
}
public static function get_other_mapping() {
$othermapped = [];
$othermapped['quizid'] = ['db' => 'quiz', 'restore' => 'quiz'];
$othermapped['groupid'] = ['db' => 'groups', 'restore' => 'group'];
return $othermapped;
}
}
@@ -0,0 +1,111 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The mod_quiz group override deleted event.
*
* @package mod_quiz
* @copyright 2014 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_quiz\event;
defined('MOODLE_INTERNAL') || die();
/**
* The mod_quiz group override deleted event class.
*
* @property-read array $other {
* Extra information about event.
*
* - int quizid: the id of the quiz.
* - int groupid: the id of the group.
* }
*
* @package mod_quiz
* @since Moodle 2.7
* @copyright 2014 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class group_override_deleted extends \core\event\base {
/**
* Init method.
*/
protected function init() {
$this->data['objecttable'] = 'quiz_overrides';
$this->data['crud'] = 'd';
$this->data['edulevel'] = self::LEVEL_TEACHING;
}
/**
* Returns localised general event name.
*
* @return string
*/
public static function get_name() {
return get_string('eventoverridedeleted', 'mod_quiz');
}
/**
* Returns description of what happened.
*
* @return string
*/
public function get_description() {
return "The user with id '$this->userid' deleted the override with id '$this->objectid' for the quiz with " .
"course module id '$this->contextinstanceid' for the group with id '{$this->other['groupid']}'.";
}
/**
* Returns relevant URL.
*
* @return \moodle_url
*/
public function get_url() {
return new \moodle_url('/mod/quiz/overrides.php', ['cmid' => $this->contextinstanceid]);
}
/**
* Custom validation.
*
* @throws \coding_exception
* @return void
*/
protected function validate_data() {
parent::validate_data();
if (!isset($this->other['quizid'])) {
throw new \coding_exception('The \'quizid\' value must be set in other.');
}
if (!isset($this->other['groupid'])) {
throw new \coding_exception('The \'groupid\' value must be set in other.');
}
}
public static function get_objectid_mapping() {
return ['db' => 'quiz_overrides', 'restore' => 'quiz_override'];
}
public static function get_other_mapping() {
$othermapped = [];
$othermapped['quizid'] = ['db' => 'quiz', 'restore' => 'quiz'];
$othermapped['groupid'] = ['db' => 'groups', 'restore' => 'group'];
return $othermapped;
}
}
@@ -0,0 +1,111 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The mod_quiz group override updated event.
*
* @package mod_quiz
* @copyright 2014 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_quiz\event;
defined('MOODLE_INTERNAL') || die();
/**
* The mod_quiz group override updated event class.
*
* @property-read array $other {
* Extra information about event.
*
* - int quizid: the id of the quiz.
* - int groupid: the id of the group.
* }
*
* @package mod_quiz
* @since Moodle 2.7
* @copyright 2014 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class group_override_updated extends \core\event\base {
/**
* Init method.
*/
protected function init() {
$this->data['objecttable'] = 'quiz_overrides';
$this->data['crud'] = 'u';
$this->data['edulevel'] = self::LEVEL_TEACHING;
}
/**
* Returns localised general event name.
*
* @return string
*/
public static function get_name() {
return get_string('eventoverrideupdated', 'mod_quiz');
}
/**
* Returns description of what happened.
*
* @return string
*/
public function get_description() {
return "The user with id '$this->userid' updated the override with id '$this->objectid' for the quiz with " .
"course module id '$this->contextinstanceid' for the group with id '{$this->other['groupid']}'.";
}
/**
* Returns relevant URL.
*
* @return \moodle_url
*/
public function get_url() {
return new \moodle_url('/mod/quiz/overrideedit.php', ['id' => $this->objectid]);
}
/**
* Custom validation.
*
* @throws \coding_exception
* @return void
*/
protected function validate_data() {
parent::validate_data();
if (!isset($this->other['quizid'])) {
throw new \coding_exception('The \'quizid\' value must be set in other.');
}
if (!isset($this->other['groupid'])) {
throw new \coding_exception('The \'groupid\' value must be set in other.');
}
}
public static function get_objectid_mapping() {
return ['db' => 'quiz_overrides', 'restore' => 'quiz_override'];
}
public static function get_other_mapping() {
$othermapped = [];
$othermapped['quizid'] = ['db' => 'quiz', 'restore' => 'quiz'];
$othermapped['groupid'] = ['db' => 'groups', 'restore' => 'group'];
return $othermapped;
}
}
@@ -0,0 +1,94 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The mod_quiz page break created event.
*
* @package mod_quiz
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_quiz\event;
/**
* The mod_quiz page break created event class.
*
* @property-read array $other {
* Extra information about event.
*
* - int quizid: the id of the quiz.
* - int slotnumber: the slot number which we will add the page break before.
* }
*
* @package mod_quiz
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class page_break_created extends \core\event\base {
protected function init() {
$this->data['objecttable'] = 'quiz_slots';
$this->data['crud'] = 'c';
$this->data['edulevel'] = self::LEVEL_TEACHING;
}
public static function get_name() {
return get_string('eventpagebreakcreated', 'mod_quiz');
}
public function get_description() {
return "The user with id '$this->userid' created the page break before the slot with id '{$this->objectid}' " .
"and slot number '{$this->other['slotnumber']}' " .
"belonging to the quiz with course module id '$this->contextinstanceid'.";
}
public function get_url() {
return new \moodle_url('/mod/quiz/edit.php', [
'cmid' => $this->contextinstanceid
]);
}
protected function validate_data() {
parent::validate_data();
if (!isset($this->objectid)) {
throw new \coding_exception('The \'objectid\' value must be set.');
}
if (!isset($this->contextinstanceid)) {
throw new \coding_exception('The \'contextinstanceid\' value must be set.');
}
if (!isset($this->other['quizid'])) {
throw new \coding_exception('The \'quizid\' value must be set in other.');
}
if (!isset($this->other['slotnumber'])) {
throw new \coding_exception('The \'slotnumber\' value must be set in other.');
}
}
public static function get_objectid_mapping() {
return ['db' => 'quiz_slots', 'restore' => 'quiz_question_instance'];
}
public static function get_other_mapping() {
$othermapped = [];
$othermapped['quizid'] = ['db' => 'quiz', 'restore' => 'quiz'];
return $othermapped;
}
}
@@ -0,0 +1,94 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The mod_quiz page break deleted event.
*
* @package mod_quiz
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_quiz\event;
/**
* The mod_quiz page break deleted event class.
*
* @property-read array $other {
* Extra information about event.
*
* - int quizid: the id of the quiz.
* - int slotnumber: the slot number which we will remove the page break before.
* }
*
* @package mod_quiz
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class page_break_deleted extends \core\event\base {
protected function init() {
$this->data['objecttable'] = 'quiz_slots';
$this->data['crud'] = 'd';
$this->data['edulevel'] = self::LEVEL_TEACHING;
}
public static function get_name() {
return get_string('eventpagebreakdeleted', 'mod_quiz');
}
public function get_description() {
return "The user with id '$this->userid' deleted the page break before the slot with id '{$this->objectid}' " .
"and slot number '{$this->other['slotnumber']}' " .
"belonging to the quiz with course module id '$this->contextinstanceid'.";
}
public function get_url() {
return new \moodle_url('/mod/quiz/edit.php', [
'cmid' => $this->contextinstanceid
]);
}
protected function validate_data() {
parent::validate_data();
if (!isset($this->objectid)) {
throw new \coding_exception('The \'objectid\' value must be set.');
}
if (!isset($this->contextinstanceid)) {
throw new \coding_exception('The \'contextinstanceid\' value must be set.');
}
if (!isset($this->other['quizid'])) {
throw new \coding_exception('The \'quizid\' value must be set in other.');
}
if (!isset($this->other['slotnumber'])) {
throw new \coding_exception('The \'slotnumber\' value must be set in other.');
}
}
public static function get_objectid_mapping() {
return ['db' => 'quiz_slots', 'restore' => 'quiz_question_instance'];
}
public static function get_other_mapping() {
$othermapped = [];
$othermapped['quizid'] = ['db' => 'quiz', 'restore' => 'quiz'];
return $othermapped;
}
}
@@ -0,0 +1,117 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The mod_quiz question manually graded event.
*
* @package core
* @copyright 2014 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_quiz\event;
defined('MOODLE_INTERNAL') || die();
/**
* The mod_quiz question manually graded event class.
*
* @property-read array $other {
* Extra information about event.
*
* - int quizid: the id of the quiz.
* - int attemptid: the id of the attempt.
* - int slot: the question number in the attempt.
* }
*
* @package core
* @since Moodle 2.7
* @copyright 2014 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class question_manually_graded extends \core\event\base {
/**
* Init method.
*/
protected function init() {
$this->data['objecttable'] = 'question';
$this->data['crud'] = 'u';
$this->data['edulevel'] = self::LEVEL_TEACHING;
}
/**
* Returns localised general event name.
*
* @return string
*/
public static function get_name() {
return get_string('eventquestionmanuallygraded', 'mod_quiz');
}
/**
* Returns description of what happened.
*
* @return string
*/
public function get_description() {
return "The user with id '$this->userid' manually graded the question with id '$this->objectid' for the attempt " .
"with id '{$this->other['attemptid']}' for the quiz with course module id '$this->contextinstanceid'.";
}
/**
* Returns relevant URL.
*
* @return \moodle_url
*/
public function get_url() {
return new \moodle_url('/mod/quiz/comment.php', ['attempt' => $this->other['attemptid'],
'slot' => $this->other['slot']]);
}
/**
* Custom validation.
*
* @throws \coding_exception
* @return void
*/
protected function validate_data() {
parent::validate_data();
if (!isset($this->other['quizid'])) {
throw new \coding_exception('The \'quizid\' value must be set in other.');
}
if (!isset($this->other['attemptid'])) {
throw new \coding_exception('The \'attemptid\' value must be set in other.');
}
if (!isset($this->other['slot'])) {
throw new \coding_exception('The \'slot\' value must be set in other.');
}
}
public static function get_objectid_mapping() {
return ['db' => 'question', 'restore' => 'question'];
}
public static function get_other_mapping() {
$othermapped = [];
$othermapped['quizid'] = ['db' => 'quiz', 'restore' => 'quiz'];
$othermapped['attemptid'] = ['db' => 'quiz_attempts', 'restore' => 'quiz_attempt'];
return $othermapped;
}
}
@@ -0,0 +1,72 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\event;
/**
* Event to record a quiz grade item being created.
*
* @property-read array $other {
* }
*
* @package mod_quiz
* @copyright 2023 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class quiz_grade_item_created extends \core\event\base {
protected function init() {
$this->data['objecttable'] = 'quiz_grade_items';
$this->data['crud'] = 'c';
$this->data['edulevel'] = self::LEVEL_TEACHING;
}
public static function get_name() {
return get_string('eventquizgradeitemcreated', 'mod_quiz');
}
public function get_description() {
return "The user with id '$this->userid' created quiz grade item with id '$this->objectid' " .
"for the quiz with course module id '$this->contextinstanceid'.";
}
public function get_url() {
return new \moodle_url('/mod/quiz/editgrading.php', [
'cmid' => $this->contextinstanceid,
]);
}
protected function validate_data() {
parent::validate_data();
if (!isset($this->objectid)) {
throw new \coding_exception('The \'objectid\' value must be set.');
}
if (!isset($this->contextinstanceid)) {
throw new \coding_exception('The \'contextinstanceid\' value must be set.');
}
}
public static function get_objectid_mapping() {
return ['db' => 'quiz_grade_items', 'restore' => 'quiz_grade_items'];
}
public static function get_other_mapping() {
return [
'quizid' => ['db' => 'quiz', 'restore' => 'quiz'],
];
}
}
@@ -0,0 +1,72 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\event;
/**
* Event to record a quiz grade item being deleted.
*
* @property-read array $other {
* }
*
* @package mod_quiz
* @copyright 2023 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class quiz_grade_item_deleted extends \core\event\base {
protected function init() {
$this->data['objecttable'] = 'quiz_grade_items';
$this->data['crud'] = 'd';
$this->data['edulevel'] = self::LEVEL_TEACHING;
}
public static function get_name() {
return get_string('eventquizgradeitemdeleted', 'mod_quiz');
}
public function get_description() {
return "The user with id '$this->userid' deleted quiz grade item with id '$this->objectid' " .
"for the quiz with course module id '$this->contextinstanceid'.";
}
public function get_url() {
return new \moodle_url('/mod/quiz/editgrading.php', [
'cmid' => $this->contextinstanceid,
]);
}
protected function validate_data() {
parent::validate_data();
if (!isset($this->objectid)) {
throw new \coding_exception('The \'objectid\' value must be set.');
}
if (!isset($this->contextinstanceid)) {
throw new \coding_exception('The \'contextinstanceid\' value must be set.');
}
}
public static function get_objectid_mapping() {
return ['db' => 'quiz_grade_items', 'restore' => 'quiz_grade_items'];
}
public static function get_other_mapping() {
return [
'quizid' => ['db' => 'quiz', 'restore' => 'quiz'],
];
}
}
@@ -0,0 +1,72 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\event;
/**
* Event to record a quiz grade item being updated.
*
* @property-read array $other {
* }
*
* @package mod_quiz
* @copyright 2023 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class quiz_grade_item_updated extends \core\event\base {
protected function init() {
$this->data['objecttable'] = 'quiz_grade_items';
$this->data['crud'] = 'u';
$this->data['edulevel'] = self::LEVEL_TEACHING;
}
public static function get_name() {
return get_string('eventquizgradeitemupdated', 'mod_quiz');
}
public function get_description() {
return "The user with id '$this->userid' updated quiz grade item with id '$this->objectid' " .
"for the quiz with course module id '$this->contextinstanceid'.";
}
public function get_url() {
return new \moodle_url('/mod/quiz/editgrading.php', [
'cmid' => $this->contextinstanceid,
]);
}
protected function validate_data() {
parent::validate_data();
if (!isset($this->objectid)) {
throw new \coding_exception('The \'objectid\' value must be set.');
}
if (!isset($this->contextinstanceid)) {
throw new \coding_exception('The \'contextinstanceid\' value must be set.');
}
}
public static function get_objectid_mapping() {
return ['db' => 'quiz_grade_items', 'restore' => 'quiz_grade_items'];
}
public static function get_other_mapping() {
return [
'quizid' => ['db' => 'quiz', 'restore' => 'quiz'],
];
}
}
@@ -0,0 +1,66 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\event;
/**
* Event to record a quiz grade item being re-ordered.
*
* @property-read array $other {
* }
*
* @package mod_quiz
* @copyright 2023 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class quiz_grade_items_reordered extends \core\event\base {
protected function init() {
$this->data['objecttable'] = 'quiz';
$this->data['crud'] = 'u';
$this->data['edulevel'] = self::LEVEL_TEACHING;
}
public static function get_name() {
return get_string('eventquizgradeitemorderchanged', 'mod_quiz');
}
public function get_description() {
return "The user with id '$this->userid' re-ordered the grade items " .
"for the quiz with course module id '$this->contextinstanceid'.";
}
public function get_url() {
return new \moodle_url('/mod/quiz/editgrading.php', [
'cmid' => $this->contextinstanceid,
]);
}
protected function validate_data() {
parent::validate_data();
if (!isset($this->contextinstanceid)) {
throw new \coding_exception('The \'contextinstanceid\' value must be set.');
}
}
public static function get_objectid_mapping() {
return ['db' => 'quiz', 'restore' => 'quiz'];
}
public static function get_other_mapping() {
return [];
}
}
@@ -0,0 +1,91 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The mod_quiz quiz grade updated event.
*
* @package mod_quiz
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_quiz\event;
/**
* The mod_quiz quiz grade updated event class.
*
* @property-read array $other {
* Extra information about event.
*
* - int newgrade: the new maximum grade value.
* - int oldgrade: the old maximum grade value.
* }
*
* @package mod_quiz
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class quiz_grade_updated extends \core\event\base {
protected function init() {
$this->data['objecttable'] = 'quiz';
$this->data['crud'] = 'u';
$this->data['edulevel'] = self::LEVEL_TEACHING;
}
public static function get_name() {
return get_string('eventquizgradeupdated', 'mod_quiz');
}
public function get_description() {
return "The user with id '$this->userid' updated the maximum grade for the quiz with " .
"course module id '$this->contextinstanceid'. " .
"The maximum grade was changed from '{$this->other['oldgrade']}' to '{$this->other['newgrade']}'.";
}
public function get_url() {
return new \moodle_url('/mod/quiz/edit.php', [
'cmid' => $this->contextinstanceid,
]);
}
protected function validate_data() {
parent::validate_data();
if (!isset($this->objectid)) {
throw new \coding_exception('The \'objectid\' value must be set.');
}
if (!isset($this->contextinstanceid)) {
throw new \coding_exception('The \'contextinstanceid\' value must be set.');
}
if (!isset($this->other['oldgrade'])) {
throw new \coding_exception('The \'oldgrade\' value must be set in other.');
}
if (!isset($this->other['newgrade'])) {
throw new \coding_exception('The \'newgrade\' value must be set in other.');
}
}
public static function get_objectid_mapping() {
return ['db' => 'quiz', 'restore' => 'quiz'];
}
public static function get_other_mapping() {
return [];
}
}
@@ -0,0 +1,85 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The mod_quiz quiz re-paginated event.
*
* @package mod_quiz
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_quiz\event;
/**
* The mod_quiz quiz re-paginated event class.
*
* @property-read array $other {
* Extra information about event.
*
* - int slotsperpage: the slot number per page option.
* }
*
* @package mod_quiz
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class quiz_repaginated extends \core\event\base {
protected function init() {
$this->data['objecttable'] = 'quiz';
$this->data['crud'] = 'u';
$this->data['edulevel'] = self::LEVEL_TEACHING;
}
public static function get_name() {
return get_string('eventquizrepaginated', 'mod_quiz');
}
public function get_description() {
return "The user with id '$this->userid' re-paginated the quiz with course module id '$this->contextinstanceid' " .
" with the new option '{$this->other['slotsperpage']}' slot(s) per page.";
}
public function get_url() {
return new \moodle_url('/mod/quiz/edit.php', [
'cmid' => $this->contextinstanceid
]);
}
protected function validate_data() {
parent::validate_data();
if (!isset($this->objectid)) {
throw new \coding_exception('The \'objectid\' value must be set.');
}
if (!isset($this->contextinstanceid)) {
throw new \coding_exception('The \'contextinstanceid\' value must be set.');
}
if (!isset($this->other['slotsperpage'])) {
throw new \coding_exception('The \'slotsperpage\' value must be set in other.');
}
}
public static function get_objectid_mapping() {
return ['db' => 'quiz', 'restore' => 'quiz'];
}
public static function get_other_mapping() {
return [];
}
}
+109
View File
@@ -0,0 +1,109 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The mod_quiz report viewed event.
*
* @package mod_quiz
* @copyright 2014 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_quiz\event;
defined('MOODLE_INTERNAL') || die();
/**
* The mod_quiz report viewed event class.
*
* @property-read array $other {
* Extra information about event.
*
* - int quizid: the id of the quiz.
* - string reportname: the name of the report.
* }
*
* @package mod_quiz
* @since Moodle 2.7
* @copyright 2014 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class report_viewed extends \core\event\base {
/**
* Init method.
*
* @return void
*/
protected function init() {
$this->data['crud'] = 'r';
$this->data['edulevel'] = self::LEVEL_TEACHING;
}
/**
* Return localised event name.
*
* @return string
*/
public static function get_name() {
return get_string('eventreportviewed', 'mod_quiz');
}
/**
* Returns description of what happened.
*
* @return string
*/
public function get_description() {
return "The user with id '$this->userid' viewed the report '" . s($this->other['reportname']) . "' for the quiz with " .
"course module id '$this->contextinstanceid'.";
}
/**
* Get URL related to the action.
*
* @return \moodle_url
*/
public function get_url() {
return new \moodle_url('/mod/quiz/report.php', ['id' => $this->contextinstanceid,
'mode' => $this->other['reportname']]);
}
/**
* Custom validation.
*
* @throws \coding_exception
* @return void
*/
protected function validate_data() {
parent::validate_data();
if (!isset($this->other['quizid'])) {
throw new \coding_exception('The \'quizid\' value must be set in other.');
}
if (!isset($this->other['reportname'])) {
throw new \coding_exception('The \'reportname\' value must be set in other.');
}
}
public static function get_other_mapping() {
$othermapped = [];
$othermapped['quizid'] = ['db' => 'quiz', 'restore' => 'quiz'];
return $othermapped;
}
}
@@ -0,0 +1,106 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The mod_quiz section break created event.
*
* @package mod_quiz
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_quiz\event;
/**
* The mod_quiz section break created event class.
*
* @property-read array $other {
* Extra information about event.
*
* - int quizid: the id of the quiz.
* - int firstslotid: id of the slot which we will add the section break before.
* - int firstslotnumber: slot number of the slot which we will add the section break before.
* - string title: the title of new section.
* }
*
* @package mod_quiz
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class section_break_created extends \core\event\base {
protected function init() {
$this->data['objecttable'] = 'quiz_sections';
$this->data['crud'] = 'c';
$this->data['edulevel'] = self::LEVEL_TEACHING;
}
public static function get_name() {
return get_string('eventsectionbreakcreated', 'mod_quiz');
}
public function get_description() {
return "The user with id '$this->userid' created a new section break with id '{$this->objectid}' " .
"and title '{$this->other['title']}' before the slot with id '{$this->other['firstslotid']}' " .
"and slot number '{$this->other['firstslotnumber']}' " .
"belonging to the quiz with course module id '$this->contextinstanceid'.";
}
public function get_url() {
return new \moodle_url('/mod/quiz/edit.php', [
'cmid' => $this->contextinstanceid
]);
}
protected function validate_data() {
parent::validate_data();
if (!isset($this->objectid)) {
throw new \coding_exception('The \'objectid\' value must be set.');
}
if (!isset($this->contextinstanceid)) {
throw new \coding_exception('The \'contextinstanceid\' value must be set.');
}
if (!isset($this->other['quizid'])) {
throw new \coding_exception('The \'quizid\' value must be set in other.');
}
if (!isset($this->other['firstslotid'])) {
throw new \coding_exception('The \'firstslotid\' value must be set in other.');
}
if (!isset($this->other['firstslotnumber'])) {
throw new \coding_exception('The \'firstslotnumber\' value must be set in other.');
}
if (!isset($this->other['title'])) {
throw new \coding_exception('The \'title\' value must be set in other.');
}
}
public static function get_objectid_mapping() {
return ['db' => 'quiz_sections', 'restore' => 'quiz_section'];
}
public static function get_other_mapping() {
$othermapped = [];
$othermapped['quizid'] = ['db' => 'quiz', 'restore' => 'quiz'];
$othermapped['firstslotid'] = ['db' => 'quiz_slots', 'restore' => 'quiz_question_instance'];
return $othermapped;
}
}
@@ -0,0 +1,100 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The mod_quiz section break deleted event.
*
* @package mod_quiz
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_quiz\event;
/**
* The mod_quiz section break deleted event class.
*
* @property-read array $other {
* Extra information about event.
*
* - int quizid: the id of the quiz.
* - int firstslotid: id of the slot which we will remove the section break before.
* - int firstslotnumber: slot number of the slot which we will remove the section break before.
* }
*
* @package mod_quiz
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class section_break_deleted extends \core\event\base {
protected function init() {
$this->data['objecttable'] = 'quiz_sections';
$this->data['crud'] = 'd';
$this->data['edulevel'] = self::LEVEL_TEACHING;
}
public static function get_name() {
return get_string('eventsectionbreakdeleted', 'mod_quiz');
}
public function get_description() {
return "The user with id '$this->userid' deleted the section break with id '{$this->objectid}' " .
"before the slot with id '{$this->other['firstslotid']}' and slot number '{$this->other['firstslotnumber']}' " .
"belonging to the quiz with course module id '$this->contextinstanceid'.";
}
public function get_url() {
return new \moodle_url('/mod/quiz/edit.php', [
'cmid' => $this->contextinstanceid
]);
}
protected function validate_data() {
parent::validate_data();
if (!isset($this->objectid)) {
throw new \coding_exception('The \'objectid\' value must be set.');
}
if (!isset($this->contextinstanceid)) {
throw new \coding_exception('The \'contextinstanceid\' value must be set.');
}
if (!isset($this->other['quizid'])) {
throw new \coding_exception('The \'quizid\' value must be set in other.');
}
if (!isset($this->other['firstslotid'])) {
throw new \coding_exception('The \'firstslotid\' value must be set in other.');
}
if (!isset($this->other['firstslotnumber'])) {
throw new \coding_exception('The \'firstslotnumber\' value must be set in other.');
}
}
public static function get_objectid_mapping() {
return ['db' => 'quiz_sections', 'restore' => 'quiz_section'];
}
public static function get_other_mapping() {
$othermapped = [];
$othermapped['quizid'] = ['db' => 'quiz', 'restore' => 'quiz'];
$othermapped['firstslotid'] = ['db' => 'quiz_slots', 'restore' => 'quiz_question_instance'];
return $othermapped;
}
}
@@ -0,0 +1,101 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The mod_quiz section shuffle updated event.
*
* @package mod_quiz
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_quiz\event;
/**
* The mod_quiz section shuffle updated event class.
*
* @property-read array $other {
* Extra information about event.
*
* - int quizid: the id of the quiz.
* - bool shuffle: shuffle option value.
* - int firstslotnumber: slot number of the slot which is right after the section break.
* }
*
* @package mod_quiz
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class section_shuffle_updated extends \core\event\base {
protected function init() {
$this->data['objecttable'] = 'quiz_sections';
$this->data['crud'] = 'u';
$this->data['edulevel'] = self::LEVEL_TEACHING;
}
public static function get_name() {
return get_string('eventsectionshuffleupdated', 'mod_quiz');
}
public function get_description() {
return "The user with id '$this->userid' updated the section with id '{$this->objectid}' " .
"before the slot number '{$this->other['firstslotnumber']}' " .
"belonging to the quiz with course module id '$this->contextinstanceid'. " .
"Its shuffle option was set to '{$this->other['shuffle']}'.";
}
public function get_url() {
return new \moodle_url('/mod/quiz/edit.php', [
'cmid' => $this->contextinstanceid
]);
}
protected function validate_data() {
parent::validate_data();
if (!isset($this->objectid)) {
throw new \coding_exception('The \'objectid\' value must be set.');
}
if (!isset($this->contextinstanceid)) {
throw new \coding_exception('The \'contextinstanceid\' value must be set.');
}
if (!isset($this->other['quizid'])) {
throw new \coding_exception('The \'quizid\' value must be set in other.');
}
if (!isset($this->other['firstslotnumber'])) {
throw new \coding_exception('The \'firstslotnumber\' value must be set in other.');
}
if (!isset($this->other['shuffle'])) {
throw new \coding_exception('The \'shuffle\' value must be set in other.');
}
}
public static function get_objectid_mapping() {
return ['db' => 'quiz_sections', 'restore' => 'quiz_section'];
}
public static function get_other_mapping() {
$othermapped = [];
$othermapped['quizid'] = ['db' => 'quiz', 'restore' => 'quiz'];
return $othermapped;
}
}
@@ -0,0 +1,104 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The mod_quiz section title updated event.
*
* @package mod_quiz
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_quiz\event;
/**
* The mod_quiz section title updated event class.
*
* @property-read array $other {
* Extra information about event.
*
* - int quizid: the id of the quiz.
* - string newtitle: new title.
* - int firstslotid: id of the slot which is right after the section break.
* - int firstslotnumber: slot number of the slot which is right after the section break.
* }
*
* @package mod_quiz
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class section_title_updated extends \core\event\base {
protected function init() {
$this->data['objecttable'] = 'quiz_sections';
$this->data['crud'] = 'u';
$this->data['edulevel'] = self::LEVEL_TEACHING;
}
public static function get_name() {
return get_string('eventsectiontitleupdated', 'mod_quiz');
}
public function get_description() {
$description = "The user with id '$this->userid' updated the section with id '{$this->objectid}' ";
if ($this->other['firstslotid'] && $this->other['firstslotnumber']) {
$description .= "before the slot with id '{$this->other['firstslotid']}' " .
"and slot number '{$this->other['firstslotnumber']}' ";
}
$description .= "belonging to the quiz with course module id '$this->contextinstanceid'. " .
"Its title was changed to '{$this->other['newtitle']}'.";
return $description;
}
public function get_url() {
return new \moodle_url('/mod/quiz/edit.php', [
'cmid' => $this->contextinstanceid
]);
}
protected function validate_data() {
parent::validate_data();
if (!isset($this->objectid)) {
throw new \coding_exception('The \'objectid\' value must be set.');
}
if (!isset($this->contextinstanceid)) {
throw new \coding_exception('The \'contextinstanceid\' value must be set.');
}
if (!isset($this->other['quizid'])) {
throw new \coding_exception('The \'quizid\' value must be set in other.');
}
if (!isset($this->other['newtitle'])) {
throw new \coding_exception('The \'newtitle\' value must be set in other.');
}
}
public static function get_objectid_mapping() {
return ['db' => 'quiz_sections', 'restore' => 'quiz_section'];
}
public static function get_other_mapping() {
$othermapped = [];
$othermapped['quizid'] = ['db' => 'quiz', 'restore' => 'quiz'];
$othermapped['firstslotid'] = ['db' => 'quiz_slots', 'restore' => 'quiz_question_instance'];
return $othermapped;
}
}
+100
View File
@@ -0,0 +1,100 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The mod_quiz slots created event.
*
* @package mod_quiz
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_quiz\event;
/**
* The mod_quiz slot created event class.
*
* @property-read array $other {
* Extra information about event.
*
* - int quizid: the id of the quiz.
* - int slotnumber: the slot number in quiz.
* - int page: page number.
* }
*
* @package mod_quiz
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class slot_created extends \core\event\base {
protected function init() {
$this->data['objecttable'] = 'quiz_slots';
$this->data['crud'] = 'c';
$this->data['edulevel'] = self::LEVEL_TEACHING;
}
public static function get_name() {
return get_string('eventslotcreated', 'mod_quiz');
}
public function get_description() {
return "The user with id '$this->userid' created a new slot with id '{$this->objectid}' " .
"and slot number '{$this->other['slotnumber']}' " .
"on page '{$this->other['page']}' " .
"of the quiz with course module id '$this->contextinstanceid'.";
}
public function get_url() {
return new \moodle_url('/mod/quiz/edit.php', [
'cmid' => $this->contextinstanceid
]);
}
protected function validate_data() {
parent::validate_data();
if (!isset($this->objectid)) {
throw new \coding_exception('The \'objectid\' value must be set.');
}
if (!isset($this->contextinstanceid)) {
throw new \coding_exception('The \'contextinstanceid\' value must be set.');
}
if (!isset($this->other['quizid'])) {
throw new \coding_exception('The \'quizid\' value must be set in other.');
}
if (!isset($this->other['slotnumber'])) {
throw new \coding_exception('The \'slotnumber\' value must be set in other.');
}
if (!isset($this->other['page'])) {
throw new \coding_exception('The \'page\' value must be set in other.');
}
}
public static function get_objectid_mapping() {
return ['db' => 'quiz_slots', 'restore' => 'quiz_question_instance'];
}
public static function get_other_mapping() {
$othermapped = [];
$othermapped['quizid'] = ['db' => 'quiz', 'restore' => 'quiz'];
return $othermapped;
}
}
+94
View File
@@ -0,0 +1,94 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The mod_quiz slots deleted event.
*
* @package mod_quiz
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_quiz\event;
/**
* The mod_quiz slot deleted event class.
*
* @property-read array $other {
* Extra information about event.
*
* - int quizid: the id of the quiz.
* - int slotnumber: the slot number in quiz.
* }
*
* @package mod_quiz
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class slot_deleted extends \core\event\base {
protected function init() {
$this->data['objecttable'] = 'quiz_slots';
$this->data['crud'] = 'd';
$this->data['edulevel'] = self::LEVEL_TEACHING;
}
public static function get_name() {
return get_string('eventslotdeleted', 'mod_quiz');
}
public function get_description() {
return "The user with id '$this->userid' deleted the slot with id '{$this->objectid}' " .
"and slot number '{$this->other['slotnumber']}' " .
"from the quiz with course module id '$this->contextinstanceid'.";
}
public function get_url() {
return new \moodle_url('/mod/quiz/edit.php', [
'cmid' => $this->contextinstanceid
]);
}
protected function validate_data() {
parent::validate_data();
if (!isset($this->objectid)) {
throw new \coding_exception('The \'objectid\' value must be set.');
}
if (!isset($this->contextinstanceid)) {
throw new \coding_exception('The \'contextinstanceid\' value must be set.');
}
if (!isset($this->other['quizid'])) {
throw new \coding_exception('The \'quizid\' value must be set in other.');
}
if (!isset($this->other['slotnumber'])) {
throw new \coding_exception('The \'slotnumber\' value must be set in other.');
}
}
public static function get_objectid_mapping() {
return ['db' => 'quiz_slots', 'restore' => 'quiz_question_instance'];
}
public static function get_other_mapping() {
$othermapped = [];
$othermapped['quizid'] = ['db' => 'quiz', 'restore' => 'quiz'];
return $othermapped;
}
}
@@ -0,0 +1,115 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\event;
/**
* The mod_quiz slot display updated event class.
*
* @property-read array $other {
* Extra information about event.
*
* - int quizid: the id of the quiz.
* - string displaynumber: the slot's customised question number value.
* }
*
* @package mod_quiz
* @copyright 2022 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class slot_displaynumber_updated extends \core\event\base {
/**
* Initialise the quiz_slots table.
*/
protected function init(): void {
$this->data['objecttable'] = 'quiz_slots';
$this->data['crud'] = 'u';
$this->data['edulevel'] = self::LEVEL_TEACHING;
}
/**
* Return the name of the event.
*
* @return string
*/
public static function get_name(): string {
return get_string('eventslotdisplayedquestionnumberupdated', 'mod_quiz');
}
/**
* Log describes which user customised the question number in a given slot and in which quiz.
*
* @return string
*/
public function get_description(): string {
return "The user with id '$this->userid' updated the slot with id '{$this->objectid}' " .
"belonging to the quiz with course module id '$this->contextinstanceid'. " .
"Its customised question number value was set to '{$this->other['displaynumber']}'.";
}
/**
* Return the url object of the quiz editing page.
*
* @return \moodle_url
*/
public function get_url(): \moodle_url {
return new \moodle_url('/mod/quiz/edit.php', ['cmid' => $this->contextinstanceid]);
}
/**
* validate the data being logged.
*/
protected function validate_data(): void {
parent::validate_data();
if (!isset($this->objectid)) {
throw new \coding_exception('The \'objectid\' value must be set.');
}
if (!isset($this->contextinstanceid)) {
throw new \coding_exception('The \'contextinstanceid\' value must be set.');
}
if (!isset($this->other['quizid'])) {
throw new \coding_exception('The \'quizid\' value must be set in other.');
}
if (!isset($this->other['displaynumber'])) {
throw new \coding_exception('The \'displaynumber\' value must be set in other.');
}
}
/**
* Return the mapped array.
*
* @return string[]
*/
public static function get_objectid_mapping(): array {
return ['db' => 'quiz_slots', 'restore' => 'quiz_question_instance'];
}
/**
* Return the mapped array.
*
* @return array
*/
public static function get_other_mapping(): array {
$othermapped = [];
$othermapped['quizid'] = ['db' => 'quiz', 'restore' => 'quiz'];
return $othermapped;
}
}
@@ -0,0 +1,93 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\event;
use core\event\base;
/**
* The quiz sub-grade that this slot contributes to has changed.
*
* @property-read array $other {
* Extra information about event.
*
* - int quizid: the id of the quiz.
* - int previousgradeitem: the previous max mark value.
* - int newgradeitem: the new max mark value.
* }
*
* @package mod_quiz
* @copyright 2023 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class slot_grade_item_updated extends base {
protected function init() {
$this->data['objecttable'] = 'quiz_slots';
$this->data['crud'] = 'u';
$this->data['edulevel'] = self::LEVEL_TEACHING;
}
public static function get_name() {
return get_string('eventslotgradeitemupdated', 'mod_quiz');
}
public function get_description() {
return "The user with id '$this->userid' updated the slot with id '{$this->objectid}' " .
"belonging to the quiz with course module id '$this->contextinstanceid'. " .
"The grade item this slot contributes to was changed from '{$this->other['previousgradeitem']}' " .
"to '{$this->other['newgradeitem']}'.";
}
public function get_url() {
return new \moodle_url('/mod/quiz/editgrading.php', [
'cmid' => $this->contextinstanceid,
]);
}
protected function validate_data() {
parent::validate_data();
if (!isset($this->objectid)) {
throw new \coding_exception('The \'objectid\' value must be set.');
}
if (!isset($this->contextinstanceid)) {
throw new \coding_exception('The \'contextinstanceid\' value must be set.');
}
if (!isset($this->other['quizid'])) {
throw new \coding_exception('The \'quizid\' value must be set in other.');
}
if (!array_key_exists('previousgradeitem', $this->other)) {
throw new \coding_exception('The \'previousgradeitem\' value must be set in other.');
}
if (!array_key_exists('newgradeitem', $this->other)) {
throw new \coding_exception('The \'newgradeitem\' value must be set in other.');
}
}
public static function get_objectid_mapping() {
return ['db' => 'quiz_slots', 'restore' => 'quiz_question_instance'];
}
public static function get_other_mapping() {
return [
'quizid' => ['db' => 'quiz', 'restore' => 'quiz'],
];
}
}
@@ -0,0 +1,91 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\event;
/**
* The mark a slot is graded out of has changed.
*
* @property-read array $other {
* Extra information about event.
*
* - int quizid: the id of the quiz.
* - int previousmaxmark: the previous max mark value.
* - int newmaxmark: the new max mark value.
* }
*
* @package mod_quiz
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class slot_mark_updated extends \core\event\base {
protected function init() {
$this->data['objecttable'] = 'quiz_slots';
$this->data['crud'] = 'u';
$this->data['edulevel'] = self::LEVEL_TEACHING;
}
public static function get_name() {
return get_string('eventslotmarkupdated', 'mod_quiz');
}
public function get_description() {
return "The user with id '$this->userid' updated the slot with id '{$this->objectid}' " .
"belonging to the quiz with course module id '$this->contextinstanceid'. " .
"Its max mark was changed from '{$this->other['previousmaxmark']}' to '{$this->other['newmaxmark']}'.";
}
public function get_url() {
return new \moodle_url('/mod/quiz/edit.php', [
'cmid' => $this->contextinstanceid
]);
}
protected function validate_data() {
parent::validate_data();
if (!isset($this->objectid)) {
throw new \coding_exception('The \'objectid\' value must be set.');
}
if (!isset($this->contextinstanceid)) {
throw new \coding_exception('The \'contextinstanceid\' value must be set.');
}
if (!isset($this->other['quizid'])) {
throw new \coding_exception('The \'quizid\' value must be set in other.');
}
if (!isset($this->other['previousmaxmark'])) {
throw new \coding_exception('The \'previousmaxmark\' value must be set in other.');
}
if (!isset($this->other['newmaxmark'])) {
throw new \coding_exception('The \'newmaxmark\' value must be set in other.');
}
}
public static function get_objectid_mapping() {
return ['db' => 'quiz_slots', 'restore' => 'quiz_question_instance'];
}
public static function get_other_mapping() {
$othermapped = [];
$othermapped['quizid'] = ['db' => 'quiz', 'restore' => 'quiz'];
return $othermapped;
}
}
+109
View File
@@ -0,0 +1,109 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The mod_quiz slots moved event.
*
* @package mod_quiz
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_quiz\event;
/**
* The mod_quiz slot moved event class.
*
* @property-read array $other {
* Extra information about event.
*
* - int quizid: the id of the quiz.
* - int previousslotnumber: the previous slot number in quiz.
* - int afterslotnumber: the new slot number in quiz.
* - int page: the page of new slot position in quiz.
* }
*
* @package mod_quiz
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class slot_moved extends \core\event\base {
protected function init() {
$this->data['objecttable'] = 'quiz_slots';
$this->data['crud'] = 'u';
$this->data['edulevel'] = self::LEVEL_TEACHING;
}
public static function get_name() {
return get_string('eventslotmoved', 'mod_quiz');
}
public function get_description() {
if ($this->other['afterslotnumber'] == 0) {
$newposition = 'before the first slot';
} else {
$newposition = "after slot number '{$this->other['afterslotnumber']}'";
}
return "The user with id '$this->userid' has moved the slot with id '{$this->objectid}' " .
"and slot number '{$this->other['previousslotnumber']}' to the new position $newposition " .
"on page '{$this->other['page']}' belonging to the quiz with course module id '$this->contextinstanceid'.";
}
public function get_url() {
return new \moodle_url('/mod/quiz/edit.php', [
'cmid' => $this->contextinstanceid
]);
}
protected function validate_data() {
parent::validate_data();
if (!isset($this->objectid)) {
throw new \coding_exception('The \'objectid\' value must be set.');
}
if (!isset($this->contextinstanceid)) {
throw new \coding_exception('The \'contextinstanceid\' value must be set.');
}
if (!isset($this->other['quizid'])) {
throw new \coding_exception('The \'quizid\' value must be set in other.');
}
if (!isset($this->other['previousslotnumber'])) {
throw new \coding_exception('The \'previousslotnumber\' value must be set in other.');
}
if (!isset($this->other['afterslotnumber'])) {
throw new \coding_exception('The \'afterslotnumber\' value must be set in other.');
}
if (!isset($this->other['page'])) {
throw new \coding_exception('The \'page\' value must be set in other.');
}
}
public static function get_objectid_mapping() {
return ['db' => 'quiz_slots', 'restore' => 'quiz_question_instance'];
}
public static function get_other_mapping() {
$othermapped = [];
$othermapped['quizid'] = ['db' => 'quiz', 'restore' => 'quiz'];
return $othermapped;
}
}
@@ -0,0 +1,94 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The mod_quiz slots require previous updated event.
*
* @package mod_quiz
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_quiz\event;
/**
* The mod_quiz slot require previous updated event class.
*
* @property-read array $other {
* Extra information about event.
*
* - int quizid: the id of the quiz.
* - bool requireprevious: the slot's require previous value.
* }
*
* @package mod_quiz
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class slot_requireprevious_updated extends \core\event\base {
protected function init() {
$this->data['objecttable'] = 'quiz_slots';
$this->data['crud'] = 'u';
$this->data['edulevel'] = self::LEVEL_TEACHING;
}
public static function get_name() {
return get_string('eventslotrequirepreviousupdated', 'mod_quiz');
}
public function get_description() {
return "The user with id '$this->userid' updated the slot with id '{$this->objectid}' " .
"belonging to the quiz with course module id '$this->contextinstanceid'. " .
"Its require previous value was set to '{$this->other['requireprevious']}'.";
}
public function get_url() {
return new \moodle_url('/mod/quiz/edit.php', [
'cmid' => $this->contextinstanceid
]);
}
protected function validate_data() {
parent::validate_data();
if (!isset($this->objectid)) {
throw new \coding_exception('The \'objectid\' value must be set.');
}
if (!isset($this->contextinstanceid)) {
throw new \coding_exception('The \'contextinstanceid\' value must be set.');
}
if (!isset($this->other['quizid'])) {
throw new \coding_exception('The \'quizid\' value must be set in other.');
}
if (!isset($this->other['requireprevious'])) {
throw new \coding_exception('The \'requireprevious\' value must be set in other.');
}
}
public static function get_objectid_mapping() {
return ['db' => 'quiz_slots', 'restore' => 'quiz_question_instance'];
}
public static function get_other_mapping() {
$othermapped = [];
$othermapped['quizid'] = ['db' => 'quiz', 'restore' => 'quiz'];
return $othermapped;
}
}
@@ -0,0 +1,109 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The mod_quiz user override created event.
*
* @package mod_quiz
* @copyright 2014 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_quiz\event;
defined('MOODLE_INTERNAL') || die();
/**
* The mod_quiz user override created event class.
*
* @property-read array $other {
* Extra information about event.
*
* - int quizid: the id of the quiz.
* }
*
* @package mod_quiz
* @since Moodle 2.7
* @copyright 2014 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class user_override_created extends \core\event\base {
/**
* Init method.
*/
protected function init() {
$this->data['objecttable'] = 'quiz_overrides';
$this->data['crud'] = 'c';
$this->data['edulevel'] = self::LEVEL_TEACHING;
}
/**
* Returns localised general event name.
*
* @return string
*/
public static function get_name() {
return get_string('eventoverridecreated', 'mod_quiz');
}
/**
* Returns description of what happened.
*
* @return string
*/
public function get_description() {
return "The user with id '$this->userid' created the override with id '$this->objectid' for the quiz with " .
"course module id '$this->contextinstanceid' for the user with id '{$this->relateduserid}'.";
}
/**
* Returns relevant URL.
*
* @return \moodle_url
*/
public function get_url() {
return new \moodle_url('/mod/quiz/overrideedit.php', ['id' => $this->objectid]);
}
/**
* Custom validation.
*
* @throws \coding_exception
* @return void
*/
protected function validate_data() {
parent::validate_data();
if (!isset($this->relateduserid)) {
throw new \coding_exception('The \'relateduserid\' must be set.');
}
if (!isset($this->other['quizid'])) {
throw new \coding_exception('The \'quizid\' value must be set in other.');
}
}
public static function get_objectid_mapping() {
return ['db' => 'quiz_overrides', 'restore' => 'quiz_override'];
}
public static function get_other_mapping() {
$othermapped = [];
$othermapped['quizid'] = ['db' => 'quiz', 'restore' => 'quiz'];
return $othermapped;
}
}
@@ -0,0 +1,109 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The mod_quiz user override deleted event.
*
* @package mod_quiz
* @copyright 2014 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_quiz\event;
defined('MOODLE_INTERNAL') || die();
/**
* The mod_quiz user override deleted event class.
*
* @property-read array $other {
* Extra information about event.
*
* - int quizid: the id of the quiz.
* }
*
* @package mod_quiz
* @since Moodle 2.7
* @copyright 2014 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class user_override_deleted extends \core\event\base {
/**
* Init method.
*/
protected function init() {
$this->data['objecttable'] = 'quiz_overrides';
$this->data['crud'] = 'd';
$this->data['edulevel'] = self::LEVEL_TEACHING;
}
/**
* Returns localised general event name.
*
* @return string
*/
public static function get_name() {
return get_string('eventoverridedeleted', 'mod_quiz');
}
/**
* Returns description of what happened.
*
* @return string
*/
public function get_description() {
return "The user with id '$this->userid' deleted the override with id '$this->objectid' for the quiz with " .
"course module id '$this->contextinstanceid' for the user with id '{$this->relateduserid}'.";
}
/**
* Returns relevant URL.
*
* @return \moodle_url
*/
public function get_url() {
return new \moodle_url('/mod/quiz/overrides.php', ['cmid' => $this->contextinstanceid]);
}
/**
* Custom validation.
*
* @throws \coding_exception
* @return void
*/
protected function validate_data() {
parent::validate_data();
if (!isset($this->relateduserid)) {
throw new \coding_exception('The \'relateduserid\' must be set.');
}
if (!isset($this->other['quizid'])) {
throw new \coding_exception('The \'quizid\' value must be set in other.');
}
}
public static function get_objectid_mapping() {
return ['db' => 'quiz_overrides', 'restore' => 'quiz_override'];
}
public static function get_other_mapping() {
$othermapped = [];
$othermapped['quizid'] = ['db' => 'quiz', 'restore' => 'quiz'];
return $othermapped;
}
}
@@ -0,0 +1,110 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The mod_quiz user override updated event.
*
* @package mod_quiz
* @copyright 2014 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_quiz\event;
defined('MOODLE_INTERNAL') || die();
/**
* The mod_quiz user override updated event class.
*
* @property-read array $other {
* Extra information about event.
*
* - int quizid: the id of the quiz.
* }
*
* @package mod_quiz
* @since Moodle 2.7
* @copyright 2014 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class user_override_updated extends \core\event\base {
/**
* Init method.
*/
protected function init() {
$this->data['objecttable'] = 'quiz_overrides';
$this->data['crud'] = 'u';
$this->data['edulevel'] = self::LEVEL_TEACHING;
}
/**
* Returns localised general event name.
*
* @return string
*/
public static function get_name() {
return get_string('eventoverrideupdated', 'mod_quiz');
}
/**
* Returns description of what happened.
*
* @return string
*/
public function get_description() {
return "The user with id '$this->userid' updated the override with id '$this->objectid' for the quiz with " .
"course module id '$this->contextinstanceid' for the user with id '{$this->relateduserid}'.";
}
/**
* Returns relevant URL.
*
* @return \moodle_url
*/
public function get_url() {
return new \moodle_url('/mod/quiz/overrideedit.php', ['id' => $this->objectid]);
}
/**
* Custom validation.
*
* @throws \coding_exception
* @return void
*/
protected function validate_data() {
parent::validate_data();
if (!isset($this->relateduserid)) {
throw new \coding_exception('The \'relateduserid\' must be set.');
}
if (!isset($this->other['quizid'])) {
throw new \coding_exception('The \'quizid\' value must be set in other.');
}
}
public static function get_objectid_mapping() {
return ['db' => 'quiz_overrides', 'restore' => 'quiz_override'];
}
public static function get_other_mapping() {
$othermapped = [];
$othermapped['quizid'] = ['db' => 'quiz', 'restore' => 'quiz'];
return $othermapped;
}
}
File diff suppressed because it is too large Load Diff
+173
View File
@@ -0,0 +1,173 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\external;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . '/externallib.php');
require_once($CFG->dirroot . '/question/editlib.php');
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
use external_function_parameters;
use external_single_structure;
use external_value;
use external_api;
use mod_quiz\question\bank\filter\custom_category_condition;
use mod_quiz\quiz_settings;
use mod_quiz\structure;
/**
* Add random questions to a quiz.
*
* @package mod_quiz
* @copyright 2022 Catalyst IT Australia Pty Ltd
* @author Nathan Nguyen <nathannguyen@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class add_random_questions extends external_api {
/**
* Parameters for the web service function
*
* @return external_function_parameters
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters (
[
'cmid' => new external_value(PARAM_INT, 'The cmid of the quiz'),
'addonpage' => new external_value(PARAM_INT, 'The page where random questions will be added to'),
'randomcount' => new external_value(PARAM_INT, 'Number of random questions'),
'filtercondition' => new external_value(
PARAM_TEXT,
'(Optional) The filter condition used when adding random questions from an existing category.
Not required if adding random questions from a new category.',
VALUE_DEFAULT,
'',
),
'newcategory' => new external_value(
PARAM_TEXT,
'(Optional) The name of a new question category to create and use for the random questions.',
VALUE_DEFAULT,
'',
),
'parentcategory' => new external_value(
PARAM_TEXT,
'(Optional) The parent of the new question category, if creating one.',
VALUE_DEFAULT,
0,
),
]
);
}
/**
* Add random questions.
*
* @param int $cmid The cmid of the quiz
* @param int $addonpage The page where random questions will be added to
* @param int $randomcount Number of random questions
* @param string $filtercondition Filter condition
* @param string $newcategory add new category
* @param string $parentcategory parent category of new category
* @return array result
*/
public static function execute(
int $cmid,
int $addonpage,
int $randomcount,
string $filtercondition = '',
string $newcategory = '',
string $parentcategory = '',
): array {
[
'cmid' => $cmid,
'addonpage' => $addonpage,
'randomcount' => $randomcount,
'filtercondition' => $filtercondition,
'newcategory' => $newcategory,
'parentcategory' => $parentcategory,
] = self::validate_parameters(self::execute_parameters(), [
'cmid' => $cmid,
'addonpage' => $addonpage,
'randomcount' => $randomcount,
'filtercondition' => $filtercondition,
'newcategory' => $newcategory,
'parentcategory' => $parentcategory,
]);
// Validate context.
$thiscontext = \context_module::instance($cmid);
self::validate_context($thiscontext);
require_capability('mod/quiz:manage', $thiscontext);
// If filtercondition is not empty, decode it. Otherwise, set it to empty array.
$filtercondition = !empty($filtercondition) ? json_decode($filtercondition, true) : [];
// Create new category.
if (!empty($newcategory)) {
$contexts = new \core_question\local\bank\question_edit_contexts($thiscontext);
$defaultcategoryobj = question_make_default_categories($contexts->all());
$defaultcategory = $defaultcategoryobj->id . ',' . $defaultcategoryobj->contextid;
$qcobject = new \qbank_managecategories\question_category_object(
null,
new \moodle_url('/'),
$contexts->having_one_edit_tab_cap('categories'),
$defaultcategoryobj->id,
$defaultcategory,
null,
$contexts->having_cap('moodle/question:add'));
$categoryid = $qcobject->add_category($parentcategory, $newcategory, '', true);
$filter = [
'category' => [
'jointype' => custom_category_condition::JOINTYPE_DEFAULT,
'values' => [$categoryid],
'filteroptions' => ['includesubcategories' => false],
]
];
// Generate default filter condition for the random question to be added in the new category.
$filtercondition = [
'qpage' => 0,
'cat' => "{$categoryid},{$thiscontext->id}",
'qperpage' => DEFAULT_QUESTIONS_PER_PAGE,
'tabname' => 'questions',
'sortdata' => [],
'filter' => $filter,
];
}
// Add random question to the quiz.
[$quiz, ] = get_module_from_cmid($cmid);
$settings = quiz_settings::create_for_cmid($cmid);
$structure = structure::create_for_quiz($settings);
$structure->add_random_questions($addonpage, $randomcount, $filtercondition);
quiz_delete_previews($quiz);
quiz_settings::create($quiz->id)->get_grade_calculator()->recompute_quiz_sumgrades();
return ['message' => get_string('addarandomquestion_success', 'mod_quiz')];
}
/**
* Returns description of method result value.
*
* @return external_value
*/
public static function execute_returns() {
return new external_single_structure([
'message' => new external_value(PARAM_TEXT, 'Message', VALUE_OPTIONAL)
]);
}
}
@@ -0,0 +1,122 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\external;
use coding_exception;
use core_external\external_api;
use core_external\external_description;
use core_external\external_function_parameters;
use core_external\external_multiple_structure;
use core_external\external_single_structure;
use core_external\external_value;
use mod_quiz\quiz_attempt;
use mod_quiz\quiz_settings;
use moodle_exception;
use stdClass;
/**
* For a quiz with no grade items yet, create a grade item for each section.
*
* And, assign the questions in each section to the corresponding grade item.
*
* The user must have the 'mod/quiz:manage' capability for the quiz.
*
* @package mod_quiz
* @copyright 2024 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class create_grade_item_per_section extends external_api {
/**
* Declare the method parameters.
*
* @return external_function_parameters
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters([
'quizid' => new external_value(PARAM_INT, 'The quiz to update slots for.'),
]);
}
/**
* For a quiz with no grade items yet, create a grade item for each section.
*
* And, assign the questions in each section to the corresponding grade item.
*
* The user must have the 'mod/quiz:manage' capability for the quiz.
*
* @param int $quizid the id of the quiz to setup grade items for.
*/
public static function execute(int $quizid): void {
global $DB;
[
'quizid' => $quizid,
] = self::validate_parameters(self::execute_parameters(), [
'quizid' => $quizid,
]);
// Check the request is valid.
$quizobj = quiz_settings::create($quizid);
require_capability('mod/quiz:manage', $quizobj->get_context());
self::validate_context($quizobj->get_context());
$structure = $quizobj->get_structure();
if ($structure->get_grade_items()) {
throw new coding_exception('Cannot use create_grade_item_per_section for a quiz ' .
'that already has grade items.');
}
$transaction = $DB->start_delegated_transaction();
$gradeitemsids = [];
foreach ($structure->get_sections() as $section) {
// Only create a grade item for sections that contain at least one real question (not description).
$hasrealquestion = false;
foreach ($structure->get_slots_in_section($section->id) as $slot) {
$hasrealquestion = $hasrealquestion || $structure->is_real_question($slot);
}
if (!$hasrealquestion) {
continue;
}
// Grade item required. Create it.
$gradeitem = new stdClass();
$gradeitem->quizid = $quizid;
$gradeitem->name = $section->heading;
$structure->create_grade_item($gradeitem);
$gradeitemsids[$section->id] = $gradeitem->id;
}
foreach ($structure->get_slots() as $slot) {
if ($structure->is_real_question($slot->slot)) {
$structure->update_slot_grade_item($slot, $gradeitemsids[$slot->section->id]);
}
}
$transaction->allow_commit();
}
/**
* Define the webservice response.
*
* @return external_description|null always null.
*/
public static function execute_returns(): ?external_description {
return null;
}
}
+103
View File
@@ -0,0 +1,103 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\external;
use core_external\external_api;
use core_external\external_description;
use core_external\external_function_parameters;
use core_external\external_multiple_structure;
use core_external\external_single_structure;
use core_external\external_value;
use mod_quiz\quiz_attempt;
use mod_quiz\quiz_settings;
use moodle_exception;
/**
* Web service method to create quiz grade items for a quiz.
*
* The user must have the 'mod/quiz:manage' capability for the quiz.
*
* The new grade items are added in order, after all the existing ones.
*
* @package mod_quiz
* @copyright 2023 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class create_grade_items extends external_api {
/**
* Declare the method parameters.
*
* @return external_function_parameters
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters([
'quizid' => new external_value(PARAM_INT, 'The quiz to update slots for.'),
'quizgradeitems' => new external_multiple_structure(
new external_single_structure([
'name' => new external_value(PARAM_TEXT,
'The name for the grade item to create. If empty string, a sensible default is used.'),
])
),
]);
}
/**
* Add new grade items to this quiz.
*
* New items are added in order, at the end of the sort order.
*
* @param int $quizid the id of the quiz to add the grade items to.
* @param array $gradeitems list of grade items to create. (Each one must have property name.)
*/
public static function execute(int $quizid, array $gradeitems): void {
global $DB;
[
'quizid' => $quizid,
'quizgradeitems' => $gradeitems,
] = self::validate_parameters(self::execute_parameters(), [
'quizid' => $quizid,
'quizgradeitems' => $gradeitems,
]);
// Check the request is valid.
$quizobj = quiz_settings::create($quizid);
require_capability('mod/quiz:manage', $quizobj->get_context());
self::validate_context($quizobj->get_context());
$transaction = $DB->start_delegated_transaction();
$structure = $quizobj->get_structure();
foreach ($gradeitems as $gradeitemdata) {
$gradeitem = (object) $gradeitemdata;
$gradeitem->quizid = $quizid;
$structure->create_grade_item($gradeitem);
}
$transaction->allow_commit();
}
/**
* Define the webservice response.
*
* @return external_description|null always null.
*/
public static function execute_returns(): ?external_description {
return null;
}
}
+99
View File
@@ -0,0 +1,99 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\external;
use core_external\external_api;
use core_external\external_description;
use core_external\external_function_parameters;
use core_external\external_multiple_structure;
use core_external\external_single_structure;
use core_external\external_value;
use mod_quiz\quiz_attempt;
use mod_quiz\quiz_settings;
use moodle_exception;
/**
* Web service method to delete quiz grade items.
*
* The user must have the 'mod/quiz:manage' capability for the quiz.
*
* The grade items to be deleted must all belong to the same quiz,
* and must not be referred to by any slot.
*
* @package mod_quiz
* @copyright 2023 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class delete_grade_items extends external_api {
/**
* Declare the method parameters.
*
* @return external_function_parameters
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters([
'quizid' => new external_value(PARAM_INT, 'The quiz to update slots for.'),
'quizgradeitems' => new external_multiple_structure(
new external_single_structure([
'id' => new external_value(PARAM_INT, 'id of the quiz grade item'),
])
),
]);
}
/**
* Delete quiz grade items, if they are unused.
*
* @param int $quizid the id of the quiz from which to dlete grade items.
* @param array $gradeitems list of grade items to delete. (They must belong to this quiz.)
*/
public static function execute(int $quizid, array $gradeitems): void {
global $DB;
[
'quizid' => $quizid,
'quizgradeitems' => $gradeitems,
] = self::validate_parameters(self::execute_parameters(), [
'quizid' => $quizid,
'quizgradeitems' => $gradeitems,
]);
// Check the request is valid.
$quizobj = quiz_settings::create($quizid);
require_capability('mod/quiz:manage', $quizobj->get_context());
self::validate_context($quizobj->get_context());
$transaction = $DB->start_delegated_transaction();
$structure = $quizobj->get_structure();
foreach ($gradeitems as $gradeitemdata) {
$structure->delete_grade_item($gradeitemdata['id']);
}
$transaction->allow_commit();
}
/**
* Define the webservice response.
*
* @return external_description|null always null.
*/
public static function execute_returns(): ?external_description {
return null;
}
}
+77
View File
@@ -0,0 +1,77 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\external;
use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_multiple_structure;
use core_external\external_single_structure;
use core_external\external_value;
use mod_quiz\quiz_settings;
/**
* Webservice for deleting quiz overrides.
*
* @package mod_quiz
* @copyright 2024 Matthew Hilton <matthewhilton@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class delete_overrides extends external_api {
/**
* Defines parameters
*
* @return external_function_parameters
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters([
// This must be nested in a single structure, because the ids structure does not play nicely at the top level.
'data' => new external_single_structure([
'quizid' => new external_value(PARAM_INT, "ID of quiz to delete overrides in"),
'ids' => new external_multiple_structure(new external_value(PARAM_INT, 'ID of override to delete')),
]),
]);
}
/**
* Executes webservice function, deleting given overrides.
*
* @param array $params array of override parameters
* @return array with ids key, which contains the ids of the overrides successfully deleted.
*/
public static function execute($params): array {
$params = self::validate_parameters(self::execute_parameters(), ['data' => $params])['data'];
$quizsettings = quiz_settings::create($params['quizid']);
$manager = $quizsettings->get_override_manager();
self::validate_context($manager->context);
$manager->require_manage_capability();
$manager->delete_overrides_by_id($params['ids']);
return ['ids' => $params['ids']];
}
/**
* Defines return type
*
* @return external_single_structure
*/
public static function execute_returns(): external_single_structure {
return new external_single_structure([
'ids' => new external_multiple_structure(new external_value(PARAM_INT, 'ID of deleted override')),
]);
}
}
@@ -0,0 +1,89 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\external;
use core_external\external_api;
use core_external\external_description;
use core_external\external_function_parameters;
use core_external\external_value;
use Exception;
use html_writer;
use mod_quiz\output\edit_grading_page;
use mod_quiz\quiz_attempt;
use mod_quiz\quiz_settings;
use moodle_exception;
/**
* Web service to get the data required o re-render the Quiz grading setup page.
*
* The use must have the 'mod/quiz:manage' capability.
*
* @package mod_quiz
* @copyright 2023 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class get_edit_grading_page_data extends external_api {
/**
* Declare the method parameters.
*
* @return external_function_parameters
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters([
'quizid' => new external_value(PARAM_INT, 'The quiz for which to return the data.'),
]);
}
/**
* Check a quiz attempt state, and return a confirmation message method implementation.
*
* @param int $quizid the quiz for which to return the data.
* @return string a suitable confirmation message (HTML), if the attempt is suitable to be reopened.
* @throws Exception an appropriate exception if the attempt cannot be reopened now.
*/
public static function execute(int $quizid): string {
global $PAGE;
[
'quizid' => $quizid,
] = self::validate_parameters(self::execute_parameters(), [
'quizid' => $quizid,
]);
// Check the request is valid.
$quizobj = quiz_settings::create($quizid);
require_capability('mod/quiz:manage', $quizobj->get_context());
self::validate_context($quizobj->get_context());
// Set dummy URL to stop debugging in the renderer (TODO: remove as part of MDL-76728).
$PAGE->set_url('/');
$structure = $quizobj->get_structure();
$editpage = new edit_grading_page($structure);
return json_encode($editpage->export_for_template($PAGE->get_renderer('core')));
}
/**
* Define the webservice response.
*
* @return external_description
*/
public static function execute_returns(): external_description {
return new external_value(PARAM_RAW, 'JSON-encoded data required to render the mod_quiz/edit_grading_page template.');
}
}
+94
View File
@@ -0,0 +1,94 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\external;
use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_multiple_structure;
use core_external\external_single_structure;
use core_external\external_value;
use mod_quiz\quiz_settings;
/**
* Webservice for searching overrides.
*
* @package mod_quiz
* @copyright 2024 Matthew Hilton <matthewhilton@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class get_overrides extends external_api {
/**
* Defines parameters for getting quiz overrides.
*
* @return external_function_parameters
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters([
'quizid' => new external_value(PARAM_INT, 'ID of quiz to get overrides for'),
]);
}
/**
* Executes webservice function, returning quiz overrides.
*
* @param int $quizid
* @return array with overrides key which contains the overrides for the given quiz.
*/
public static function execute($quizid): array {
$params = self::validate_parameters(self::execute_parameters(), ['quizid' => $quizid]);
$quizsettings = quiz_settings::create($params['quizid']);
$manager = $quizsettings->get_override_manager();
self::validate_context($manager->context);
$manager->require_read_capability();
// Filter for those overrides user can access.
$overrides = array_filter(
$manager->get_all_overrides(),
fn(\stdClass $override) => $manager->can_view_override(
$override,
$quizsettings->get_course(),
$quizsettings->get_cm(),
),
);
return ['overrides' => $overrides];
}
/**
* Defines return type
*
* @return external_single_structure
*/
public static function execute_returns(): external_single_structure {
$overridedatastructure = new external_single_structure([
'id' => new external_value(PARAM_INT, 'Override ID'),
'quiz' => new external_value(PARAM_INT, 'Quiz ID'),
'userid' => new external_value(PARAM_INT, 'User ID', VALUE_DEFAULT, null),
'groupid' => new external_value(PARAM_INT, 'Group ID', VALUE_DEFAULT, null),
'timeopen' => new external_value(PARAM_INT, 'Override time open value', VALUE_DEFAULT, null),
'timeclose' => new external_value(PARAM_INT, 'Override time close value', VALUE_DEFAULT, null),
'timelimit' => new external_value(PARAM_INT, 'Override time limit value', VALUE_DEFAULT, null),
'attempts' => new external_value(PARAM_INT, 'Override attempts value', VALUE_DEFAULT, null),
'password' => new external_value(PARAM_TEXT, 'Override password', VALUE_DEFAULT, null),
]);
return new external_single_structure([
'overrides' => new external_multiple_structure($overridedatastructure),
]);
}
}
@@ -0,0 +1,98 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\external;
use core_external\external_api;
use core_external\external_description;
use core_external\external_function_parameters;
use core_external\external_value;
use Exception;
use html_writer;
use mod_quiz\quiz_attempt;
use moodle_exception;
/**
* Web service to check a quiz attempt state, and return a confirmation message if it can be reopened now.
*
* The use must have the 'mod/quiz:reopenattempts' capability and the attempt
* must (at least for now) be in the 'Never submitted' state (quiz_attempt::ABANDONED).
*
* @package mod_quiz
* @copyright 2023 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class get_reopen_attempt_confirmation extends external_api {
/**
* Declare the method parameters.
*
* @return external_function_parameters
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters([
'attemptid' => new external_value(PARAM_INT, 'The id of the attempt to reopen'),
]);
}
/**
* Check a quiz attempt state, and return a confirmation message method implementation.
*
* @param int $attemptid the id of the attempt to reopen.
* @return string a suitable confirmation message (HTML), if the attempt is suitable to be reopened.
* @throws Exception an appropriate exception if the attempt cannot be reopened now.
*/
public static function execute(int $attemptid): string {
global $DB;
['attemptid' => $attemptid] = self::validate_parameters(
self::execute_parameters(), ['attemptid' => $attemptid]);
// Check the request is valid.
$attemptobj = quiz_attempt::create($attemptid);
require_capability('mod/quiz:reopenattempts', $attemptobj->get_context());
self::validate_context($attemptobj->get_context());
if ($attemptobj->get_state() != quiz_attempt::ABANDONED) {
throw new moodle_exception('reopenattemptwrongstate', 'quiz', '',
['attemptid' => $attemptid, 'state' => quiz_attempt_state_name($attemptobj->get_state())]);
}
// Work out what the affect or re-opening will be.
$timestamp = time();
$timeclose = $attemptobj->get_access_manager(time())->get_end_time($attemptobj->get_attempt());
if ($timeclose && $timestamp > $timeclose) {
$expectedoutcome = get_string('reopenedattemptwillbesubmitted', 'quiz');
} else if ($timeclose) {
$expectedoutcome = get_string('reopenedattemptwillbeinprogressuntil', 'quiz', userdate($timeclose));
} else {
$expectedoutcome = get_string('reopenedattemptwillbeinprogress', 'quiz');
}
// Return the required message.
$user = $DB->get_record('user', ['id' => $attemptobj->get_userid()], '*', MUST_EXIST);
return html_writer::tag('p', get_string('reopenattemptareyousuremessage', 'quiz',
['attemptnumber' => $attemptobj->get_attempt_number(), 'attemptuser' => s(fullname($user))])) .
html_writer::tag('p', $expectedoutcome);
}
/**
* Define the webservice response.
*
* @return external_description
*/
public static function execute_returns(): external_description {
return new external_value(PARAM_RAW, 'Confirmation to show the user before the attempt is reopened.');
}
}
+79
View File
@@ -0,0 +1,79 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\external;
use core_external\external_api;
use core_external\external_description;
use core_external\external_function_parameters;
use core_external\external_value;
use mod_quiz\quiz_attempt;
use moodle_exception;
/**
* Web service method for re-opening a quiz attempt.
*
* The use must have the 'mod/quiz:reopenattempts' capability and the attempt
* must (at least for now) be in the 'Never submitted' state (quiz_attempt::ABANDONED).
*
* @package mod_quiz
* @copyright 2023 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class reopen_attempt extends external_api {
/**
* Declare the method parameters.
*
* @return external_function_parameters
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters([
'attemptid' => new external_value(PARAM_INT, 'The id of the attempt to reopen'),
]);
}
/**
* Re-opening a submitted attempt method implementation.
*
* @param int $attemptid the id of the attempt to reopen.
*/
public static function execute(int $attemptid): void {
['attemptid' => $attemptid] = self::validate_parameters(
self::execute_parameters(), ['attemptid' => $attemptid]);
// Check the request is valid.
$attemptobj = quiz_attempt::create($attemptid);
require_capability('mod/quiz:reopenattempts', $attemptobj->get_context());
self::validate_context($attemptobj->get_context());
if ($attemptobj->get_state() != quiz_attempt::ABANDONED) {
throw new moodle_exception('reopenattemptwrongstate', 'quiz', '',
['attemptid' => $attemptid, 'state' => quiz_attempt_state_name($attemptobj->get_state())]);
}
// Re-open the attempt.
$attemptobj->process_reopen_abandoned(time());
}
/**
* Define the webservice response.
*
* @return external_description|null always null.
*/
public static function execute_returns(): ?external_description {
return null;
}
}
+100
View File
@@ -0,0 +1,100 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\external;
use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_multiple_structure;
use core_external\external_single_structure;
use core_external\external_value;
use mod_quiz\quiz_settings;
/**
* Webservice for upserting quiz overrides.
*
* @package mod_quiz
* @copyright 2024 Matthew Hilton <matthewhilton@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class save_overrides extends external_api {
/**
* Defines parameters
*
* @return external_function_parameters
*/
public static function execute_parameters(): external_function_parameters {
$overridestructure = new external_single_structure([
'id' => new external_value(PARAM_INT, 'ID of existing override (if updating)', VALUE_DEFAULT, null),
'groupid' => new external_value(PARAM_INT, 'ID of group', VALUE_DEFAULT, null),
'userid' => new external_value(PARAM_INT, 'ID of user', VALUE_DEFAULT, null),
'timeopen' => new external_value(PARAM_INT, 'Quiz override opening timestamp', VALUE_DEFAULT, null),
'timeclose' => new external_value(PARAM_INT, 'Quiz override closing timestamp', VALUE_OPTIONAL, null),
'timelimit' => new external_value(PARAM_INT, 'Quiz override time limit', VALUE_DEFAULT, null),
'attempts' => new external_value(PARAM_INT, 'Quiz override attempt count', VALUE_DEFAULT, null),
'password' => new external_value(PARAM_TEXT, 'Quiz override password', VALUE_DEFAULT, null),
]);
return new external_function_parameters([
// This must be nested in a single structure, because the overrides structure does not play nicely at the top level.
'data' => new external_single_structure([
'quizid' => new external_value(PARAM_INT, 'ID of quiz to save overrides to'),
'overrides' => new external_multiple_structure($overridestructure),
]),
]);
}
/**
* Executes webservice function, saving the requested overrides.
*
* @param array $data array with quizid key and overrides key containing list of overrides to save.
* @return array with ids key which contains ids of created/updated overrides.
*/
public static function execute($data): array {
$params = self::validate_parameters(self::execute_parameters(), ['data' => $data])['data'];
$quizsettings = quiz_settings::create($params['quizid']);
$manager = $quizsettings->get_override_manager();
self::validate_context($manager->context);
$manager->require_manage_capability();
// Filter for those overrides user can access.
$overrides = array_filter(
$params['overrides'],
fn(array $override) => $manager->can_view_override(
(object) $override,
$quizsettings->get_course(),
$quizsettings->get_cm(),
),
);
// Iterate over and save all overrides.
$ids = array_map(fn($override) => $manager->save_override($override), $overrides);
return ['ids' => $ids];
}
/**
* Defines return type
*
* @return external_single_structure
*/
public static function execute_returns(): external_single_structure {
return new external_single_structure([
'ids' => new external_multiple_structure(new external_value(PARAM_INT, 'ID of created/updated override')),
]);
}
}
+101
View File
@@ -0,0 +1,101 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\external;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/question/engine/lib.php');
require_once($CFG->dirroot . '/question/engine/datalib.php');
require_once($CFG->libdir . '/questionlib.php');
use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_single_structure;
use core_external\external_value;
use stdClass;
/**
* External api for changing the question version in the quiz.
*
* @package mod_quiz
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class submit_question_version extends external_api {
/**
* Parameters for the submit_question_version.
*
* @return external_function_parameters
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters([
'slotid' => new external_value(PARAM_INT, ''),
'newversion' => new external_value(PARAM_INT, '')
]);
}
/**
* Set the questions slot parameters to display the question template.
*
* @param int $slotid Slot id to display.
* @param int $newversion the version to set. 0 means 'always latest'.
* @return array
*/
public static function execute(int $slotid, int $newversion): array {
global $DB;
$params = [
'slotid' => $slotid,
'newversion' => $newversion
];
$params = self::validate_parameters(self::execute_parameters(), $params);
$response = [];
// Get the required data.
$referencedata = $DB->get_record('question_references',
['itemid' => $params['slotid'], 'component' => 'mod_quiz', 'questionarea' => 'slot']);
$slotdata = $DB->get_record('quiz_slots', ['id' => $slotid]);
// Capability check.
[, $cm] = get_course_and_cm_from_instance($slotdata->quizid, 'quiz');
$context = \context_module::instance($cm->id);
self::validate_context($context);
require_capability('mod/quiz:manage', $context);
$reference = new stdClass();
$reference->id = $referencedata->id;
if ($params['newversion'] === 0) {
$reference->version = null;
} else {
$reference->version = $params['newversion'];
}
$response['result'] = $DB->update_record('question_references', $reference);
return $response;
}
/**
* Define the webservice response.
*
* @return \core_external\external_description
*/
public static function execute_returns() {
return new external_single_structure([
'result' => new external_value(PARAM_BOOL, '')
]);
}
}
+104
View File
@@ -0,0 +1,104 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\external;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . '/externallib.php');
require_once($CFG->dirroot . '/question/editlib.php');
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
use external_function_parameters;
use external_single_structure;
use external_value;
use external_api;
/**
* Update the filter condition for a random question.
*
* @package mod_quiz
* @copyright 2022 Catalyst IT Australia Pty Ltd
* @author Nathan Nguyen <nathannguyen@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class update_filter_condition extends external_api {
/**
* Parameters for the web service function
*
* @return external_function_parameters
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters ([
'cmid' => new external_value(PARAM_INT, 'The cmid of the quiz'),
'slotid' => new external_value(PARAM_INT, 'The quiz slot ID for the random question.'),
'filtercondition' => new external_value(PARAM_TEXT, 'Filter condition'),
]);
}
/**
* Add random questions.
*
* @param int $cmid course module id
* @param int $slotid The quiz slot id
* @param string $filtercondition
* @return array result
*/
public static function execute(
int $cmid,
int $slotid,
string $filtercondition,
): array {
global $DB;
[
'cmid' => $cmid,
'slotid' => $slotid,
'filtercondition' => $filtercondition,
] = self::validate_parameters(self::execute_parameters(), [
'cmid' => $cmid,
'slotid' => $slotid,
'filtercondition' => $filtercondition,
]);
// Validate context.
$thiscontext = \context_module::instance($cmid);
self::validate_context($thiscontext);
require_capability('mod/quiz:manage', $thiscontext);
// Update filter condition.
$setparams = [
'itemid' => $slotid,
'questionarea' => 'slot',
'component' => 'mod_quiz',
];
$DB->set_field('question_set_references', 'filtercondition', $filtercondition, $setparams);
return ['message' => get_string('updatefilterconditon_success', 'mod_quiz')];
}
/**
* Returns description of method result value.
*
* @return external_value
*/
public static function execute_returns() {
return new external_single_structure([
'message' => new external_value(PARAM_TEXT, 'Message', VALUE_OPTIONAL)
]);
}
}
+105
View File
@@ -0,0 +1,105 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\external;
use core_external\external_api;
use core_external\external_description;
use core_external\external_function_parameters;
use core_external\external_multiple_structure;
use core_external\external_single_structure;
use core_external\external_value;
use mod_quiz\quiz_attempt;
use mod_quiz\quiz_settings;
use moodle_exception;
/**
* Web service method to update the properties of quiz grade items.
*
* The user must have the 'mod/quiz:manage' capability for the quiz.
*
* All the properties that can be set are optional. Only the ones passed are changed.
*
* @package mod_quiz
* @copyright 2023 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class update_grade_items extends external_api {
/**
* Declare the method parameters.
*
* @return external_function_parameters
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters([
'quizid' => new external_value(PARAM_INT, 'The quiz to update slots for.'),
'quizgradeitems' => new external_multiple_structure(
new external_single_structure([
'id' => new external_value(PARAM_INT, 'id of the quiz grade item'),
'name' => new external_value(
PARAM_TEXT,
'If passed, new name to set. Null, or not specified, to leave unchanged.',
VALUE_OPTIONAL
),
])
),
]);
}
/**
* Update grade items to this quiz.
*
* @param int $quizid the id of the quiz from which to dlete grade items.
* @param array $gradeitems list of grade items to update. Must have properties id and name
*/
public static function execute(int $quizid, array $gradeitems): void {
global $DB;
[
'quizid' => $quizid,
'quizgradeitems' => $gradeitems,
] = self::validate_parameters(self::execute_parameters(), [
'quizid' => $quizid,
'quizgradeitems' => $gradeitems,
]);
// Check the request is valid.
$quizobj = quiz_settings::create($quizid);
require_capability('mod/quiz:manage', $quizobj->get_context());
self::validate_context($quizobj->get_context());
$transaction = $DB->start_delegated_transaction();
$structure = $quizobj->get_structure();
foreach ($gradeitems as $gradeitemdata) {
if ($gradeitemdata['name'] !== null) {
$structure->update_grade_item((object) $gradeitemdata);
}
}
$transaction->allow_commit();
}
/**
* Define the webservice response.
*
* @return external_description|null always null.
*/
public static function execute_returns(): ?external_description {
return null;
}
}
+148
View File
@@ -0,0 +1,148 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\external;
use core_external\external_api;
use core_external\external_description;
use core_external\external_function_parameters;
use core_external\external_multiple_structure;
use core_external\external_single_structure;
use core_external\external_value;
use mod_quiz\quiz_attempt;
use mod_quiz\quiz_settings;
use moodle_exception;
/**
* Web service method to update the properties of one or more slots in a quiz.
*
* The user must have the 'mod/quiz:manage' capability for the quiz.
*
* All the properties that can be set are optional. Only the ones passed are changed.
* The full properties of the updated slot are returned.
*
* @package mod_quiz
* @copyright 2023 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class update_slots extends external_api {
/**
* Declare the method parameters.
*
* @return external_function_parameters
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters([
'quizid' => new external_value(PARAM_INT, 'The quiz to update slots for.'),
'slots' => new external_multiple_structure(
new external_single_structure([
'id' => new external_value(PARAM_INT, 'id of the slot'),
'displaynumber' => new external_value(
PARAM_TEXT,
'If passed, new customised question number. Empty string to clear customisation. ' .
'Null, or not specified, to leave unchanged.',
VALUE_OPTIONAL
),
'requireprevious' => new external_value(
PARAM_BOOL,
'Whether to make this slot dependent on the previous one. Null, or not specified, to leave unchanged.',
VALUE_OPTIONAL
),
'maxmark' => new external_value(
PARAM_FLOAT,
'Mark that this questions is out of. Null, or not specified, to leave unchanged.',
VALUE_OPTIONAL
),
'quizgradeitemid' => new external_value(
PARAM_INT,
'For quizzes with multiple grades, which grade this slot contributes to (quiz_grade_id). ' .
'0 to set to nothing. Null, or not specified, to leave unchanged.',
VALUE_OPTIONAL
),
])
),
]);
}
/**
* Update the properties of one or more slots in a quiz.
*
* @param int $quizid the id of the quiz to update slots in.
* @param array $slotsdata list of slots update. Must have properties id, any any other properties to change.
*/
public static function execute(int $quizid, array $slotsdata): void {
global $DB;
[
'quizid' => $quizid,
'slots' => $slotsdata,
] = self::validate_parameters(self::execute_parameters(), [
'quizid' => $quizid,
'slots' => $slotsdata,
]);
// Check the request is valid.
$quizobj = quiz_settings::create($quizid);
require_capability('mod/quiz:manage', $quizobj->get_context());
self::validate_context($quizobj->get_context());
$transaction = $DB->start_delegated_transaction();
$structure = $quizobj->get_structure();
$gradingsetupchanged = false;
foreach ($slotsdata as $slotdata) {
// Check this slot exists in this quiz.
$slot = $structure->get_slot_by_id($slotdata['id']);
if (isset($slotdata['displaynumber'])) {
$structure->update_slot_display_number($slot->id, $slotdata['displaynumber']);
}
if (isset($slotdata['requireprevious'])) {
$structure->update_question_dependency($slot->id, $slotdata['requireprevious']);
}
if (isset($slotdata['maxmark'])) {
$gradingsetupchanged = $structure->update_slot_maxmark($slot, $slotdata['maxmark'])
|| $gradingsetupchanged;
}
if (array_key_exists('quizgradeitemid', $slotdata)) {
$gradingsetupchanged = $structure->update_slot_grade_item($slot, $slotdata['quizgradeitemid'])
|| $gradingsetupchanged;
}
}
// If the grade setup has canged, recompute things.
if ($gradingsetupchanged) {
$gradecalculator = $quizobj->get_grade_calculator();
quiz_delete_previews($quizobj->get_quiz());
$gradecalculator->recompute_quiz_sumgrades();
$gradecalculator->recompute_all_attempt_sumgrades();
$gradecalculator->recompute_all_final_grades();
quiz_update_grades($quizobj->get_quiz(), 0, true);
}
$transaction->allow_commit();
}
/**
* Define the webservice response.
*
* @return external_description|null always null.
*/
public static function execute_returns(): ?external_description {
return null;
}
}
+172
View File
@@ -0,0 +1,172 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\form;
use core\check\performance\debugging;
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
* @deprecated Moodle 4.3 MDL-72321. This form is new generated in a modal with mod_quiz/add_random_question_form.mustache
* @todo Final deprecation in Moodle 4.7 MDL-78091
*/
class add_random_form extends moodleform {
/**
* Deprecated.
*
* @return void
* @deprecated Moodle 4.3 MDL-72321
* @todo Final deprecation in Moodle 4.7 MDL-78091
*/
protected function definition() {
debugging(
'add_random_form is deprecated. Please use mod_quiz/add_random_question_form.mustache instead.',
DEBUG_DEVELOPER
);
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'),
['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 = [];
$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 = [
'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'),
['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
]);
}
/**
* Deprecated.
*
* @param array $fromform
* @param array $files
* @return array
* @deprecated Moodle 4.3 MDL-72321
* @todo Final deprecation in Moodle 4.7 MDL-78091
*/
public function validation($fromform, $files) {
debugging(
'add_random_form is deprecated. Please use mod_quiz/add_random_question_form.mustache instead.',
DEBUG_DEVELOPER
);
$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 = [];
for ($i = 1; $i <= min(100, $maxrand); $i++) {
$randomcount[$i] = $i;
}
return $randomcount;
}
}
@@ -0,0 +1,294 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\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;
/** @var int overrideid, if provided. */
protected int $overrideid;
/**
* 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;
$this->overrideid = $override->id ?? 0;
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 = [
$this->groupid => format_string(groups_get_group_name($this->groupid), true, ['context' => $this->context]),
];
$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) {
if ($group->visibility != GROUPS_VISIBILITY_NONE) {
$groupchoices[$group->id] = format_string($group->name, true, ['context' => $this->context]);
}
}
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 DISTINCT $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;
}
/**
* Validate the data from the form.
*
* @param array $data form data
* @param array $files form files
* @return array An array of error messages, where the key is is the mform element name and the value is the error.
*/
public function validation($data, $files): array {
$errors = parent::validation($data, $files);
$data['id'] = $this->overrideid;
$data['quiz'] = $this->quiz->id;
$manager = new \mod_quiz\local\override_manager($this->quiz, $this->context);
$errors = array_merge($errors, $manager->validate_data($data));
// Any 'general' errors we merge with the group/user selector element.
if (!empty($errors['general'])) {
if ($this->groupmode) {
$errors['groupid'] = $errors['groupid'] ?? "" . $errors['general'];
} else {
$errors['userid'] = $errors['userid'] ?? "" . $errors['general'];
}
}
return $errors;
}
}
@@ -0,0 +1,64 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\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(['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']));
}
}
@@ -0,0 +1,100 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Defines the editing form for random questions.
*
* @package mod_quiz
* @copyright 2018 Shamim Rezaie <shamim@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_quiz\form;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot.'/lib/formslib.php');
/**
* Class randomquestion_form
*
* @package mod_quiz
* @copyright 2018 Shamim Rezaie <shamim@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class randomquestion_form extends \moodleform {
/**
* Form definiton.
*/
public function definition() {
$mform = $this->_form;
$contexts = $this->_customdata['contexts'];
$usablecontexts = $contexts->having_cap('moodle/question:useall');
// Standard fields at the start of the form.
$mform->addElement('header', 'generalheader', get_string("general", 'form'));
$mform->addElement('questioncategory', 'category', get_string('category', 'question'),
['contexts' => $usablecontexts, 'top' => true]);
$mform->addElement('advcheckbox', 'includesubcategories', get_string('recurse', 'quiz'), null, null, [0, 1]);
$tops = question_get_top_categories_for_contexts(array_column($contexts->all(), 'id'));
$mform->hideIf('includesubcategories', 'category', 'in', $tops);
$tags = \core_tag_tag::get_tags_by_area_in_contexts('core_question', 'question', $usablecontexts);
$tagstrings = [];
foreach ($tags as $tag) {
$tagstrings["{$tag->id},{$tag->name}"] = $tag->name;
}
$options = [
'multiple' => true,
'noselectionstring' => get_string('anytags', 'quiz'),
];
$mform->addElement('autocomplete', 'fromtags', get_string('randomquestiontags', 'mod_quiz'), $tagstrings, $options);
$mform->addHelpButton('fromtags', 'randomquestiontags', 'mod_quiz');
$mform->addElement('hidden', 'slotid');
$mform->setType('slotid', PARAM_INT);
$mform->addElement('hidden', 'returnurl');
$mform->setType('returnurl', PARAM_LOCALURL);
$buttonarray = [];
$buttonarray[] = $mform->createElement('submit', 'submitbutton', get_string('savechanges'));
$buttonarray[] = $mform->createElement('cancel');
$mform->addGroup($buttonarray, 'buttonar', '', [' '], false);
$mform->closeHeaderBefore('buttonar');
}
public function set_data($defaultvalues) {
$mform = $this->_form;
if ($defaultvalues->fromtags) {
$fromtagselement = $mform->getElement('fromtags');
foreach ($defaultvalues->fromtags as $fromtag) {
if (!$fromtagselement->optionExists($fromtag)) {
$optionname = get_string('randomfromunavailabletag', 'mod_quiz', explode(',', $fromtag)[1]);
$fromtagselement->addOption($optionname, $fromtag);
}
}
}
parent::set_data($defaultvalues);
}
}
+657
View File
@@ -0,0 +1,657 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz;
use coding_exception;
use core\di;
use core\hook;
use core_component;
use mod_quiz\event\quiz_grade_updated;
use mod_quiz\hook\structure_modified;
use mod_quiz\output\grades\grade_out_of;
use qubaid_condition;
use qubaid_list;
use question_engine_data_mapper;
use question_usage_by_activity;
use stdClass;
/**
* This class contains all the logic for computing the grade of a quiz.
*
* There are two sorts of calculation which need to be done. For a single
* attempt, we need to compute the total attempt score from score for each question.
* And for a quiz user, we need to compute the final grade from all the separate attempt grades.
*
* @package mod_quiz
* @copyright 2023 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class grade_calculator {
/** @var float a number that is effectively zero. Used to avoid division-by-zero or underflow problems. */
const ALMOST_ZERO = 0.000005;
/** @var quiz_settings the quiz for which this instance computes grades. */
protected $quizobj;
/**
* @var stdClass[]|null quiz_grade_items for this quiz indexed by id, sorted by sortorder, with a maxmark field added.
*
* Lazy-loaded when needed. See {@see ensure_grade_items_loaded()}.
*/
protected ?array $gradeitems = null;
/**
* @var ?stdClass[]|null quiz_slot for this quiz. Only ->slot and ->quizgradeitemid fields are used.
*
* This is either set by another class that already has the data, using {@see set_slots()}
* or it is lazy-loaded when needed. See {@see ensure_slots_loaded()}.
*/
protected ?array $slots = null;
/**
* Constructor. Recommended way to get an instance is $quizobj->get_grade_calculator();
*
* @param quiz_settings $quizobj
*/
protected function __construct(quiz_settings $quizobj) {
$this->quizobj = $quizobj;
}
/**
* Factory. The recommended way to get an instance is $quizobj->get_grade_calculator();
*
* @param quiz_settings $quizobj settings of a quiz.
* @return grade_calculator instance of this class for the given quiz.
*/
public static function create(quiz_settings $quizobj): grade_calculator {
return new self($quizobj);
}
/**
* Update the sumgrades field of the quiz.
*
* This needs to be called whenever the grading structure of the quiz is changed.
* For example if a question is added or removed, or a question weight is changed.
*
* You should call {@see quiz_delete_previews()} before you call this function.
*/
public function recompute_quiz_sumgrades(): void {
global $DB;
$quiz = $this->quizobj->get_quiz();
// Update sumgrades in the database.
$DB->execute("
UPDATE {quiz}
SET sumgrades = COALESCE((
SELECT SUM(maxmark)
FROM {quiz_slots}
WHERE quizid = {quiz}.id
), 0)
WHERE id = ?
", [$quiz->id]);
// Update the value in memory.
$quiz->sumgrades = $DB->get_field('quiz', 'sumgrades', ['id' => $quiz->id]);
if ($quiz->sumgrades < self::ALMOST_ZERO && quiz_has_attempts($quiz->id)) {
// If the quiz has been attempted, and the sumgrades has been
// set to 0, then we must also set the maximum possible grade to 0, or
// we will get a divide by zero error.
self::update_quiz_maximum_grade(0);
}
// This class callback is deprecated, and will be removed in Moodle 4.8 (MDL-80327).
// Use the structure_modified hook instead.
$callbackclasses = core_component::get_plugin_list_with_class('quiz', 'quiz_structure_modified');
foreach ($callbackclasses as $callbackclass) {
component_class_callback($callbackclass, 'callback', [$quiz->id], null, true);
}
di::get(hook\manager::class)->dispatch(new structure_modified($this->quizobj->get_structure()));
}
/**
* Update the sumgrades field of attempts at this quiz.
*/
public function recompute_all_attempt_sumgrades(): void {
global $DB;
$dm = new question_engine_data_mapper();
$timenow = time();
$DB->execute("
UPDATE {quiz_attempts}
SET timemodified = :timenow,
sumgrades = (
{$dm->sum_usage_marks_subquery('uniqueid')}
)
WHERE quiz = :quizid AND state = :finishedstate
", [
'timenow' => $timenow,
'quizid' => $this->quizobj->get_quizid(),
'finishedstate' => quiz_attempt::FINISHED
]);
}
/**
* Update the final grade at this quiz for a particular student.
*
* That is, given the quiz settings, and all the attempts this user has made,
* compute their final grade for the quiz, as shown in the gradebook.
*
* The $attempts parameter is for efficiency. If you already have the data for
* all this user's attempts loaded (for example from {@see quiz_get_user_attempts()}
* or because you are looping through a large recordset fetched in one efficient query,
* then you can pass that data here to save DB queries.
*
* @param int|null $userid The userid to calculate the grade for. Defaults to the current user.
* @param array $attempts if you already have this user's attempt records loaded, pass them here to save queries.
*/
public function recompute_final_grade(?int $userid = null, array $attempts = []): void {
global $DB, $USER;
$quiz = $this->quizobj->get_quiz();
if (empty($userid)) {
$userid = $USER->id;
}
if (!$attempts) {
// Get all the attempts made by the user.
$attempts = quiz_get_user_attempts($quiz->id, $userid);
}
// Calculate the best grade.
$bestgrade = $this->compute_final_grade_from_attempts($attempts);
$bestgrade = quiz_rescale_grade($bestgrade, $quiz, false);
// Save the best grade in the database.
if (is_null($bestgrade)) {
$DB->delete_records('quiz_grades', ['quiz' => $quiz->id, 'userid' => $userid]);
} else if ($grade = $DB->get_record('quiz_grades',
['quiz' => $quiz->id, 'userid' => $userid])) {
$grade->grade = $bestgrade;
$grade->timemodified = time();
$DB->update_record('quiz_grades', $grade);
} else {
$grade = new stdClass();
$grade->quiz = $quiz->id;
$grade->userid = $userid;
$grade->grade = $bestgrade;
$grade->timemodified = time();
$DB->insert_record('quiz_grades', $grade);
}
quiz_update_grades($quiz, $userid);
}
/**
* Calculate the overall grade for a quiz given a number of attempts by a particular user.
*
* @param array $attempts an array of all the user's attempts at this quiz in order.
* @return float|null the overall grade, or null if the user does not have a grade.
*/
protected function compute_final_grade_from_attempts(array $attempts): ?float {
$grademethod = $this->quizobj->get_quiz()->grademethod;
switch ($grademethod) {
case QUIZ_ATTEMPTFIRST:
$firstattempt = reset($attempts);
return $firstattempt->sumgrades;
case QUIZ_ATTEMPTLAST:
$lastattempt = end($attempts);
return $lastattempt->sumgrades;
case QUIZ_GRADEAVERAGE:
$sum = 0;
$count = 0;
foreach ($attempts as $attempt) {
if (!is_null($attempt->sumgrades)) {
$sum += $attempt->sumgrades;
$count++;
}
}
if ($count == 0) {
return null;
}
return $sum / $count;
case QUIZ_GRADEHIGHEST:
$max = null;
foreach ($attempts as $attempt) {
if ($attempt->sumgrades > $max) {
$max = $attempt->sumgrades;
}
}
return $max;
default:
throw new coding_exception('Unrecognised grading method ' . $grademethod);
}
}
/**
* Update the final grade at this quiz for all students.
*
* This function is equivalent to calling {@see recompute_final_grade()} for all
* users who have attempted the quiz, but is much more efficient.
*/
public function recompute_all_final_grades(): void {
global $DB;
$quiz = $this->quizobj->get_quiz();
// If the quiz does not contain any graded questions, then there is nothing to do.
if (!$quiz->sumgrades) {
return;
}
$param = ['iquizid' => $quiz->id, 'istatefinished' => quiz_attempt::FINISHED];
$firstlastattemptjoin = "JOIN (
SELECT
iquiza.userid,
MIN(attempt) AS firstattempt,
MAX(attempt) AS lastattempt
FROM {quiz_attempts} iquiza
WHERE
iquiza.state = :istatefinished AND
iquiza.preview = 0 AND
iquiza.quiz = :iquizid
GROUP BY iquiza.userid
) first_last_attempts ON first_last_attempts.userid = quiza.userid";
switch ($quiz->grademethod) {
case QUIZ_ATTEMPTFIRST:
// Because of the where clause, there will only be one row, but we
// must still use an aggregate function.
$select = 'MAX(quiza.sumgrades)';
$join = $firstlastattemptjoin;
$where = 'quiza.attempt = first_last_attempts.firstattempt AND';
break;
case QUIZ_ATTEMPTLAST:
// Because of the where clause, there will only be one row, but we
// must still use an aggregate function.
$select = 'MAX(quiza.sumgrades)';
$join = $firstlastattemptjoin;
$where = 'quiza.attempt = first_last_attempts.lastattempt AND';
break;
case QUIZ_GRADEAVERAGE:
$select = 'AVG(quiza.sumgrades)';
$join = '';
$where = '';
break;
default:
case QUIZ_GRADEHIGHEST:
$select = 'MAX(quiza.sumgrades)';
$join = '';
$where = '';
break;
}
if ($quiz->sumgrades >= self::ALMOST_ZERO) {
$finalgrade = $select . ' * ' . ($quiz->grade / $quiz->sumgrades);
} else {
$finalgrade = '0';
}
$param['quizid'] = $quiz->id;
$param['quizid2'] = $quiz->id;
$param['quizid3'] = $quiz->id;
$param['quizid4'] = $quiz->id;
$param['statefinished'] = quiz_attempt::FINISHED;
$param['statefinished2'] = quiz_attempt::FINISHED;
$param['almostzero'] = self::ALMOST_ZERO;
$finalgradesubquery = "
SELECT quiza.userid, $finalgrade AS newgrade
FROM {quiz_attempts} quiza
$join
WHERE
$where
quiza.state = :statefinished AND
quiza.preview = 0 AND
quiza.quiz = :quizid3
GROUP BY quiza.userid";
$changedgrades = $DB->get_records_sql("
SELECT users.userid, qg.id, qg.grade, newgrades.newgrade
FROM (
SELECT userid
FROM {quiz_grades} qg
WHERE quiz = :quizid
UNION
SELECT DISTINCT userid
FROM {quiz_attempts} quiza2
WHERE
quiza2.state = :statefinished2 AND
quiza2.preview = 0 AND
quiza2.quiz = :quizid2
) users
LEFT JOIN {quiz_grades} qg ON qg.userid = users.userid AND qg.quiz = :quizid4
LEFT JOIN (
$finalgradesubquery
) newgrades ON newgrades.userid = users.userid
WHERE
ABS(newgrades.newgrade - qg.grade) > :almostzero OR
((newgrades.newgrade IS NULL OR qg.grade IS NULL) AND NOT
(newgrades.newgrade IS NULL AND qg.grade IS NULL))",
// The mess on the previous line is detecting where the value is
// NULL in one column, and NOT NULL in the other, but SQL does
// not have an XOR operator, and MS SQL server can't cope with
// (newgrades.newgrade IS NULL) <> (qg.grade IS NULL).
$param);
$timenow = time();
$todelete = [];
foreach ($changedgrades as $changedgrade) {
if (is_null($changedgrade->newgrade)) {
$todelete[] = $changedgrade->userid;
} else if (is_null($changedgrade->grade)) {
$toinsert = new stdClass();
$toinsert->quiz = $quiz->id;
$toinsert->userid = $changedgrade->userid;
$toinsert->timemodified = $timenow;
$toinsert->grade = $changedgrade->newgrade;
$DB->insert_record('quiz_grades', $toinsert);
} else {
$toupdate = new stdClass();
$toupdate->id = $changedgrade->id;
$toupdate->grade = $changedgrade->newgrade;
$toupdate->timemodified = $timenow;
$DB->update_record('quiz_grades', $toupdate);
}
}
if (!empty($todelete)) {
list($test, $params) = $DB->get_in_or_equal($todelete);
$DB->delete_records_select('quiz_grades', 'quiz = ? AND userid ' . $test,
array_merge([$quiz->id], $params));
}
}
/**
* Update the quiz setting for the grade the quiz is out of.
*
* This function will update the data in quiz_grades and quiz_feedback, and
* pass the new grades on to the gradebook.
*
* @param float $newgrade the new maximum grade for the quiz.
*/
public function update_quiz_maximum_grade(float $newgrade): void {
global $DB;
$quiz = $this->quizobj->get_quiz();
// This is potentially expensive, so only do it if necessary.
if (abs($quiz->grade - $newgrade) < self::ALMOST_ZERO) {
// Nothing to do.
return;
}
// Use a transaction.
$transaction = $DB->start_delegated_transaction();
// Update the quiz table.
$oldgrade = $quiz->grade;
$quiz->grade = $newgrade;
$timemodified = time();
$DB->update_record('quiz', (object) [
'id' => $quiz->id,
'grade' => $newgrade,
'timemodified' => $timemodified,
]);
// Rescale the grade of all quiz attempts.
if ($oldgrade < $newgrade) {
// The new total is bigger, so we need to recompute fully to avoid underflow problems.
$this->recompute_all_final_grades();
} else {
// New total smaller, so we can rescale the grades efficiently.
$DB->execute("
UPDATE {quiz_grades}
SET grade = ? * grade, timemodified = ?
WHERE quiz = ?
", [$newgrade / $oldgrade, $timemodified, $quiz->id]);
}
// Rescale the overall feedback boundaries.
if ($oldgrade > self::ALMOST_ZERO) {
// Update the quiz_feedback table.
$factor = $newgrade / $oldgrade;
$DB->execute("
UPDATE {quiz_feedback}
SET mingrade = ? * mingrade, maxgrade = ? * maxgrade
WHERE quizid = ?
", [$factor, $factor, $quiz->id]);
}
// Update grade item and send all grades to gradebook.
quiz_grade_item_update($quiz);
quiz_update_grades($quiz);
// Log quiz grade updated event.
quiz_grade_updated::create([
'context' => $this->quizobj->get_context(),
'objectid' => $quiz->id,
'other' => [
'oldgrade' => $oldgrade + 0, // Remove trailing 0s.
'newgrade' => $newgrade,
]
])->trigger();
$transaction->allow_commit();
}
/**
* Ensure the {@see grade_calculator::$gradeitems} field is ready to use.
*/
protected function ensure_grade_items_loaded(): void {
global $DB;
if ($this->gradeitems !== null) {
return; // Already done.
}
$this->gradeitems = $DB->get_records_sql("
SELECT gi.id,
gi.quizid,
gi.sortorder,
gi.name,
COALESCE(SUM(slot.maxmark), 0) AS maxmark
FROM {quiz_grade_items} gi
LEFT JOIN {quiz_slots} slot ON slot.quizgradeitemid = gi.id
WHERE gi.quizid = ? AND slot.quizid = ?
GROUP BY gi.id, gi.quizid, gi.sortorder, gi.name
ORDER BY gi.sortorder
", [$this->quizobj->get_quizid(), $this->quizobj->get_quizid()]);
}
/**
* Get the extra grade items for this quiz.
*
* Returned objects have fields ->id, ->quizid, ->sortorder, ->name and maxmark.
* @return stdClass[] the grade items for this quiz.
*/
public function get_grade_items(): array {
$this->ensure_grade_items_loaded();
return $this->gradeitems;
}
/**
* Lets other code pass in the slot information, so it does note have to be re-loaded from the DB.
*
* @param stdClass[] $slots the data from quiz_slots. The only required fields are ->slot and ->quizgradeitemid.
*/
public function set_slots(array $slots): void {
global $CFG;
$this->slots = $slots;
if ($CFG->debugdeveloper) {
foreach ($slots as $slot) {
if (!property_exists($slot, 'slot') || !property_exists($slot, 'quizgradeitemid')) {
debugging('Slot data passed to grade_calculator::set_slots ' .
'must have at least ->slot and ->quizgradeitemid set.', DEBUG_DEVELOPER);
break; // Only necessary to say this once.
}
}
}
}
/**
* Ensure the {@see $gradeitems} field is ready to use.
*/
protected function ensure_slots_loaded(): void {
global $DB;
if ($this->slots !== null) {
return; // Already done.
}
$this->slots = $DB->get_records('quiz_slots', ['quizid' => $this->quizobj->get_quizid()],
'slot', 'slot, id, quizgradeitemid');
}
/**
* Compute the grade and maximum for each item, for an attempt where the question_usage_by_activity is available.
*
* @param question_usage_by_activity $quba usage for the quiz attempt we want to calculate the grades of.
* @return grade_out_of[] the grade for each item where the total grade is not zero.
* ->name will be set to the grade item name. Must be output through {@see format_string()}.
*/
public function compute_grade_item_totals(question_usage_by_activity $quba): array {
$this->ensure_grade_items_loaded();
if (empty($this->gradeitems)) {
// No extra grade items.
return [];
}
$this->ensure_slots_loaded();
// Prepare a place to store the results for each grade-item.
$grades = [];
foreach ($this->gradeitems as $gradeitem) {
$grades[$gradeitem->id] = new grade_out_of(
$this->quizobj->get_quiz(), 0, $gradeitem->maxmark, name: $gradeitem->name);
}
// Add up the scores.
foreach ($this->slots as $slot) {
if (!$slot->quizgradeitemid) {
continue;
}
$grades[$slot->quizgradeitemid]->grade += $quba->get_question_mark($slot->slot);
}
// Remove any grade items where the total is 0.
foreach ($grades as $gradeitemid => $grade) {
if ($grade->maxgrade < self::ALMOST_ZERO) {
unset($grades[$gradeitemid]);
}
}
return $grades;
}
/**
* Compute the grade and maximum for each item, for some attempts where we only have the usage ids.
*
* @param int[] $qubaids array of usage ids.
* @return grade_out_of[][] question_usage.id => array of grade_out_of.
* ->name will be set to the grade item name. Must be output through {@see format_string()}..
*/
public function compute_grade_item_totals_for_attempts(array $qubaids): array {
$this->ensure_grade_items_loaded();
$grades = [];
foreach ($qubaids as $qubaid) {
$grades[$qubaid] = [];
}
if (empty($this->gradeitems || empty($qubaids))) {
// Nothing to do.
return $grades;
}
$gradesdata = $this->load_grade_item_totals(new qubaid_list($qubaids));
foreach ($qubaids as $qubaid) {
foreach ($this->gradeitems as $gradeitem) {
if ($gradeitem->maxmark < self::ALMOST_ZERO) {
continue;
}
$grades[$qubaid][$gradeitem->id] = new grade_out_of(
$this->quizobj->get_quiz(),
$gradesdata[$qubaid][$gradeitem->id] ?? 0,
$gradeitem->maxmark,
name: $gradeitem->name,
);
}
}
return $grades;
}
/**
* Query the database return the total mark for each grade item for a set of attempts.
*
* @param qubaid_condition $qubaids which question_usages to computer the total marks for.
* @return float[][] Array question_usage.id => quiz_grade_item.id => mark.
*/
public function load_grade_item_totals(qubaid_condition $qubaids): array {
global $DB;
$dm = new question_engine_data_mapper();
[$qalatestview, $viewparams] = $dm->question_attempt_latest_state_view('qalatest', $qubaids);
$totals = $DB->get_records_sql("
SELECT " . $DB->sql_concat('qalatest.questionusageid', "'#'", 'slot.quizgradeitemid') . " AS uniquefirstcolumn,
qalatest.questionusageid,
slot.quizgradeitemid,
SUM(qalatest.fraction * qalatest.maxmark) AS summarks
FROM $qalatestview
JOIN {quiz_slots} slot ON slot.slot = qalatest.slot
JOIN {quiz_grade_items} qgi ON qgi.id = slot.quizgradeitemid
GROUP BY qalatest.questionusageid, slot.quizgradeitemid
", $viewparams);
$marks = [];
foreach ($totals as $total) {
$marks[$total->questionusageid][$total->quizgradeitemid] = $total->summarks + 0; // Convert to float with + 0.
}
return $marks;
}
}
+113
View File
@@ -0,0 +1,113 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Group observers.
*
* @package mod_quiz
* @copyright 2013 Frédéric Massart
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_quiz;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
/**
* Group observers class.
*
* @package mod_quiz
* @copyright 2013 Frédéric Massart
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class group_observers {
/**
* Flag whether a course reset is in progress or not.
*
* @var int The course ID.
*/
protected static $resetinprogress = false;
/**
* A course reset has started.
*
* @param \core\event\base $event The event.
* @return void
*/
public static function course_reset_started($event) {
self::$resetinprogress = $event->courseid;
}
/**
* A course reset has ended.
*
* @param \core\event\base $event The event.
* @return void
*/
public static function course_reset_ended($event) {
if (!empty(self::$resetinprogress)) {
if (!empty($event->other['reset_options']['reset_groups_remove'])) {
quiz_process_group_deleted_in_course($event->courseid);
}
if (!empty($event->other['reset_options']['reset_groups_members'])) {
quiz_update_open_attempts(['courseid' => $event->courseid]);
}
}
self::$resetinprogress = null;
}
/**
* A group was deleted.
*
* @param \core\event\base $event The event.
* @return void
*/
public static function group_deleted($event) {
if (!empty(self::$resetinprogress)) {
// We will take care of that once the course reset ends.
return;
}
quiz_process_group_deleted_in_course($event->courseid);
}
/**
* A group member was removed.
*
* @param \core\event\base $event The event.
* @return void
*/
public static function group_member_added($event) {
quiz_update_open_attempts(['userid' => $event->relateduserid, 'groupid' => $event->objectid]);
}
/**
* A group member was deleted.
*
* @param \core\event\base $event The event.
* @return void
*/
public static function group_member_removed($event) {
if (!empty(self::$resetinprogress)) {
// We will take care of that once the course reset ends.
return;
}
quiz_update_open_attempts(['userid' => $event->relateduserid, 'groupid' => $event->objectid]);
}
}
@@ -0,0 +1,73 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\hook;
use core\attribute;
/**
* A quiz attempt changed state.
*
* @package mod_quiz
* @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
#[attribute\label('A quiz attempt changed state.')]
#[attribute\tags('quiz', 'attempt')]
#[attribute\hook\replaces_callbacks('quiz_attempt_deleted::callback')]
class attempt_state_changed {
/**
* Create a new hook instance.
*
* @param ?\stdClass $originalattempt The original database record for the attempt, null if it has just been created.
* @param ?\stdClass $updatedattempt The updated database record of the new attempt, null if it has just been deleted.
*/
public function __construct(
/** @var ?\stdClass The original database record for the attempt, null if it has just been created. */
protected ?\stdClass $originalattempt,
/** @var ?\stdClass The updated database record of the new attempt, null if it has just been deleted. */
protected ?\stdClass $updatedattempt,
) {
if (is_null($this->originalattempt) && is_null($this->updatedattempt)) {
throw new \InvalidArgumentException('originalattempt and updatedattempt cannot both be null.');
}
if (
!is_null($this->originalattempt)
&& !is_null($this->updatedattempt)
&& $this->originalattempt->id != $this->updatedattempt->id
) {
throw new \InvalidArgumentException('originalattempt and updatedattempt must have the same id.');
}
}
/**
* Get the original attempt, null if it has just been created.
*
* @return ?\stdClass
*/
public function get_original_attempt(): ?\stdClass {
return $this->originalattempt;
}
/**
* Get the updated attempt, null if it has just been deleted.
*
* @return ?\stdClass
*/
public function get_updated_attempt(): ?\stdClass {
return $this->updatedattempt;
}
}
@@ -0,0 +1,52 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\hook;
use core\attribute;
use mod_quiz\structure;
/**
* The quiz structure has been modified
*
* @package mod_quiz
* @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
#[attribute\label('The quiz structure has been modified')]
#[attribute\tags('quiz', 'structure')]
#[attribute\hook\replaces_callbacks('quiz_structure_modified::callback')]
class structure_modified {
/**
* Create a new hook with the modified structure.
*
* @param structure $structure The new structure.
*/
public function __construct(
/** @var structure The new structure */
protected structure $structure
) {
}
/**
* Returns the new structure of the quiz.
*
* @return structure The structure object.
*/
public function get_structure(): structure {
return $this->structure;
}
}
+356
View File
@@ -0,0 +1,356 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\local;
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 stdClass $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 stdClass $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 stdClass $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 stdClass $quiz the data from the quiz form, including $quiz->id
* which is the id of the quiz being saved.
*/
public static function save_settings($quiz) {
// 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 stdClass $quiz the data from the database, including $quiz->id
* which is the id of the quiz being deleted.
* @since Moodle 2.7.1, 2.6.4, 2.5.7
*/
public static function delete_settings($quiz) {
// 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 [];
}
}
+126
View File
@@ -0,0 +1,126 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\local;
/**
* Cache manager for quiz overrides
*
* Override cache data is set via its data source, {@see \mod_quiz\cache\overrides}
* @package mod_quiz
* @copyright 2024 Matthew Hilton <matthewhilton@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class override_cache {
/** @var string invalidation event used to purge data when reset_userdata is called, {@see \cache_helper::purge_by_event()} **/
public const INVALIDATION_USERDATARESET = 'userdatareset';
/**
* Create override_cache object and link to quiz
*
* @param int $quizid The quiz to link this cache to
*/
public function __construct(
/** @var int $quizid ID of quiz cache is being operated on **/
protected readonly int $quizid
) {
}
/**
* Returns the override cache
*
* @return \cache
*/
protected function get_cache(): \cache {
return \cache::make('mod_quiz', 'overrides');
}
/**
* Returns group cache key
*
* @param int $groupid
* @return string the group cache key
*/
protected function get_group_cache_key(int $groupid): string {
return "{$this->quizid}_g_{$groupid}";
}
/**
* Returns user cache key
*
* @param int $userid
* @return string the user cache key
*/
protected function get_user_cache_key(int $userid): string {
return "{$this->quizid}_u_{$userid}";
}
/**
* Returns the override value in the cache for the given group
*
* @param int $groupid group to get cached override data for
* @return ?\stdClass override value in the cache for the given group, or null if there is none.
*/
public function get_cached_group_override(int $groupid): ?\stdClass {
$raw = $this->get_cache()->get($this->get_group_cache_key($groupid));
return empty($raw) || !is_object($raw) ? null : (object) $raw;
}
/**
* Returns the override value in the cache for the given user
*
* @param int $userid user to get cached override data for
* @return ?\stdClass the override value in the cache for the given user, or null if there is none.
*/
public function get_cached_user_override(int $userid): ?\stdClass {
$raw = $this->get_cache()->get($this->get_user_cache_key($userid));
return empty($raw) || !is_object($raw) ? null : (object) $raw;
}
/**
* Deletes the cached override data for a given group
*
* @param int $groupid group to delete data for
*/
public function clear_for_group(int $groupid): void {
$this->get_cache()->delete($this->get_group_cache_key($groupid));
}
/**
* Deletes the cached override data for the given user
*
* @param int $userid user to delete data for
*/
public function clear_for_user(int $userid): void {
$this->get_cache()->delete($this->get_user_cache_key($userid));
}
/**
* Clears the cache for the given user and/or group.
*
* @param ?int $userid user to delete data for, or null.
* @param ?int $groupid group to delete data for, or null.
*/
public function clear_for(?int $userid = null, ?int $groupid = null): void {
if (!empty($userid)) {
$this->clear_for_user($userid);
}
if (!empty($groupid)) {
$this->clear_for_group($groupid);
}
}
}
+630
View File
@@ -0,0 +1,630 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\local;
use mod_quiz\event\group_override_created;
use mod_quiz\event\group_override_deleted;
use mod_quiz\event\group_override_updated;
use mod_quiz\event\user_override_created;
use mod_quiz\event\user_override_deleted;
use mod_quiz\event\user_override_updated;
use mod_quiz\quiz_settings;
/**
* Manager class for quiz overrides
*
* @package mod_quiz
* @copyright 2024 Matthew Hilton <matthewhilton@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class override_manager {
/** @var array quiz setting keys that can be overwritten **/
private const OVERRIDEABLE_QUIZ_SETTINGS = ['timeopen', 'timeclose', 'timelimit', 'attempts', 'password'];
/**
* Create override manager
*
* @param \stdClass $quiz The quiz to link the manager to.
* @param \context_module $context Context being operated in
*/
public function __construct(
/** @var \stdClass The quiz linked to this manager instance **/
protected readonly \stdClass $quiz,
/** @var \context_module The context being operated in **/
public readonly \context_module $context
) {
global $CFG;
// Required for quiz_* methods.
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
// Sanity check that the context matches the quiz.
if (empty($quiz->cmid) || $quiz->cmid != $context->instanceid) {
throw new \coding_exception("Given context does not match the quiz object");
}
}
/**
* Returns all overrides for the linked quiz.
*
* @return array of quiz_override records
*/
public function get_all_overrides(): array {
global $DB;
return $DB->get_records('quiz_overrides', ['quiz' => $this->quiz->id]);
}
/**
* Validates the data, usually from a moodleform or a webservice call.
* If it contains an 'id' property, additional validation is performed against the existing record.
*
* @param array $formdata data from moodleform or webservice call.
* @return array array where the keys are error elements, and the values are lists of errors for each element.
*/
public function validate_data(array $formdata): array {
global $DB;
// Because this can be called directly (e.g. via edit_override_form)
// and not just through save_override, we must ensure the data
// is parsed in the same way.
$formdata = $this->parse_formdata($formdata);
$formdata = (object) $formdata;
$errors = [];
// Ensure at least one of the overrideable settings is set.
$keysthatareset = array_map(function ($key) use ($formdata) {
return isset($formdata->$key) && !is_null($formdata->$key);
}, self::OVERRIDEABLE_QUIZ_SETTINGS);
if (!in_array(true, $keysthatareset)) {
$errors['general'][] = new \lang_string('nooverridedata', 'quiz');
}
// Ensure quiz is a valid quiz.
if (empty($formdata->quiz) || empty(get_coursemodule_from_instance('quiz', $formdata->quiz))) {
$errors['quiz'][] = new \lang_string('overrideinvalidquiz', 'quiz');
}
// Ensure either userid or groupid is set.
if (empty($formdata->userid) && empty($formdata->groupid)) {
$errors['general'][] = new \lang_string('overridemustsetuserorgroup', 'quiz');
}
// Ensure not both userid and groupid are set.
if (!empty($formdata->userid) && !empty($formdata->groupid)) {
$errors['general'][] = new \lang_string('overridecannotsetbothgroupanduser', 'quiz');
}
// If group is set, ensure it is a real group.
if (!empty($formdata->groupid) && empty(groups_get_group($formdata->groupid))) {
$errors['groupid'][] = new \lang_string('overrideinvalidgroup', 'quiz');
}
// If user is set, ensure it is a valid user.
if (!empty($formdata->userid) && !\core_user::is_real_user($formdata->userid, true)) {
$errors['userid'][] = new \lang_string('overrideinvaliduser', 'quiz');
}
// Ensure timeclose is later than timeopen, if both are set.
if (!empty($formdata->timeclose) && !empty($formdata->timeopen) && $formdata->timeclose <= $formdata->timeopen) {
$errors['timeclose'][] = new \lang_string('closebeforeopen', 'quiz');
}
// Ensure attempts is a integer greater than or equal to 0 (0 is unlimited attempts).
if (isset($formdata->attempts) && ((int) $formdata->attempts < 0)) {
$errors['attempts'][] = new \lang_string('overrideinvalidattempts', 'quiz');
}
// Ensure timelimit is greather than zero.
if (!empty($formdata->timelimit) && $formdata->timelimit <= 0) {
$errors['timelimit'][] = new \lang_string('overrideinvalidtimelimit', 'quiz');
}
// Ensure other records do not exist with the same group or user.
if (!empty($formdata->quiz) && (!empty($formdata->userid) || !empty($formdata->groupid))) {
$existingrecordparams = ['quiz' => $formdata->quiz, 'groupid' => $formdata->groupid ?? null,
'userid' => $formdata->userid ?? null, ];
$records = $DB->get_records('quiz_overrides', $existingrecordparams, '', 'id');
// Ignore self if updating.
if (!empty($formdata->id)) {
unset($records[$formdata->id]);
}
// If count is not zero, it means existing records exist already for this user/group.
if (!empty($records)) {
$errors['general'][] = new \lang_string('overridemultiplerecordsexist', 'quiz');
}
}
// If is existing record, validate it against the existing record.
if (!empty($formdata->id)) {
$existingrecorderrors = self::validate_against_existing_record($formdata->id, $formdata);
$errors = array_merge($errors, $existingrecorderrors);
}
// Implode each value (array of error strings) into a single error string.
foreach ($errors as $key => $value) {
$errors[$key] = implode(",", $value);
}
return $errors;
}
/**
* Returns the existing quiz override record with the given ID or null if does not exist.
*
* @param int $id existing quiz override id
* @return ?\stdClass record, if exists
*/
private static function get_existing(int $id): ?\stdClass {
global $DB;
return $DB->get_record('quiz_overrides', ['id' => $id]) ?: null;
}
/**
* Validates the formdata against an existing record.
*
* @param int $existingid id of existing quiz override record
* @param \stdClass $formdata formdata, usually from moodleform or webservice call.
* @return array array where the keys are error elements, and the values are lists of errors for each element.
*/
private static function validate_against_existing_record(int $existingid, \stdClass $formdata): array {
$existingrecord = self::get_existing($existingid);
$errors = [];
// Existing record must exist.
if (empty($existingrecord)) {
$errors['general'][] = new \lang_string('overrideinvalidexistingid', 'quiz');
}
// Group value must match existing record if it is set in the formdata.
if (!empty($existingrecord) && !empty($formdata->groupid) && $existingrecord->groupid != $formdata->groupid) {
$errors['groupid'][] = new \lang_string('overridecannotchange', 'quiz');
}
// User value must match existing record if it is set in the formdata.
if (!empty($existingrecord) && !empty($formdata->userid) && $existingrecord->userid != $formdata->userid) {
$errors['userid'][] = new \lang_string('overridecannotchange', 'quiz');
}
return $errors;
}
/**
* Parses the formdata by finding only the OVERRIDEABLE_QUIZ_SETTINGS,
* clearing any values that match the existing quiz, and re-adds the user or group id.
*
* @param array $formdata data usually from moodleform or webservice call.
* @return array array containing parsed formdata, with keys as the properties and values as the values.
* Any values set the same as the existing quiz are set to null.
*/
public function parse_formdata(array $formdata): array {
// Get the data from the form that we want to update.
$settings = array_intersect_key($formdata, array_flip(self::OVERRIDEABLE_QUIZ_SETTINGS));
// Remove values that are the same as currently in the quiz.
$settings = $this->clear_unused_values($settings);
// Add the user / group back as applicable.
$userorgroupdata = array_intersect_key($formdata, array_flip(['userid', 'groupid', 'quiz', 'id']));
return array_merge($settings, $userorgroupdata);
}
/**
* Saves the given override. If an id is given, it updates, otherwise it creates a new one.
* Note, capabilities are not checked, {@see require_manage_capability()}
*
* @param array $formdata data usually from moodleform or webservice call.
* @return int updated/inserted record id
*/
public function save_override(array $formdata): int {
global $DB;
// Extract only the necessary data.
$datatoset = $this->parse_formdata($formdata);
$datatoset['quiz'] = $this->quiz->id;
// Validate the data is OK.
$errors = $this->validate_data($datatoset);
if (!empty($errors)) {
$errorstr = implode(',', $errors);
throw new \invalid_parameter_exception($errorstr);
}
// Insert or update.
$id = $datatoset['id'] ?? 0;
if (!empty($id)) {
$DB->update_record('quiz_overrides', $datatoset);
} else {
$id = $DB->insert_record('quiz_overrides', $datatoset);
}
$userid = $datatoset['userid'] ?? null;
$groupid = $datatoset['groupid'] ?? null;
// Clear the cache.
$cache = new override_cache($this->quiz->id);
$cache->clear_for($userid, $groupid);
// Trigger moodle events.
if (empty($formdata['id'])) {
$this->fire_created_event($id, $userid, $groupid);
} else {
$this->fire_updated_event($id, $userid, $groupid);
}
// Update open events.
quiz_update_open_attempts(['quizid' => $this->quiz->id]);
// Update calendar events.
$isgroup = !empty($datatoset['groupid']);
if ($isgroup) {
// If is group, must update the entire quiz calendar events.
quiz_update_events($this->quiz);
} else {
// If is just a user, can update only their calendar event.
quiz_update_events($this->quiz, (object) $datatoset);
}
return $id;
}
/**
* Deletes all the overrides for the linked quiz
*
* @param bool $shouldlog If true, will log a override_deleted event
*/
public function delete_all_overrides(bool $shouldlog = true): void {
global $DB;
$overrides = $DB->get_records('quiz_overrides', ['quiz' => $this->quiz->id], '', 'id,userid,groupid');
$this->delete_overrides($overrides, $shouldlog);
}
/**
* Deletes overrides given just their ID.
* Note, the given IDs must exist and user must have access to them otherwise an exception will be thrown.
* Also note, capabilities are not checked, {@see require_manage_capability()}
*
* @param array $ids IDs of overrides to delete
* @param bool $shouldlog If true, will log a override_deleted event
*/
public function delete_overrides_by_id(array $ids, bool $shouldlog = true): void {
global $DB;
$quizsettings = quiz_settings::create($this->quiz->id);
// Filter for those overrides user can access.
[$sql, $params] = self::get_override_in_sql($this->quiz->id, $ids);
$records = array_filter(
$DB->get_records_select('quiz_overrides', $sql, $params, '', 'id,userid,groupid'),
fn(\stdClass $override) => $this->can_view_override(
$override,
$quizsettings->get_course(),
$quizsettings->get_cm(),
),
);
// Ensure all the given ids exist, so the user is aware if they give a dodgy id.
$missingids = array_diff($ids, array_keys($records));
if (!empty($missingids)) {
throw new \invalid_parameter_exception(get_string('overridemissingdelete', 'quiz', implode(',', $missingids)));
}
$this->delete_overrides($records, $shouldlog);
}
/**
* Builds sql and parameters to find overrides in quiz with the given ids
*
* @param int $quizid id of quiz
* @param array $ids array of quiz override ids
* @return array sql and params
*/
private static function get_override_in_sql(int $quizid, array $ids): array {
global $DB;
[$insql, $inparams] = $DB->get_in_or_equal($ids, SQL_PARAMS_NAMED);
$params = array_merge($inparams, ['quizid' => $quizid]);
$sql = 'id ' . $insql . ' AND quiz = :quizid';
return [$sql, $params];
}
/**
* Deletes the given overrides in the quiz linked to the override manager.
* Note - capabilities are not checked, {@see require_manage_capability()}
*
* @param array $overrides override to delete. Must specify an id, quizid, and either a userid or groupid.
* @param bool $shouldlog If true, will log a override_deleted event
*/
public function delete_overrides(array $overrides, bool $shouldlog = true): void {
global $DB;
foreach ($overrides as $override) {
if (empty($override->id)) {
throw new \coding_exception("All overrides must specify an ID");
}
// Sanity check that user xor group is specified.
// User or group is required to clear the cache.
self::ensure_userid_xor_groupid_set($override->userid ?? null, $override->groupid ?? null);
}
if (empty($overrides)) {
// Exit early, since delete select requires at least 1 record.
return;
}
// Match id and quiz.
[$sql, $params] = self::get_override_in_sql($this->quiz->id, array_column($overrides, 'id'));
$DB->delete_records_select('quiz_overrides', $sql, $params);
$cache = new override_cache($this->quiz->id);
// Perform other cleanup.
foreach ($overrides as $override) {
$userid = $override->userid ?? null;
$groupid = $override->groupid ?? null;
$cache->clear_for($userid, $groupid);
$this->delete_override_events($userid, $groupid);
if ($shouldlog) {
$this->fire_deleted_event($override->id, $userid, $groupid);
}
}
}
/**
* Ensures either userid or groupid is set, but not both.
* If neither or both are set, a coding exception is thrown.
*
* @param ?int $userid user for the record, or null
* @param ?int $groupid group for the record, or null
*/
private static function ensure_userid_xor_groupid_set(?int $userid = null, ?int $groupid = null): void {
$groupset = !empty($groupid);
$userset = !empty($userid);
// If either set, but not both (xor).
$xorset = $groupset ^ $userset;
if (!$xorset) {
throw new \coding_exception("Either userid or groupid must be specified, but not both.");
}
}
/**
* Deletes the events associated with the override.
*
* @param ?int $userid or null if groupid is specified
* @param ?int $groupid or null if the userid is specified
*/
private function delete_override_events(?int $userid = null, ?int $groupid = null): void {
global $DB;
// Sanity check.
self::ensure_userid_xor_groupid_set($userid, $groupid);
$eventssearchparams = ['modulename' => 'quiz', 'instance' => $this->quiz->id];
if (!empty($userid)) {
$eventssearchparams['userid'] = $userid;
}
if (!empty($groupid)) {
$eventssearchparams['groupid'] = $groupid;
}
$events = $DB->get_records('event', $eventssearchparams);
foreach ($events as $event) {
$eventold = \calendar_event::load($event);
$eventold->delete();
}
}
/**
* Requires the user has the override management capability
*/
public function require_manage_capability(): void {
require_capability('mod/quiz:manageoverrides', $this->context);
}
/**
* Requires the user has the override viewing capability
*/
public function require_read_capability(): void {
// If user can manage, they can also view.
// It would not make sense to be able to create and edit overrides without being able to view them.
if (!has_any_capability(['mod/quiz:viewoverrides', 'mod/quiz:manageoverrides'], $this->context)) {
throw new \required_capability_exception($this->context, 'mod/quiz:viewoverrides', 'nopermissions', '');
}
}
/**
* Determine whether user can view a given override record
*
* @param \stdClass $override
* @param \stdClass $course
* @param \cm_info $cm
* @return bool
*/
public function can_view_override(\stdClass $override, \stdClass $course, \cm_info $cm): bool {
if ($override->groupid) {
return groups_group_visible($override->groupid, $course, $cm);
} else {
return groups_user_groups_visible($course, $override->userid, $cm);
}
}
/**
* Builds common event data
*
* @param int $id override id
* @return array of data to add as parameters to an event.
*/
private function get_base_event_params(int $id): array {
return [
'context' => $this->context,
'other' => [
'quizid' => $this->quiz->id,
],
'objectid' => $id,
];
}
/**
* Log that a given override was deleted
*
* @param int $id of quiz override that was just deleted
* @param ?int $userid user attached to override record, or null
* @param ?int $groupid group attached to override record, or null
*/
private function fire_deleted_event(int $id, ?int $userid = null, ?int $groupid = null): void {
// Sanity check.
self::ensure_userid_xor_groupid_set($userid, $groupid);
$params = $this->get_base_event_params($id);
$params['objectid'] = $id;
if (!empty($userid)) {
$params['relateduserid'] = $userid;
user_override_deleted::create($params)->trigger();
}
if (!empty($groupid)) {
$params['other']['groupid'] = $groupid;
group_override_deleted::create($params)->trigger();
}
}
/**
* Log that a given override was created
*
* @param int $id of quiz override that was just created
* @param ?int $userid user attached to override record, or null
* @param ?int $groupid group attached to override record, or null
*/
private function fire_created_event(int $id, ?int $userid = null, ?int $groupid = null): void {
// Sanity check.
self::ensure_userid_xor_groupid_set($userid, $groupid);
$params = $this->get_base_event_params($id);
if (!empty($userid)) {
$params['relateduserid'] = $userid;
user_override_created::create($params)->trigger();
}
if (!empty($groupid)) {
$params['other']['groupid'] = $groupid;
group_override_created::create($params)->trigger();
}
}
/**
* Log that a given override was updated
*
* @param int $id of quiz override that was just updated
* @param ?int $userid user attached to override record, or null
* @param ?int $groupid group attached to override record, or null
*/
private function fire_updated_event(int $id, ?int $userid = null, ?int $groupid = null): void {
// Sanity check.
self::ensure_userid_xor_groupid_set($userid, $groupid);
$params = $this->get_base_event_params($id);
if (!empty($userid)) {
$params['relateduserid'] = $userid;
user_override_updated::create($params)->trigger();
}
if (!empty($groupid)) {
$params['other']['groupid'] = $groupid;
group_override_updated::create($params)->trigger();
}
}
/**
* Clears any overrideable settings in the formdata, where the value matches what is already in the quiz
* If they match, the data is set to null.
*
* @param array $formdata data usually from moodleform or webservice call.
* @return array formdata with same values cleared
*/
private function clear_unused_values(array $formdata): array {
foreach (self::OVERRIDEABLE_QUIZ_SETTINGS as $key) {
// If the formdata is the same as the current quiz object data, clear it.
if (isset($formdata[$key]) && $formdata[$key] == $this->quiz->$key) {
$formdata[$key] = null;
}
// Ensure these keys always are set (even if null).
$formdata[$key] = $formdata[$key] ?? null;
// If the formdata is empty, set it to null.
// This avoids putting 0, false, or '' into the DB since the override logic expects null.
// Attempts is the exception, it can have a integer value of '0', so we use is_numeric instead.
if ($key != 'attempts' && empty($formdata[$key])) {
$formdata[$key] = null;
}
if ($key == 'attempts' && !is_numeric($formdata[$key])) {
$formdata[$key] = null;
}
}
return $formdata;
}
/**
* Deletes orphaned group overrides in a given course.
* Note - permissions are not checked and events are not logged for performance reasons.
*
* @param int $courseid ID of course to delete orphaned group overrides in
* @return array array of quizzes that had orphaned group overrides.
*/
public static function delete_orphaned_group_overrides_in_course(int $courseid): array {
global $DB;
// It would be nice if we got the groupid that was deleted.
// Instead, we just update all quizzes with orphaned group overrides.
$sql = "SELECT o.id, o.quiz, o.groupid
FROM {quiz_overrides} o
JOIN {quiz} quiz ON quiz.id = o.quiz
LEFT JOIN {groups} grp ON grp.id = o.groupid
WHERE quiz.course = :courseid
AND o.groupid IS NOT NULL
AND grp.id IS NULL";
$params = ['courseid' => $courseid];
$records = $DB->get_records_sql($sql, $params);
$DB->delete_records_list('quiz_overrides', 'id', array_keys($records));
// Purge cache for each record.
foreach ($records as $record) {
$cache = new override_cache($record->quiz);
$cache->clear_for_group($record->groupid);
}
return array_unique(array_column($records, 'quiz'));
}
}
@@ -0,0 +1,408 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\local\reports;
use coding_exception;
use context_module;
use mod_quiz\quiz_settings;
use moodle_url;
use stdClass;
use table_sql;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir.'/tablelib.php');
/**
* Base class for quiz reports that are basically a table with one row for each attempt.
*
* @package mod_quiz
* @copyright 2010 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class attempts_report extends report_base {
/** @var int default page size for reports. */
const DEFAULT_PAGE_SIZE = 30;
/** @var string constant used for the options, means all users with attempts. */
const ALL_WITH = 'all_with';
/** @var string constant used for the options, means only enrolled users with attempts. */
const ENROLLED_WITH = 'enrolled_with';
/** @var string constant used for the options, means only enrolled users without attempts. */
const ENROLLED_WITHOUT = 'enrolled_without';
/** @var string constant used for the options, means all enrolled users. */
const ENROLLED_ALL = 'enrolled_any';
/** @var string the mode this report is. */
protected $mode;
/** @var context_module the quiz context. */
protected $context;
/** @var attempts_report_options_form The settings form to use. */
protected $form;
/** @var string SQL fragment for selecting the attempt that gave the final grade,
* if applicable. */
protected $qmsubselect;
/** @var boolean caches the results of {@see should_show_grades()}. */
protected $showgrades = null;
/** @var quiz_settings|null quiz settings object. Set in {@see init()}. */
protected $quizobj = null;
/**
* Can be used in subclasses to cache this information, but it will only get set if you set it.
* @example an example use in quiz_overview_report.
*
* @var bool
*/
protected $hasgroupstudents;
/**
* Initialise various aspects of this report.
*
* @param string $mode
* @param string $formclass
* @param stdClass $quiz
* @param stdClass $cm
* @param stdClass $course
* @return array with four elements:
* 0 => integer the current group id (0 for none).
* 1 => \core\dml\sql_join Contains joins, wheres, params for all the students in this course.
* 2 => \core\dml\sql_join Contains joins, wheres, params for all the students in the current group.
* 3 => \core\dml\sql_join Contains joins, wheres, params for all the students to show in the report.
* Will be the same as either element 1 or 2.
*/
public function init($mode, $formclass, $quiz, $cm, $course): array {
$this->mode = $mode;
$this->quizobj = new quiz_settings($quiz, $cm, $course);
$this->context = $this->quizobj->get_context();
[$currentgroup, $studentsjoins, $groupstudentsjoins, $allowedjoins] = $this->get_students_joins(
$cm, $course);
$this->qmsubselect = quiz_report_qm_filter_select($quiz);
$this->form = new $formclass($this->get_base_url(),
['quiz' => $quiz, 'currentgroup' => $currentgroup, 'context' => $this->context]);
return [$currentgroup, $studentsjoins, $groupstudentsjoins, $allowedjoins];
}
/**
* Get the base URL for this report.
* @return moodle_url the URL.
*/
protected function get_base_url() {
return new moodle_url('/mod/quiz/report.php',
['id' => $this->context->instanceid, 'mode' => $this->mode]);
}
/**
* Get sql fragments (joins) which can be used to build queries that
* will select an appropriate set of students to show in the reports.
*
* @param stdClass $cm the course module.
* @param stdClass $course the course settings.
* @return array with four elements:
* 0 => integer the current group id (0 for none).
* 1 => \core\dml\sql_join Contains joins, wheres, params for all the students in this course.
* 2 => \core\dml\sql_join Contains joins, wheres, params for all the students in the current group.
* 3 => \core\dml\sql_join Contains joins, wheres, params for all the students to show in the report.
* Will be the same as either element 1 or 2.
*/
protected function get_students_joins($cm, $course = null) {
$currentgroup = $this->get_current_group($cm, $course, $this->context);
$empty = new \core\dml\sql_join();
if ($currentgroup == self::NO_GROUPS_ALLOWED) {
return [$currentgroup, $empty, $empty, $empty];
}
$studentsjoins = get_enrolled_with_capabilities_join($this->context, '',
['mod/quiz:attempt', 'mod/quiz:reviewmyattempts']);
if (empty($currentgroup)) {
return [$currentgroup, $studentsjoins, $empty, $studentsjoins];
}
// We have a currently selected group.
$groupstudentsjoins = get_enrolled_with_capabilities_join($this->context, '',
['mod/quiz:attempt', 'mod/quiz:reviewmyattempts'], $currentgroup);
return [$currentgroup, $studentsjoins, $groupstudentsjoins, $groupstudentsjoins];
}
/**
* Outputs the things you commonly want at the top of a quiz report.
*
* Calls through to {@see print_header_and_tabs()} and then
* outputs the standard group selector, number of attempts summary,
* and messages to cover common cases when the report can't be shown.
*
* @param stdClass $cm the course_module information.
* @param stdClass $course the course settings.
* @param stdClass $quiz the quiz settings.
* @param attempts_report_options $options the current report settings.
* @param int $currentgroup the current group.
* @param bool $hasquestions whether there are any questions in the quiz.
* @param bool $hasstudents whether there are any relevant students.
*/
protected function print_standard_header_and_messages($cm, $course, $quiz,
$options, $currentgroup, $hasquestions, $hasstudents) {
global $OUTPUT;
$this->print_header_and_tabs($cm, $course, $quiz, $this->mode);
if (groups_get_activity_groupmode($cm)) {
// Groups are being used, so output the group selector if we are not downloading.
groups_print_activity_menu($cm, $options->get_url());
}
// Print information on the number of existing attempts.
if ($strattemptnum = quiz_num_attempt_summary($quiz, $cm, true, $currentgroup)) {
echo '<div class="quizattemptcounts">' . $strattemptnum . '</div>';
}
if (!$hasquestions) {
echo quiz_no_questions_message($quiz, $cm, $this->context);
} else if ($currentgroup == self::NO_GROUPS_ALLOWED) {
echo $OUTPUT->notification(get_string('notingroup'));
} else if (!$hasstudents) {
echo $OUTPUT->notification(get_string('nostudentsyet'));
} else if ($currentgroup && !$this->hasgroupstudents) {
echo $OUTPUT->notification(get_string('nostudentsingroup'));
}
}
/**
* Add all the user-related columns to the $columns and $headers arrays.
* @param table_sql $table the table being constructed.
* @param array $columns the list of columns. Added to.
* @param array $headers the columns headings. Added to.
*/
protected function add_user_columns($table, &$columns, &$headers) {
global $CFG;
if (!$table->is_downloading() && $CFG->grade_report_showuserimage) {
$columns[] = 'picture';
$headers[] = '';
}
if (!$table->is_downloading()) {
$columns[] = 'fullname';
$headers[] = get_string('name');
} else {
$columns[] = 'lastname';
$headers[] = get_string('lastname');
$columns[] = 'firstname';
$headers[] = get_string('firstname');
}
$extrafields = \core_user\fields::get_identity_fields($this->context);
foreach ($extrafields as $field) {
$columns[] = $field;
$headers[] = \core_user\fields::get_display_name($field);
}
}
/**
* Set the display options for the user-related columns in the table.
* @param table_sql $table the table being constructed.
*/
protected function configure_user_columns($table) {
$table->column_suppress('picture');
$table->column_suppress('fullname');
$extrafields = \core_user\fields::get_identity_fields($this->context);
foreach ($extrafields as $field) {
$table->column_suppress($field);
}
$table->column_class('picture', 'picture');
$table->column_class('lastname', 'bold');
$table->column_class('firstname', 'bold');
$table->column_class('fullname', 'bold');
$table->column_sticky('fullname');
}
/**
* Add the state column to the $columns and $headers arrays.
* @param array $columns the list of columns. Added to.
* @param array $headers the columns headings. Added to.
*/
protected function add_state_column(&$columns, &$headers) {
global $PAGE;
$columns[] = 'state';
$headers[] = get_string('attemptstate', 'quiz');
$PAGE->requires->js_call_amd('mod_quiz/reopen_attempt_ui', 'init');
}
/**
* Add all the time-related columns to the $columns and $headers arrays.
* @param array $columns the list of columns. Added to.
* @param array $headers the columns headings. Added to.
*/
protected function add_time_columns(&$columns, &$headers) {
$columns[] = 'timestart';
$headers[] = get_string('startedon', 'quiz');
$columns[] = 'timefinish';
$headers[] = get_string('timecompleted', 'quiz');
$columns[] = 'duration';
$headers[] = get_string('attemptduration', 'quiz');
}
/**
* Add a column for the overall quiz grade and the overall feedback, if applicable.
*
* @param stdClass $quiz the quiz settings.
* @param bool $usercanseegrades whether the user is allowed to see grades for this quiz.
* @param array $columns the list of columns. Added to.
* @param array $headers the columns headings. Added to.
* @param bool $includefeedback whether to include the feedbacktext columns
*/
protected function add_grade_columns($quiz, $usercanseegrades, &$columns, &$headers, $includefeedback = true) {
if ($usercanseegrades) {
$columns[] = 'sumgrades';
$headers[] = get_string('gradenoun') . '/' .
quiz_format_grade($quiz, $quiz->grade);
}
if ($includefeedback && quiz_has_feedback($quiz)) {
$columns[] = 'feedbacktext';
$headers[] = get_string('feedback', 'quiz');
}
}
/**
* Add columns for any extra quiz grade items, if applicable.
*
* @param bool $usercanseegrades whether the user is allowed to see grades for this quiz.
* @param array $columns the list of columns. Added to.
* @param array $headers the columns headings. Added to.
*/
protected function add_grade_item_columns(bool $usercanseegrades, array &$columns, array &$headers) {
if (!$usercanseegrades) {
return;
}
$gradeitems = $this->quizobj->get_grade_calculator()->get_grade_items();
if (!$gradeitems) {
return;
}
foreach ($gradeitems as $gradeitem) {
$columns[] = 'marks' . $gradeitem->id;
$headers[] = format_string($gradeitem->name) . '/' .
quiz_format_grade($this->quizobj->get_quiz(), $gradeitem->maxmark);
}
}
/**
* Set up the table.
* @param attempts_report_table $table the table being constructed.
* @param array $columns the list of columns.
* @param array $headers the columns headings.
* @param moodle_url $reporturl the URL of this report.
* @param attempts_report_options $options the display options.
* @param bool $collapsible whether to allow columns in the report to be collapsed.
*/
protected function set_up_table_columns($table, $columns, $headers, $reporturl,
attempts_report_options $options, $collapsible) {
$table->set_quiz_setting($this->quizobj);
$table->define_columns($columns);
$table->define_headers($headers);
$table->sortable(true, 'uniqueid');
$table->define_baseurl($options->get_url());
$this->configure_user_columns($table);
$table->no_sorting('feedbacktext');
$table->column_class('sumgrades', 'bold');
$table->set_attribute('id', 'attempts');
$table->collapsible($collapsible);
}
/**
* Process any submitted actions.
* @param stdClass $quiz the quiz settings.
* @param stdClass $cm the cm object for the quiz.
* @param int $currentgroup the currently selected group.
* @param \core\dml\sql_join $groupstudentsjoins (joins, wheres, params) the students in the current group.
* @param \core\dml\sql_join $allowedjoins (joins, wheres, params) the users whose attempt this user is allowed to modify.
* @param moodle_url $redirecturl where to redircet to after a successful action.
*/
protected function process_actions($quiz, $cm, $currentgroup, \core\dml\sql_join $groupstudentsjoins,
\core\dml\sql_join $allowedjoins, $redirecturl) {
if (empty($currentgroup) || $this->hasgroupstudents) {
if (optional_param('delete', 0, PARAM_BOOL) && confirm_sesskey()) {
if ($attemptids = optional_param_array('attemptid', [], PARAM_INT)) {
require_capability('mod/quiz:deleteattempts', $this->context);
$this->delete_selected_attempts($quiz, $cm, $attemptids, $allowedjoins);
redirect($redirecturl);
}
}
}
}
/**
* Delete the quiz attempts
* @param stdClass $quiz the quiz settings. Attempts that don't belong to
* this quiz are not deleted.
* @param stdClass $cm the course_module object.
* @param array $attemptids the list of attempt ids to delete.
* @param \core\dml\sql_join $allowedjoins (joins, wheres, params) This list of userids that are visible in the report.
* Users can only delete attempts that they are allowed to see in the report.
* Empty means all users.
*/
protected function delete_selected_attempts($quiz, $cm, $attemptids, \core\dml\sql_join $allowedjoins) {
global $DB;
foreach ($attemptids as $attemptid) {
if (empty($allowedjoins->joins)) {
$sql = "SELECT quiza.*
FROM {quiz_attempts} quiza
JOIN {user} u ON u.id = quiza.userid
WHERE quiza.id = :attemptid";
} else {
$sql = "SELECT quiza.*
FROM {quiz_attempts} quiza
JOIN {user} u ON u.id = quiza.userid
{$allowedjoins->joins}
WHERE {$allowedjoins->wheres} AND quiza.id = :attemptid";
}
$params = $allowedjoins->params + ['attemptid' => $attemptid];
$attempt = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE);
if (!$attempt || $attempt->quiz != $quiz->id || $attempt->preview != 0) {
// Ensure the attempt exists, belongs to this quiz and belongs to
// a student included in the report. If not skip.
continue;
}
// Set the course module id before calling quiz_delete_attempt().
$quiz->cmid = $cm->id;
quiz_delete_attempt($attempt, $quiz);
}
}
}
@@ -0,0 +1,286 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\local\reports;
use context_module;
use mod_quiz\quiz_attempt;
use moodle_url;
use stdClass;
/**
* Base class for the options that control what is visible in an {@see attempts_report}.
*
* @package mod_quiz
* @copyright 2012 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class attempts_report_options {
/** @var string the report mode. */
public $mode;
/** @var stdClass the settings for the quiz being reported on. */
public $quiz;
/** @var stdClass the course module objects for the quiz being reported on. */
public $cm;
/** @var stdClass the course settings for the course the quiz is in. */
public $course;
/**
* @var array form field name => corresponding quiz_attempt:: state constant.
*/
protected static $statefields = [
'stateinprogress' => quiz_attempt::IN_PROGRESS,
'stateoverdue' => quiz_attempt::OVERDUE,
'statefinished' => quiz_attempt::FINISHED,
'stateabandoned' => quiz_attempt::ABANDONED,
];
/**
* @var string attempts_report::ALL_WITH, attempts_report::ENROLLED_WITH,
* attempts_report::ENROLLED_WITHOUT or attempts_report::ENROLLED_ALL
*/
public $attempts = attempts_report::ENROLLED_WITH;
/** @var int the currently selected group. 0 if no group is selected. */
public $group = 0;
/**
* @var array|null of quiz_attempt::IN_PROGRESS, etc. constants. null means
* no restriction.
*/
public $states = [quiz_attempt::IN_PROGRESS, quiz_attempt::OVERDUE,
quiz_attempt::FINISHED, quiz_attempt::ABANDONED];
/**
* @var bool whether to show all finished attmepts, or just the one that gave
* the final grade for the user.
*/
public $onlygraded = false;
/** @var int Number of attempts to show per page. */
public $pagesize = attempts_report::DEFAULT_PAGE_SIZE;
/** @var string whether the data should be downloaded in some format, or '' to display it. */
public $download = '';
/** @var bool whether the current user has permission to see grades. */
public $usercanseegrades;
/** @var bool whether the report table should have a column of checkboxes. */
public $checkboxcolumn = false;
/**
* Constructor.
*
* @param string $mode which report these options are for.
* @param stdClass $quiz the settings for the quiz being reported on.
* @param stdClass $cm the course module objects for the quiz being reported on.
* @param stdClass $course the course settings for the coures this quiz is in.
*/
public function __construct($mode, $quiz, $cm, $course) {
$this->mode = $mode;
$this->quiz = $quiz;
$this->cm = $cm;
$this->course = $course;
$this->usercanseegrades = quiz_report_should_show_grades($quiz, context_module::instance($cm->id));
}
/**
* Get the URL parameters required to show the report with these options.
* @return array URL parameter name => value.
*/
protected function get_url_params() {
$params = [
'id' => $this->cm->id,
'mode' => $this->mode,
'attempts' => $this->attempts,
'onlygraded' => $this->onlygraded,
];
if ($this->states) {
$params['states'] = implode('-', $this->states);
}
if (groups_get_activity_groupmode($this->cm, $this->course)) {
$params['group'] = $this->group;
}
return $params;
}
/**
* Get the URL to show the report with these options.
* @return moodle_url the URL.
*/
public function get_url() {
return new moodle_url('/mod/quiz/report.php', $this->get_url_params());
}
/**
* Process the data we get when the settings form is submitted. This includes
* updating the fields of this class, and updating the user preferences
* where appropriate.
* @param stdClass $fromform The data from $mform->get_data() from the settings form.
*/
public function process_settings_from_form($fromform) {
$this->setup_from_form_data($fromform);
$this->resolve_dependencies();
$this->update_user_preferences();
}
/**
* Set up this preferences object using optional_param (using user_preferences
* to set anything not specified by the params.
*/
public function process_settings_from_params() {
$this->setup_from_user_preferences();
$this->setup_from_params();
$this->resolve_dependencies();
}
/**
* Get the current value of the settings to pass to the settings form.
*/
public function get_initial_form_data() {
$toform = new stdClass();
$toform->attempts = $this->attempts;
$toform->onlygraded = $this->onlygraded;
$toform->pagesize = $this->pagesize;
if ($this->states) {
foreach (self::$statefields as $field => $state) {
$toform->$field = in_array($state, $this->states);
}
}
return $toform;
}
/**
* Set the fields of this object from the form data.
* @param stdClass $fromform The data from $mform->get_data() from the settings form.
*/
public function setup_from_form_data($fromform) {
$this->attempts = $fromform->attempts;
$this->group = groups_get_activity_group($this->cm, true);
$this->onlygraded = !empty($fromform->onlygraded);
$this->pagesize = $fromform->pagesize;
$this->states = [];
foreach (self::$statefields as $field => $state) {
if (!empty($fromform->$field)) {
$this->states[] = $state;
}
}
}
/**
* Set the fields of this object from the URL parameters.
*/
public function setup_from_params() {
$this->attempts = optional_param('attempts', $this->attempts, PARAM_ALPHAEXT);
$this->group = groups_get_activity_group($this->cm, true);
$this->onlygraded = optional_param('onlygraded', $this->onlygraded, PARAM_BOOL);
$this->pagesize = optional_param('pagesize', $this->pagesize, PARAM_INT);
$states = optional_param('states', '', PARAM_ALPHAEXT);
if (!empty($states)) {
$this->states = explode('-', $states);
}
$this->download = optional_param('download', $this->download, PARAM_ALPHA);
}
/**
* Set the fields of this object from the user's preferences.
* (For those settings that are backed by user-preferences).
*/
public function setup_from_user_preferences() {
$this->pagesize = get_user_preferences('quiz_report_pagesize', $this->pagesize);
}
/**
* Update the user preferences so they match the settings in this object.
* (For those settings that are backed by user-preferences).
*/
public function update_user_preferences() {
set_user_preference('quiz_report_pagesize', $this->pagesize);
}
/**
* Check the settings, and remove any 'impossible' combinations.
*/
public function resolve_dependencies() {
if ($this->group) {
// Default for when a group is selected.
if ($this->attempts === null || $this->attempts == attempts_report::ALL_WITH) {
$this->attempts = attempts_report::ENROLLED_WITH;
}
} else if (!$this->group && $this->course->id == SITEID) {
// Force report on front page to show all, unless a group is selected.
$this->attempts = attempts_report::ALL_WITH;
} else if (!in_array($this->attempts, [attempts_report::ALL_WITH, attempts_report::ENROLLED_WITH,
attempts_report::ENROLLED_WITHOUT, attempts_report::ENROLLED_ALL])) {
$this->attempts = attempts_report::ENROLLED_WITH;
}
$cleanstates = [];
foreach (self::$statefields as $state) {
if (in_array($state, $this->states)) {
$cleanstates[] = $state;
}
}
$this->states = $cleanstates;
if (count($this->states) == count(self::$statefields)) {
// If all states have been selected, then there is no constraint
// required in the SQL, so clear the array.
$this->states = null;
}
if (!quiz_report_can_filter_only_graded($this->quiz)) {
// A grading mode like 'average' has been selected, so we cannot do
// the show the attempt that gave the final grade thing.
$this->onlygraded = false;
}
if ($this->attempts == attempts_report::ENROLLED_WITHOUT) {
$this->states = null;
$this->onlygraded = false;
}
if (!$this->is_showing_finished_attempts()) {
$this->onlygraded = false;
}
if ($this->pagesize < 1) {
$this->pagesize = attempts_report::DEFAULT_PAGE_SIZE;
}
}
/**
* Whether the options are such that finished attempts are being shown.
* @return boolean
*/
protected function is_showing_finished_attempts() {
return $this->states === null || in_array(quiz_attempt::FINISHED, $this->states);
}
}
@@ -0,0 +1,136 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\local\reports;
use html_writer;
use MoodleQuickForm;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . '/formslib.php');
/**
* Base class for the settings form for {@see attempts_report}s.
*
* @package mod_quiz
* @copyright 2012 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class attempts_report_options_form extends \moodleform {
protected function definition() {
$mform = $this->_form;
$mform->addElement('header', 'preferencespage',
get_string('reportwhattoinclude', 'quiz'));
$this->standard_attempt_fields($mform);
$this->other_attempt_fields($mform);
$mform->addElement('header', 'preferencesuser',
get_string('reportdisplayoptions', 'quiz'));
$this->standard_preference_fields($mform);
$this->other_preference_fields($mform);
$mform->addElement('submit', 'submitbutton',
get_string('showreport', 'quiz'));
}
/**
* Add the standard form fields for selecting which attempts to include in the report.
*
* @param MoodleQuickForm $mform the form we are building.
*/
protected function standard_attempt_fields(MoodleQuickForm $mform) {
$mform->addElement('select', 'attempts', get_string('reportattemptsfrom', 'quiz'), [
attempts_report::ENROLLED_WITH => get_string('reportuserswith', 'quiz'),
attempts_report::ENROLLED_WITHOUT => get_string('reportuserswithout', 'quiz'),
attempts_report::ENROLLED_ALL => get_string('reportuserswithorwithout', 'quiz'),
attempts_report::ALL_WITH => get_string('reportusersall', 'quiz'),
]);
$stategroup = [
$mform->createElement('advcheckbox', 'stateinprogress', '',
get_string('stateinprogress', 'quiz')),
$mform->createElement('advcheckbox', 'stateoverdue', '',
get_string('stateoverdue', 'quiz')),
$mform->createElement('advcheckbox', 'statefinished', '',
get_string('statefinished', 'quiz')),
$mform->createElement('advcheckbox', 'stateabandoned', '',
get_string('stateabandoned', 'quiz')),
];
$mform->addGroup($stategroup, 'stateoptions',
get_string('reportattemptsthatare', 'quiz'), [' '], false);
$mform->setDefault('stateinprogress', 1);
$mform->setDefault('stateoverdue', 1);
$mform->setDefault('statefinished', 1);
$mform->setDefault('stateabandoned', 1);
$mform->disabledIf('stateinprogress', 'attempts', 'eq', attempts_report::ENROLLED_WITHOUT);
$mform->disabledIf('stateoverdue', 'attempts', 'eq', attempts_report::ENROLLED_WITHOUT);
$mform->disabledIf('statefinished', 'attempts', 'eq', attempts_report::ENROLLED_WITHOUT);
$mform->disabledIf('stateabandoned', 'attempts', 'eq', attempts_report::ENROLLED_WITHOUT);
if (quiz_report_can_filter_only_graded($this->_customdata['quiz'])) {
$gm = html_writer::tag('span',
quiz_get_grading_option_name($this->_customdata['quiz']->grademethod),
['class' => 'highlight']);
$mform->addElement('advcheckbox', 'onlygraded', '',
get_string('reportshowonlyfinished', 'quiz', $gm));
$mform->disabledIf('onlygraded', 'attempts', 'eq', attempts_report::ENROLLED_WITHOUT);
$mform->disabledIf('onlygraded', 'statefinished', 'notchecked');
}
}
/**
* Extension point to allow subclasses to add their own fields in the attempts section.
*
* @param MoodleQuickForm $mform the form we are building.
*/
protected function other_attempt_fields(MoodleQuickForm $mform) {
}
/**
* Add the standard options fields to the form.
*
* @param MoodleQuickForm $mform the form we are building.
*/
protected function standard_preference_fields(MoodleQuickForm $mform) {
$mform->addElement('text', 'pagesize', get_string('pagesize', 'quiz'));
$mform->setType('pagesize', PARAM_INT);
}
/**
* Extension point to allow subclasses to add their own fields in the options section.
*
* @param MoodleQuickForm $mform the form we are building.
*/
protected function other_preference_fields(MoodleQuickForm $mform) {
}
public function validation($data, $files) {
$errors = parent::validation($data, $files);
if ($data['attempts'] != attempts_report::ENROLLED_WITHOUT && !(
$data['stateinprogress'] || $data['stateoverdue'] || $data['statefinished'] || $data['stateabandoned'])) {
$errors['stateoptions'] = get_string('reportmustselectstate', 'quiz');
}
return $errors;
}
}
@@ -0,0 +1,846 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\local\reports;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir.'/tablelib.php');
use coding_exception;
use context_module;
use html_writer;
use mod_quiz\quiz_attempt;
use mod_quiz\quiz_settings;
use moodle_url;
use popup_action;
use question_state;
use qubaid_condition;
use qubaid_join;
use qubaid_list;
use question_engine_data_mapper;
use stdClass;
/**
* Base class for the table used by a {@see attempts_report}.
*
* @package mod_quiz
* @copyright 2010 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class attempts_report_table extends \table_sql {
public $useridfield = 'userid';
/** @var moodle_url the URL of this report. */
protected $reporturl;
/** @var array the display options. */
protected $displayoptions;
/**
* @var array information about the latest step of each question.
* Loaded by {@see load_question_latest_steps()}, if applicable.
*/
protected $lateststeps = null;
/**
* @var float[][]|null total mark for each grade item. Array question_usage.id => quiz_grade_item.id => mark.
* Loaded by {@see load_grade_item_marks()}, if applicable.
*/
protected ?array $gradeitemtotals = null;
/** @var stdClass the quiz settings for the quiz we are reporting on. */
protected $quiz;
/** @var quiz_settings quiz settings object for this quiz. Gets set in {@see attempts_report::et_up_table_columns()}. */
protected quiz_settings $quizobj;
/** @var context_module the quiz context. */
protected $context;
/** @var string HTML fragment to select the first/best/last attempt, if appropriate. */
protected $qmsubselect;
/** @var stdClass attempts_report_options the options affecting this report. */
protected $options;
/** @var \core\dml\sql_join Contains joins, wheres, params to find students
* in the currently selected group, if applicable.
*/
protected $groupstudentsjoins;
/** @var \core\dml\sql_join Contains joins, wheres, params to find the students in the course. */
protected $studentsjoins;
/** @var array the questions that comprise this quiz. */
protected $questions;
/** @var bool whether to include the column with checkboxes to select each attempt. */
protected $includecheckboxes;
/** @var string The toggle group name for the checkboxes in the checkbox column. */
protected $togglegroup = 'quiz-attempts';
/** @var string strftime format. */
protected $strtimeformat;
/** @var bool|null used by {@see col_state()} to cache the has_capability result. */
protected $canreopen = null;
/**
* Constructor.
*
* @param string $uniqueid
* @param stdClass $quiz
* @param context_module $context
* @param string $qmsubselect
* @param attempts_report_options $options
* @param \core\dml\sql_join $groupstudentsjoins Contains joins, wheres, params
* @param \core\dml\sql_join $studentsjoins Contains joins, wheres, params
* @param array $questions
* @param moodle_url $reporturl
*/
public function __construct($uniqueid, $quiz, $context, $qmsubselect,
attempts_report_options $options, \core\dml\sql_join $groupstudentsjoins, \core\dml\sql_join $studentsjoins,
$questions, $reporturl) {
parent::__construct($uniqueid);
$this->quiz = $quiz;
$this->context = $context;
$this->qmsubselect = $qmsubselect;
$this->groupstudentsjoins = $groupstudentsjoins;
$this->studentsjoins = $studentsjoins;
$this->questions = $questions;
$this->includecheckboxes = $options->checkboxcolumn;
$this->reporturl = $reporturl;
$this->options = $options;
}
/**
* A way for the report to pass in the quiz settings object. Currently done in {@see attempts_report::set_up_table_columns()}.
*
* @param quiz_settings $quizobj
*/
public function set_quiz_setting(quiz_settings $quizobj): void {
$this->quizobj = $quizobj;
}
/**
* Generate the display of the checkbox column.
*
* @param stdClass $attempt the table row being output.
* @return string HTML content to go inside the td.
*/
public function col_checkbox($attempt) {
global $OUTPUT;
if ($attempt->attempt) {
$checkbox = new \core\output\checkbox_toggleall($this->togglegroup, false, [
'id' => "attemptid_{$attempt->attempt}",
'name' => 'attemptid[]',
'value' => $attempt->attempt,
'label' => get_string('selectattempt', 'quiz'),
'labelclasses' => 'accesshide',
]);
return $OUTPUT->render($checkbox);
} else {
return '';
}
}
/**
* Generate the display of the user's picture column.
*
* @param stdClass $attempt the table row being output.
* @return string HTML content to go inside the td.
*/
public function col_picture($attempt) {
global $OUTPUT;
$user = new stdClass();
$additionalfields = explode(',', implode(',', \core_user\fields::get_picture_fields()));
$user = username_load_fields_from_object($user, $attempt, null, $additionalfields);
$user->id = $attempt->userid;
return $OUTPUT->user_picture($user);
}
/**
* Generate the display of the user's full name column.
*
* @param stdClass $attempt the table row being output.
* @return string HTML content to go inside the td.
*/
public function col_fullname($attempt) {
$html = parent::col_fullname($attempt);
if ($this->is_downloading() || empty($attempt->attempt)) {
return $html;
}
return $html . html_writer::empty_tag('br') . html_writer::link(
new moodle_url('/mod/quiz/review.php', ['attempt' => $attempt->attempt]),
get_string('reviewattempt', 'quiz'), ['class' => 'reviewlink']);
}
/**
* Generate the display of the attempt state column.
*
* @param stdClass $attempt the table row being output.
* @return string HTML content to go inside the td.
*/
public function col_state($attempt) {
if (is_null($attempt->attempt)) {
return '-';
}
$display = quiz_attempt::state_name($attempt->state);
if ($this->is_downloading()) {
return $display;
}
$this->canreopen ??= has_capability('mod/quiz:reopenattempts', $this->context);
if ($attempt->state == quiz_attempt::ABANDONED && $this->canreopen) {
$display .= ' ' . html_writer::tag('button', get_string('reopenattempt', 'quiz'), [
'type' => 'button',
'class' => 'btn btn-secondary',
'data-action' => 'reopen-attempt',
'data-attempt-id' => $attempt->attempt,
'data-after-action-url' => $this->reporturl->out_as_local_url(false),
]);
}
return $display;
}
/**
* Generate the display of the start time column.
*
* @param stdClass $attempt the table row being output.
* @return string HTML content to go inside the td.
*/
public function col_timestart($attempt) {
if ($attempt->attempt) {
return userdate($attempt->timestart, $this->strtimeformat);
} else {
return '-';
}
}
/**
* Generate the display of the finish time column.
*
* @param stdClass $attempt the table row being output.
* @return string HTML content to go inside the td.
*/
public function col_timefinish($attempt) {
if ($attempt->attempt && $attempt->timefinish) {
return userdate($attempt->timefinish, $this->strtimeformat);
} else {
return '-';
}
}
/**
* Generate the display of the duration column.
*
* @param stdClass $attempt the table row being output.
* @return string HTML content to go inside the td.
*/
public function col_duration($attempt) {
if ($attempt->timefinish) {
return format_time($attempt->timefinish - $attempt->timestart);
} else {
return '-';
}
}
/**
* Generate the display of the feedback column.
*
* @param stdClass $attempt the table row being output.
* @return string HTML content to go inside the td.
*/
public function col_feedbacktext($attempt) {
if ($attempt->state != quiz_attempt::FINISHED) {
return '-';
}
$feedback = quiz_report_feedback_for_grade(
quiz_rescale_grade($attempt->sumgrades, $this->quiz, false),
$this->quiz->id, $this->context);
if ($this->is_downloading()) {
$feedback = strip_tags($feedback);
}
return $feedback;
}
public function other_cols($colname, $attempt) {
$gradeitemid = $this->is_grade_item_column($colname);
if (!$gradeitemid) {
return parent::other_cols($colname, $attempt);
}
if (isset($this->gradeitemtotals[$attempt->usageid][$gradeitemid])) {
$grade = quiz_format_grade($this->quiz, $this->gradeitemtotals[$attempt->usageid][$gradeitemid]);
return $grade;
} else {
return '-';
}
}
/**
* Is this the column key for an extra grade item column?
*
* @param string $columnname e.g. 'marks123' or 'duration'.
* @return int grade item id if this is a column for showing that grade item grade, else, 0.
*/
protected function is_grade_item_column(string $columnname): int {
if (preg_match('/^marks(\d+)$/', $columnname, $matches)) {
return $matches[1];
}
return 0;
}
public function get_row_class($attempt) {
if ($this->qmsubselect && $attempt->gradedattempt) {
return 'gradedattempt';
} else {
return '';
}
}
/**
* Make a link to review an individual question in a popup window.
*
* @param string $data HTML fragment. The text to make into the link.
* @param stdClass $attempt data for the row of the table being output.
* @param int $slot the number used to identify this question within this usage.
*/
public function make_review_link($data, $attempt, $slot) {
global $OUTPUT, $CFG;
$flag = '';
if ($this->is_flagged($attempt->usageid, $slot)) {
$flag = $OUTPUT->pix_icon('i/flagged', get_string('flagged', 'question'),
'moodle', ['class' => 'questionflag']);
}
$feedbackimg = '';
$state = $this->slot_state($attempt, $slot);
if ($state && $state->is_finished() && $state != question_state::$needsgrading) {
$feedbackimg = $this->icon_for_fraction($this->slot_fraction($attempt, $slot));
}
$output = html_writer::tag('span', $feedbackimg . html_writer::tag('span',
$data, ['class' => $state->get_state_class(true)]) . $flag, ['class' => 'que']);
$reviewparams = ['attempt' => $attempt->attempt, 'slot' => $slot];
if (isset($attempt->try)) {
$reviewparams['step'] = $this->step_no_for_try($attempt->usageid, $slot, $attempt->try);
}
$url = new moodle_url('/mod/quiz/reviewquestion.php', $reviewparams);
$output = $OUTPUT->action_link($url, $output,
new popup_action('click', $url, 'reviewquestion',
['height' => 450, 'width' => 650]),
['title' => get_string('reviewresponse', 'quiz')]);
if (!empty($CFG->enableplagiarism)) {
require_once($CFG->libdir . '/plagiarismlib.php');
$output .= plagiarism_get_links([
'context' => $this->context->id,
'component' => 'qtype_'.$this->questions[$slot]->qtype,
'cmid' => $this->context->instanceid,
'area' => $attempt->usageid,
'itemid' => $slot,
'userid' => $attempt->userid]);
}
return $output;
}
/**
* Get the question attempt state for a particular question in a particular quiz attempt.
*
* @param stdClass $attempt the row data.
* @param int $slot indicates which question.
* @return question_state the state of that question.
*/
protected function slot_state($attempt, $slot) {
$stepdata = $this->lateststeps[$attempt->usageid][$slot];
return question_state::get($stepdata->state);
}
/**
* Work out if a particular question in a particular attempt has been flagged.
*
* @param int $questionusageid used to identify the attempt of interest.
* @param int $slot identifies which question in the attempt to check.
* @return bool true if the question is flagged in the attempt.
*/
protected function is_flagged($questionusageid, $slot) {
$stepdata = $this->lateststeps[$questionusageid][$slot];
return $stepdata->flagged;
}
/**
* Get the mark (out of 1) for the question in a particular slot.
*
* @param stdClass $attempt the row data
* @param int $slot which slot to check.
* @return float the score for this question on a scale of 0 - 1.
*/
protected function slot_fraction($attempt, $slot) {
$stepdata = $this->lateststeps[$attempt->usageid][$slot];
return $stepdata->fraction;
}
/**
* Return an appropriate icon (green tick, red cross, etc.) for a grade.
*
* @param float $fraction grade on a scale 0..1.
* @return string html fragment.
*/
protected function icon_for_fraction($fraction) {
global $OUTPUT;
$feedbackclass = question_state::graded_state_for_fraction($fraction)->get_feedback_class();
return $OUTPUT->pix_icon('i/grade_' . $feedbackclass, get_string($feedbackclass, 'question'),
'moodle', ['class' => 'icon']);
}
/**
* Load any extra data after main query.
*
* At this point you can call {@see get_qubaids_condition} to get the condition
* that limits the query to just the question usages shown in this report page or
* alternatively for all attempts if downloading a full report.
*/
protected function load_extra_data() {
$this->lateststeps = $this->load_question_latest_steps();
}
/**
* Load the total mark for each grade item for each attempt.
*/
protected function load_grade_item_marks(): void {
if (count($this->rawdata) === 0) {
$this->gradeitemtotals = [];
return;
}
$this->gradeitemtotals = $this->quizobj->get_grade_calculator()->load_grade_item_totals(
$this->get_qubaids_condition());
}
/**
* Load information about the latest state of selected questions in selected attempts.
*
* The results are returned as a two-dimensional array $qubaid => $slot => $dataobject.
*
* @param qubaid_condition|null $qubaids used to restrict which usages are included
* in the query. See {@see qubaid_condition}.
* @return array of records. See the SQL in this function to see the fields available.
*/
protected function load_question_latest_steps(qubaid_condition $qubaids = null) {
if ($qubaids === null) {
$qubaids = $this->get_qubaids_condition();
}
$dm = new question_engine_data_mapper();
$latesstepdata = $dm->load_questions_usages_latest_steps(
$qubaids, array_keys($this->questions));
$lateststeps = [];
foreach ($latesstepdata as $step) {
$lateststeps[$step->questionusageid][$step->slot] = $step;
}
return $lateststeps;
}
/**
* Does this report require loading any more data after the main query.
*
* @return bool should {@see query_db()} call {@see load_extra_data}?
*/
protected function requires_extra_data() {
return $this->requires_latest_steps_loaded();
}
/**
* Does this report require the detailed information for each question from the question_attempts_steps table?
*
* @return bool should {@see load_extra_data} call {@see load_question_latest_steps}?
*/
protected function requires_latest_steps_loaded() {
return false;
}
/**
* Is this a column that depends on joining to the latest state information?
*
* If so, return the corresponding slot. If not, return false.
*
* @param string $column a column name
* @return int|false false if no, else a slot.
*/
protected function is_latest_step_column($column) {
return false;
}
/**
* Get any fields that might be needed when sorting on date for a particular slot.
*
* Note: these values are only used for sorting. The values displayed are taken
* from $this->lateststeps loaded in load_extra_data().
*
* @param int $slot the slot for the column we want.
* @param string $alias the table alias for latest state information relating to that slot.
* @return string definitions of extra fields to add to the SELECT list of the query.
*/
protected function get_required_latest_state_fields($slot, $alias) {
return '';
}
/**
* Contruct all the parts of the main database query.
*
* @param \core\dml\sql_join $allowedstudentsjoins (joins, wheres, params) defines allowed users for the report.
* @return array with 4 elements [$fields, $from, $where, $params] that can be used to
* build the actual database query.
*/
public function base_sql(\core\dml\sql_join $allowedstudentsjoins) {
global $DB;
// Please note this uniqueid column is not the same as quiza.uniqueid.
$fields = 'DISTINCT ' . $DB->sql_concat('u.id', "'#'", 'COALESCE(quiza.attempt, 0)') . ' AS uniqueid,';
if ($this->qmsubselect) {
$fields .= "\n(CASE WHEN $this->qmsubselect THEN 1 ELSE 0 END) AS gradedattempt,";
}
$userfieldsapi = \core_user\fields::for_identity($this->context)->with_name()
->excluding('id', 'idnumber', 'picture', 'imagealt', 'institution', 'department', 'email');
$userfields = $userfieldsapi->get_sql('u', true, '', '', false);
$fields .= '
quiza.uniqueid AS usageid,
quiza.id AS attempt,
u.id AS userid,
u.idnumber,
u.picture,
u.imagealt,
u.institution,
u.department,
u.email,' . $userfields->selects . ',
quiza.state,
quiza.sumgrades,
quiza.timefinish,
quiza.timestart,
CASE WHEN quiza.timefinish = 0 THEN null
WHEN quiza.timefinish > quiza.timestart THEN quiza.timefinish - quiza.timestart
ELSE 0 END AS duration';
// To explain that last bit, timefinish can be non-zero and less
// than timestart when you have two load-balanced servers with very
// badly synchronised clocks, and a student does a really quick attempt.
// This part is the same for all cases. Join the users and quiz_attempts tables.
$from = " {user} u";
$from .= "\n{$userfields->joins}";
$from .= "\nLEFT JOIN {quiz_attempts} quiza ON
quiza.userid = u.id AND quiza.quiz = :quizid";
$params = array_merge($userfields->params, ['quizid' => $this->quiz->id]);
if ($this->qmsubselect && $this->options->onlygraded) {
$from .= " AND (quiza.state <> :finishedstate OR $this->qmsubselect)";
$params['finishedstate'] = quiz_attempt::FINISHED;
}
switch ($this->options->attempts) {
case attempts_report::ALL_WITH:
// Show all attempts, including students who are no longer in the course.
$where = 'quiza.id IS NOT NULL AND quiza.preview = 0';
break;
case attempts_report::ENROLLED_WITH:
// Show only students with attempts.
$from .= "\n" . $allowedstudentsjoins->joins;
$where = "quiza.preview = 0 AND quiza.id IS NOT NULL AND " . $allowedstudentsjoins->wheres;
$params = array_merge($params, $allowedstudentsjoins->params);
break;
case attempts_report::ENROLLED_WITHOUT:
// Show only students without attempts.
$from .= "\n" . $allowedstudentsjoins->joins;
$where = "quiza.id IS NULL AND " . $allowedstudentsjoins->wheres;
$params = array_merge($params, $allowedstudentsjoins->params);
break;
case attempts_report::ENROLLED_ALL:
// Show all students with or without attempts.
$from .= "\n" . $allowedstudentsjoins->joins;
$where = "(quiza.preview = 0 OR quiza.preview IS NULL) AND " . $allowedstudentsjoins->wheres;
$params = array_merge($params, $allowedstudentsjoins->params);
break;
}
if ($this->options->states) {
[$statesql, $stateparams] = $DB->get_in_or_equal($this->options->states,
SQL_PARAMS_NAMED, 'state');
$params += $stateparams;
$where .= " AND (quiza.state $statesql OR quiza.state IS NULL)";
}
return [$fields, $from, $where, $params];
}
/**
* Lets subclasses modify the SQL after the count query has been created and before the full query is.
*
* @param string $fields SELECT list.
* @param string $from JOINs part of the SQL.
* @param string $where WHERE clauses.
* @param array $params Query params.
* @return array with 4 elements ($fields, $from, $where, $params) as from base_sql.
*/
protected function update_sql_after_count($fields, $from, $where, $params) {
return [$fields, $from, $where, $params];
}
/**
* Set up the SQL queries (count rows, and get data).
*
* @param \core\dml\sql_join $allowedjoins (joins, wheres, params) defines allowed users for the report.
*/
public function setup_sql_queries($allowedjoins) {
[$fields, $from, $where, $params] = $this->base_sql($allowedjoins);
// The WHERE clause is vital here, because some parts of tablelib.php will expect to
// add bits like ' AND x = 1' on the end, and that needs to leave to valid SQL.
$this->set_count_sql("SELECT COUNT(1) FROM (SELECT $fields FROM $from WHERE $where) temp WHERE 1 = 1", $params);
[$fields, $from, $where, $params] = $this->update_sql_after_count($fields, $from, $where, $params);
$this->set_sql($fields, $from, $where, $params);
}
/**
* Add the information about the latest state of the question with slot
* $slot to the query.
*
* The extra information is added as a join to a
* 'table' with alias qa$slot, with columns that are a union of
* the columns of the question_attempts and question_attempts_states tables.
*
* @param int $slot the question to add information for.
*/
protected function add_latest_state_join($slot) {
$alias = 'qa' . $slot;
$fields = $this->get_required_latest_state_fields($slot, $alias);
if (!$fields) {
return;
}
// This condition roughly filters the list of attempts to be considered.
// It is only used in a sub-select to help database query optimisers (see MDL-30122).
// Therefore, it is better to use a very simple which may include
// too many records, than to do a super-accurate join.
$qubaids = new qubaid_join("{quiz_attempts} {$alias}quiza", "{$alias}quiza.uniqueid",
"{$alias}quiza.quiz = :{$alias}quizid", ["{$alias}quizid" => $this->sql->params['quizid']]);
$dm = new question_engine_data_mapper();
[$inlineview, $viewparams] = $dm->question_attempt_latest_state_view($alias, $qubaids);
$this->sql->fields .= ",\n$fields";
$this->sql->from .= "\nLEFT JOIN $inlineview ON " .
"$alias.questionusageid = quiza.uniqueid AND $alias.slot = :{$alias}slot";
$this->sql->params[$alias . 'slot'] = $slot;
$this->sql->params = array_merge($this->sql->params, $viewparams);
}
/**
* Add a field marks$gradeitemid to the query, with the total score for that grade item.
*
* @param int $gradeitemid the grade item to add information for.
*/
protected function add_grade_item_mark(int $gradeitemid): void {
$dm = new question_engine_data_mapper();
$alias = 'gimarks' . $gradeitemid;
$this->sql->fields .= ",\n(
SELECT SUM({$alias}qas.fraction * {$alias}qa.maxmark) AS summarks
FROM {quiz_slots} {$alias}slot
JOIN {question_attempts} {$alias}qa ON {$alias}qa.slot = {$alias}slot.slot
JOIN {question_attempt_steps} {$alias}qas ON {$alias}qas.questionattemptid = {$alias}qa.id
AND {$alias}qas.sequencenumber = {$dm->latest_step_for_qa_subquery("{$alias}qa.id")}
WHERE {$alias}qa.questionusageid = quiza.uniqueid
AND {$alias}slot.quizgradeitemid = :{$alias}gradeitemid
) AS marks$gradeitemid";
$this->sql->params[$alias . 'gradeitemid'] = $gradeitemid;
}
/**
* Get an appropriate qubaid_condition for loading more data about the attempts we are displaying.
*
* @return qubaid_condition
*/
protected function get_qubaids_condition() {
if (is_null($this->rawdata)) {
throw new coding_exception(
'Cannot call get_qubaids_condition until the main data has been loaded.');
}
if ($this->is_downloading()) {
// We want usages for all attempts.
return new qubaid_join("(
SELECT DISTINCT quiza.uniqueid
FROM " . $this->sql->from . "
WHERE " . $this->sql->where . "
) quizasubquery", 'quizasubquery.uniqueid',
"1 = 1", $this->sql->params);
}
$qubaids = [];
foreach ($this->rawdata as $attempt) {
if ($attempt->usageid > 0) {
$qubaids[] = $attempt->usageid;
}
}
return new qubaid_list($qubaids);
}
public function query_db($pagesize, $useinitialsbar = true) {
$doneslots = [];
$donegradeitems = [];
foreach ($this->get_sort_columns() as $column => $notused) {
$slot = $this->is_latest_step_column($column);
if ($slot && !in_array($slot, $doneslots)) {
$this->add_latest_state_join($slot);
$doneslots[] = $slot;
}
$gradeitemid = $this->is_grade_item_column($column);
if ($gradeitemid && !in_array($gradeitemid, $donegradeitems)) {
$this->add_grade_item_mark($gradeitemid);
$donegradeitems[] = $gradeitemid;
}
}
parent::query_db($pagesize, $useinitialsbar);
// Load grade-item totals if required.
foreach ($this->columns as $columnname => $notused) {
if ($this->is_grade_item_column($columnname)) {
$this->load_grade_item_marks();
break;
}
}
if ($this->requires_extra_data()) {
$this->load_extra_data();
}
}
public function get_sort_columns() {
// Add attemptid as a final tie-break to the sort. This ensures that
// Attempts by the same student appear in order when just sorting by name.
$sortcolumns = parent::get_sort_columns();
$sortcolumns['quiza.id'] = SORT_ASC;
return $sortcolumns;
}
public function wrap_html_start() {
if ($this->is_downloading() || !$this->includecheckboxes) {
return;
}
$url = $this->options->get_url();
$url->param('sesskey', sesskey());
echo '<div id="tablecontainer">';
echo '<form id="attemptsform" method="post" action="' . $url->out_omit_querystring() . '">';
echo html_writer::input_hidden_params($url);
echo '<div>';
}
public function wrap_html_finish() {
global $PAGE;
if ($this->is_downloading() || !$this->includecheckboxes) {
return;
}
echo '<div id="commands">';
$this->submit_buttons();
echo '</div>';
// Close the form.
echo '</div>';
echo '</form></div>';
}
/**
* Output any submit buttons required by the $this->includecheckboxes form.
*/
protected function submit_buttons() {
global $PAGE;
if (has_capability('mod/quiz:deleteattempts', $this->context)) {
$deletebuttonparams = [
'type' => 'submit',
'class' => 'btn btn-secondary mr-1',
'id' => 'deleteattemptsbutton',
'name' => 'delete',
'value' => get_string('deleteselected', 'quiz_overview'),
'data-action' => 'toggle',
'data-togglegroup' => $this->togglegroup,
'data-toggle' => 'action',
'disabled' => true,
'data-modal' => 'confirmation',
'data-modal-type' => 'delete',
'data-modal-content-str' => json_encode(['deleteattemptcheck', 'quiz']),
];
echo html_writer::empty_tag('input', $deletebuttonparams);
}
}
/**
* Generates the contents for the checkbox column header.
*
* It returns the HTML for a master \core\output\checkbox_toggleall component that selects/deselects all quiz attempts.
*
* @param string $columnname The name of the checkbox column.
* @return string
*/
public function checkbox_col_header(string $columnname) {
global $OUTPUT;
// Make sure to disable sorting on this column.
$this->no_sorting($columnname);
// Build the select/deselect all control.
$selectallid = $this->uniqueid . '-selectall-attempts';
$selectalltext = get_string('selectall', 'quiz');
$deselectalltext = get_string('selectnone', 'quiz');
$mastercheckbox = new \core\output\checkbox_toggleall($this->togglegroup, true, [
'id' => $selectallid,
'name' => $selectallid,
'value' => 1,
'label' => $selectalltext,
'labelclasses' => 'accesshide',
'selectall' => $selectalltext,
'deselectall' => $deselectalltext,
]);
return $OUTPUT->render($mastercheckbox);
}
}
@@ -0,0 +1,96 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\local\reports;
use context;
use context_module;
use stdClass;
/**
* Base class for quiz report plugins.
*
* Doesn't do anything on its own -- it needs to be extended.
* This class displays quiz reports. Because it is called from
* within /mod/quiz/report.php you can assume that the page header
* and footer are taken care of.
*
* This file can refer to itself as report.php to pass variables
* to itself - all these will also be globally available. You must
* pass "id=$cm->id" or q=$quiz->id", and "mode=reportname".
*
* @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
*/
abstract class report_base {
/** @var int special value used in place of groupid, to mean the use cannot access any groups. */
const NO_GROUPS_ALLOWED = -2;
/**
* Override this function to display the report.
*
* @param stdClass $quiz this quiz.
* @param stdClass $cm the course-module for this quiz.
* @param stdClass $course the coures we are in.
*/
abstract public function display($quiz, $cm, $course);
/**
* Initialise some parts of $PAGE and start output.
*
* @param stdClass $cm the course_module information.
* @param stdClass $course the course settings.
* @param stdClass $quiz the quiz settings.
* @param string $reportmode the report name.
*/
public function print_header_and_tabs($cm, $course, $quiz, $reportmode = 'overview') {
global $PAGE, $OUTPUT, $CFG;
// Print the page header.
$PAGE->set_title($quiz->name);
$PAGE->set_heading($course->fullname);
echo $OUTPUT->header();
$context = context_module::instance($cm->id);
if (!$PAGE->has_secondary_navigation()) {
echo $OUTPUT->heading(format_string($quiz->name, true, ['context' => $context]));
}
if (!empty($CFG->enableplagiarism)) {
require_once($CFG->libdir . '/plagiarismlib.php');
echo plagiarism_update_status($course, $cm);
}
}
/**
* Get the current group for the user user looking at the report.
*
* @param stdClass $cm the course_module information.
* @param stdClass $course the course settings.
* @param context $context the quiz context.
* @return int the current group id, if applicable. 0 for all users,
* NO_GROUPS_ALLOWED if the user cannot see any group.
*/
public function get_current_group($cm, $course, $context) {
$groupmode = groups_get_activity_groupmode($cm, $course);
$currentgroup = groups_get_activity_group($cm, true);
if ($groupmode == SEPARATEGROUPS && !$currentgroup && !has_capability('moodle/site:accessallgroups', $context)) {
$currentgroup = self::NO_GROUPS_ALLOWED;
}
return $currentgroup;
}
}
@@ -0,0 +1,232 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\local\structure;
use context_module;
/**
* Class slot_random, represents a random question slot type.
*
* @package mod_quiz
* @copyright 2018 Shamim Rezaie <shamim@moodle.com>
* @author 2021 Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class slot_random {
/** @var \stdClass Slot's properties. A record retrieved from the quiz_slots table. */
protected $record;
/**
* @var \stdClass set reference record
*/
protected $referencerecord;
/**
* @var \stdClass The quiz this question slot belongs to.
*/
protected $quiz = null;
/**
* @var \core_tag_tag[] List of tags for this slot.
*/
protected $tags = [];
/**
* @var string filter condition
*/
protected $filtercondition = null;
/**
* slot_random constructor.
*
* @param \stdClass $slotrecord Represents a record in the quiz_slots table.
*/
public function __construct($slotrecord = null) {
$this->record = new \stdClass();
$this->referencerecord = new \stdClass();
$slotproperties = ['id', 'slot', 'quizid', 'page', 'requireprevious', 'maxmark'];
$setreferenceproperties = ['usingcontextid', 'questionscontextid'];
foreach ($slotproperties as $property) {
if (isset($slotrecord->$property)) {
$this->record->$property = $slotrecord->$property;
}
}
foreach ($setreferenceproperties as $referenceproperty) {
if (isset($slotrecord->$referenceproperty)) {
$this->referencerecord->$referenceproperty = $slotrecord->$referenceproperty;
}
}
}
/**
* Returns the quiz for this question slot.
* The quiz is fetched the first time it is requested and then stored in a member variable to be returned each subsequent time.
*
* @return mixed
* @throws \coding_exception
*/
public function get_quiz() {
global $DB;
if (empty($this->quiz)) {
if (empty($this->record->quizid)) {
throw new \coding_exception('quizid is not set.');
}
$this->quiz = $DB->get_record('quiz', ['id' => $this->record->quizid]);
}
return $this->quiz;
}
/**
* Sets the quiz object for the quiz slot.
* It is not mandatory to set the quiz as the quiz slot can fetch it the first time it is accessed,
* however it helps with the performance to set the quiz if you already have it.
*
* @param \stdClass $quiz The qui object.
*/
public function set_quiz($quiz) {
$this->quiz = $quiz;
$this->record->quizid = $quiz->id;
}
/**
* Set some tags for this quiz slot.
*
* @param \core_tag_tag[] $tags
*
* @deprecated since Moodle 4.3
* @todo Final deprecation on Moodle 4.7 MDL-78091
*/
public function set_tags($tags) {
debugging('Method set_tags() is deprecated, ' .
'please do not use this function.', DEBUG_DEVELOPER);
$this->tags = [];
foreach ($tags as $tag) {
// We use $tag->id as the key for the array so not only it handles duplicates of the same tag being given,
// but also it is consistent with the behaviour of set_tags_by_id() below.
$this->tags[$tag->id] = $tag;
}
}
/**
* Set some tags for this quiz slot. This function uses tag ids to find tags.
*
* @param int[] $tagids
* @deprecated since Moodle 4.3
* @todo Final deprecation on Moodle 4.7 MDL-78091
*/
public function set_tags_by_id($tagids) {
debugging(
'Method set_tags_by_id() is deprecated, please do not use this function.',
DEBUG_DEVELOPER
);
$this->tags = \core_tag_tag::get_bulk($tagids, 'id, name');
}
/**
* Set filter condition.
*
* @param \string $filters
*/
public function set_filter_condition(string $filters): void {
$this->filtercondition = $filters;
}
/**
* Inserts the quiz slot at the $page page.
* It is required to call this function if you are building a quiz slot object from scratch.
*
* @param int $page The page that this slot will be inserted at.
*/
public function insert($page) {
global $DB;
$slots = $DB->get_records('quiz_slots', ['quizid' => $this->record->quizid],
'slot', 'id, slot, page');
$quiz = $this->get_quiz();
$trans = $DB->start_delegated_transaction();
$maxpage = 1;
$numonlastpage = 0;
foreach ($slots as $slot) {
if ($slot->page > $maxpage) {
$maxpage = $slot->page;
$numonlastpage = 1;
} else {
$numonlastpage += 1;
}
}
if (is_int($page) && $page >= 1) {
// Adding on a given page.
$lastslotbefore = 0;
foreach (array_reverse($slots) as $otherslot) {
if ($otherslot->page > $page) {
$DB->set_field('quiz_slots', 'slot', $otherslot->slot + 1, ['id' => $otherslot->id]);
} else {
$lastslotbefore = $otherslot->slot;
break;
}
}
$this->record->slot = $lastslotbefore + 1;
$this->record->page = min($page, $maxpage + 1);
quiz_update_section_firstslots($this->record->quizid, 1, max($lastslotbefore, 1));
} else {
$lastslot = end($slots);
if ($lastslot) {
$this->record->slot = $lastslot->slot + 1;
} else {
$this->record->slot = 1;
}
if ($quiz->questionsperpage && $numonlastpage >= $quiz->questionsperpage) {
$this->record->page = $maxpage + 1;
} else {
$this->record->page = $maxpage;
}
}
$this->record->id = $DB->insert_record('quiz_slots', $this->record);
$this->referencerecord->component = 'mod_quiz';
$this->referencerecord->questionarea = 'slot';
$this->referencerecord->itemid = $this->record->id;
$this->referencerecord->filtercondition = $this->filtercondition;
$DB->insert_record('question_set_references', $this->referencerecord);
$trans->allow_commit();
// Log slot created event.
$cm = get_coursemodule_from_instance('quiz', $quiz->id);
$event = \mod_quiz\event\slot_created::create([
'context' => context_module::instance($cm->id),
'objectid' => $this->record->id,
'other' => [
'quizid' => $quiz->id,
'slotnumber' => $this->record->slot,
'page' => $this->record->page
]
]);
$event->trigger();
}
}
@@ -0,0 +1,60 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\navigation\views;
use core\navigation\views\secondary as core_secondary;
/**
* Class secondary_navigation_view.
*
* Custom implementation for a plugin.
*
* @package mod_quiz
* @category navigation
* @copyright 2021 Sujith Haridasan <sujith@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class secondary extends core_secondary {
/**
* Define a custom secondary nav order/view.
*
* @return array
*/
protected function get_default_module_mapping(): array {
$defaultmaping = parent::get_default_module_mapping();
$defaultmaping[self::TYPE_SETTING] = array_merge($defaultmaping[self::TYPE_SETTING], [
'mod_quiz_edit' => 3,
'quiz_report' => 4,
'mod_quiz_useroverrides' => 6,
'roleassign' => 7,
'filtermanage' => 8,
'roleoverride' => 9,
'rolecheck' => 9.1,
'logreport' => 10,
'backup' => 11,
'restore' => 12,
'competencybreakdown' => 13,
]);
$defaultmaping[self::TYPE_CONTAINER] = [
'questionbank' => 5,
];
return $defaultmaping;
}
}
@@ -0,0 +1,346 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\output;
use action_link;
use core\output\named_templatable;
use html_writer;
use mod_quiz\grade_calculator;
use mod_quiz\output\grades\grade_out_of;
use mod_quiz\quiz_attempt;
use moodle_url;
use mod_quiz\question\display_options;
use question_display_options;
use renderable;
use renderer_base;
use stdClass;
use user_picture;
/**
* A summary of a single quiz attempt for rendering.
*
* This is used in places like
* - at the top of the review attempt page (review.php)
* - at the top of the review single question page (reviewquestion.php)
* - on the quiz entry page (view.php).
*
* @package mod_quiz
* @copyright 2024 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class attempt_summary_information implements renderable, named_templatable {
/** @var array[] The rows of summary data. {@see add_item()} should make the structure clear. */
protected array $summarydata = [];
/**
* Add an item to the summary.
*
* @param string $shortname unique identifier of this item (not displayed).
* @param string|renderable $title the title of this item.
* @param string|renderable $content the content of this item.
*/
public function add_item(string $shortname, string|renderable $title, string|renderable $content): void {
$this->summarydata[$shortname] = [
'title' => $title,
'content' => $content,
];
}
/**
* Add an item to the summary just before the given item.
*
* If that item is not present, then add as the first item.
*
* @param string $shortname unique identifier of this item (not displayed).
* @param string|renderable $title the title of this item.
* @param string|renderable $content the content of this item.
* @param string $addbefore identifier of the other item to add this before.
*/
public function add_item_before(
string $shortname,
string|renderable $title,
string|renderable $content,
string $addbefore,
): void {
$position = array_search($addbefore, array_keys($this->summarydata));
if ($position !== false) {
$this->insert_new_item_at_position($shortname, $title, $content, $position);
} else {
$this->insert_new_item_at_position($shortname, $title, $content, 0);
}
}
/**
* Add an item to the summary just after the given item.
*
* If that item is not present, then just add at the end.
*
* @param string $shortname unique identifier of this item (not displayed).
* @param string|renderable $title the title of this item.
* @param string|renderable $content the content of this item.
* @param string $addafter identifier of the other item to add this before.
*/
public function add_item_after(
string $shortname,
string|renderable $title,
string|renderable $content,
string $addafter,
): void {
$position = array_search($addafter, array_keys($this->summarydata));
if ($position !== false) {
$this->insert_new_item_at_position($shortname, $title, $content, $position + 1);
} else {
$this->add_item($shortname, $title, $content);
}
}
/**
* Add an item to the summary just before the given position.
*
* @param string $shortname unique identifier of this item (not displayed).
* @param string|renderable $title the title of this item.
* @param string|renderable $content the content of this item.
* @param int $position Numerical position to insert the item at. 0 means first.
*/
protected function insert_new_item_at_position(
string $shortname,
string|renderable $title,
string|renderable $content,
int $position,
) {
$this->summarydata = array_merge(
array_slice($this->summarydata, 0, $position),
[$shortname => ['title' => $title, 'content' => $content]],
array_slice($this->summarydata, $position, count($this->summarydata)),
);
}
/**
* Remove an item, if present.
*
* @param string $shortname
*/
public function remove_item(string $shortname): void {
unset($this->summarydata[$shortname]);
}
/**
* Filter the data held, to keep only the information with the given shortnames.
*
* @param array $shortnames items to keep.
*/
public function filter_keeping_only(array $shortnames): void {
foreach ($this->summarydata as $shortname => $rowdata) {
if (!in_array($shortname, $shortnames)) {
unset($this->summarydata[$shortname]);
}
}
}
/**
* To aid conversion of old code. This converts the old array format into an instance of this class.
*
* @param array $items array of $shortname => [$title, $content].
* @return static
*/
public static function create_from_legacy_array(array $items): static {
$summary = new static();
foreach ($items as $shortname => $item) {
$summary->add_item($shortname, $item['title'], $item['content']);
}
return $summary;
}
/**
* Initialise an instance of this class for a particular quiz attempt.
*
* @param quiz_attempt $attemptobj the attempt to summarise.
* @param display_options $options options for what can be seen.
* @param int|null $pageforlinkingtootherattempts if null, no links to other attempsts will be created.
* 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.
* @return self summary information.
*/
public static function create_for_attempt(
quiz_attempt $attemptobj,
display_options $options,
?int $pageforlinkingtootherattempts = null,
?bool $showall = null,
): static {
global $DB, $USER;
$summary = new static();
// Prepare summary information about the whole attempt.
if (!$attemptobj->get_quiz()->showuserpicture && $attemptobj->get_userid() != $USER->id) {
// If showuserpicture is true, the picture is shown elsewhere, so don't repeat it.
$student = $DB->get_record('user', ['id' => $attemptobj->get_userid()]);
$userpicture = new user_picture($student);
$userpicture->courseid = $attemptobj->get_courseid();
$summary->add_item('user', $userpicture,
new action_link(
new moodle_url('/user/view.php', ['id' => $student->id, 'course' => $attemptobj->get_courseid()]),
fullname($student, true),
)
);
}
if ($pageforlinkingtootherattempts !== null && $attemptobj->has_capability('mod/quiz:viewreports')) {
$attemptlist = $attemptobj->links_to_other_attempts(
$attemptobj->review_url(null, $pageforlinkingtootherattempts, $showall));
if ($attemptlist) {
$summary->add_item('attemptlist', get_string('attempts', 'quiz'), $attemptlist);
}
}
// Attempt state.
$summary->add_item('state', get_string('attemptstate', 'quiz'),
quiz_attempt::state_name($attemptobj->get_attempt()->state));
// Timing information.
$attempt = $attemptobj->get_attempt();
$quiz = $attemptobj->get_quiz();
$overtime = 0;
if ($attempt->state == quiz_attempt::FINISHED) {
if ($timetaken = ($attempt->timefinish - $attempt->timestart)) {
if ($quiz->timelimit && $timetaken > ($quiz->timelimit + 60)) {
$overtime = $timetaken - $quiz->timelimit;
$overtime = format_time($overtime);
}
$timetaken = format_time($timetaken);
} else {
$timetaken = "-";
}
} else {
$timetaken = get_string('unfinished', 'quiz');
}
$summary->add_item('startedon', get_string('startedon', 'quiz'), userdate($attempt->timestart));
if ($attempt->state == quiz_attempt::FINISHED) {
$summary->add_item('completedon', get_string('completedon', 'quiz'),
userdate($attempt->timefinish));
$summary->add_item('timetaken', get_string('attemptduration', 'quiz'), $timetaken);
}
if (!empty($overtime)) {
$summary->add_item('overdue', get_string('overdue', 'quiz'), $overtime);
}
// Show marks (if the user is allowed to see marks at the moment).
$grade = $summary->add_attempt_grades_if_appropriate($attemptobj, $options);
// Any additional summary data from the behaviour.
foreach ($attemptobj->get_additional_summary_data($options) as $shortname => $data) {
$summary->add_item($shortname, $data['title'], $data['content']);
}
// Feedback if there is any, and the user is allowed to see it now.
$feedback = $attemptobj->get_overall_feedback($grade);
if ($options->overallfeedback && $feedback) {
$summary->add_item('feedback', get_string('feedback', 'quiz'), $feedback);
}
return $summary;
}
/**
* Add the grade information to this summary information.
*
* This is a helper used by {@see create_for_attempt()}.
*
* @param quiz_attempt $attemptobj the attempt to summarise.
* @param display_options $options options for what can be seen.
* @return float|null the overall attempt grade, if it exists, else null. Raw value, not formatted.
*/
public function add_attempt_grades_if_appropriate(
quiz_attempt $attemptobj,
display_options $options,
): ?float {
$quiz = $attemptobj->get_quiz();
$grade = quiz_rescale_grade($attemptobj->get_sum_marks(), $quiz, false);
if ($options->marks < question_display_options::MARK_AND_MAX) {
// User can't see grades.
return $grade;
}
if (!quiz_has_grades($quiz) || $attemptobj->get_state() != quiz_attempt::FINISHED) {
// No grades to show.
return $grade;
}
if (is_null($grade)) {
// Attempt needs ot be graded.
$this->add_item('grade', get_string('gradenoun'), quiz_format_grade($quiz, $grade));
return $grade;
}
// Grades for extra grade items, if any.
foreach ($attemptobj->get_grade_item_totals() as $gradeitemid => $gradeoutof) {
$this->add_item('marks' . $gradeitemid, format_string($gradeoutof->name), $gradeoutof);
}
// Show raw marks only if they are different from the grade.
if ($quiz->grade != $quiz->sumgrades) {
$this->add_item('marks', get_string('marks', 'quiz'),
new grade_out_of($quiz, $attemptobj->get_sum_marks(), $quiz->sumgrades, style: grade_out_of::SHORT));
}
// Now the scaled grade.
$this->add_item('grade', get_string('gradenoun'),
new grade_out_of($quiz, $grade, $quiz->grade,
style: abs($quiz->grade - 100) < grade_calculator::ALMOST_ZERO ?
grade_out_of::NORMAL : grade_out_of::WITH_PERCENT));
return $grade;
}
public function export_for_template(renderer_base $output): array {
$templatecontext = [
'hasitems' => !empty($this->summarydata),
'items' => [],
];
foreach ($this->summarydata as $item) {
if ($item['title'] instanceof renderable) {
$title = $output->render($item['title']);
} else {
$title = $item['title'];
}
if ($item['content'] instanceof renderable) {
$content = $output->render($item['content']);
} else {
$content = $item['content'];
}
$templatecontext['items'][] = (object) ['title' => $title, 'content' => $content];
}
return $templatecontext;
}
public function get_template_name(renderer_base $renderer): string {
// Only reason we are forced to implement this is that we want the quiz renderer
// passed to export_for_template, not a core_renderer.
return 'mod_quiz/attempt_summary_information';
}
}
@@ -0,0 +1,123 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\output;
use mod_quiz\structure;
use renderable;
use renderer_base;
use templatable;
/**
* Represents the page where teachers can set up additional grade items.
*
* @package mod_quiz
* @category output
* @copyright 2023 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class edit_grading_page implements renderable, templatable {
/**
* Constructor.
*
* @param structure $structure
*/
public function __construct(
/** @var structure structure of the quiz for which to display the grade edit page. */
protected readonly structure $structure,
) {
}
public function export_for_template(renderer_base $output) {
global $PAGE;
/** @var edit_renderer $editrenderer */
$editrenderer = $PAGE->get_renderer('mod_quiz', 'edit');
// Get the list of grade items, but to be the choices for a slot, and the list to be edited.
$gradeitemchoices = [
0 => [
'id' => 0,
'choice' => get_string('gradeitemnoneselected', 'quiz'),
'isselected' => false,
],
];
$selectdgradeitemchoices = [];
$gradeitems = [];
foreach ($this->structure->get_grade_items() as $gradeitem) {
$gradeitem = clone($gradeitem);
unset($gradeitem->quizid);
$gradeitem->displayname = format_string($gradeitem->name);
$gradeitem->isused = $this->structure->is_grade_item_used($gradeitem->id);
$gradeitem->summarks = $gradeitem->isused ?
$this->structure->formatted_grade_item_sum_marks($gradeitem->id) :
'-';
$gradeitems[] = $gradeitem;
$gradeitemchoices[$gradeitem->id] = (object) [
'id' => $gradeitem->id,
'choice' => $gradeitem->displayname,
'isselected' => false,
];
$selectdgradeitemchoices[$gradeitem->id] = clone($gradeitemchoices[$gradeitem->id]);
$selectdgradeitemchoices[$gradeitem->id]->isselected = true;
}
// Get the list of quiz sections.
$sections = [];
foreach ($this->structure->get_sections() as $section) {
$sections[$section->id] = (object) [
'displayname' => $section->heading ? format_string($section->heading) : get_string('sectionnoname', 'quiz'),
'slots' => [],
];
}
// Add the relevant slots ot each section.
foreach ($this->structure->get_slots() as $slot) {
if (!$this->structure->is_real_question($slot->slot)) {
continue;
}
// Mark the right choice as selected.
$choices = $gradeitemchoices;
if ($slot->quizgradeitemid) {
$choices[$slot->quizgradeitemid] = $selectdgradeitemchoices[$slot->quizgradeitemid];
}
$sections[$slot->section->id]->slots[] = (object) [
'id' => $slot->id,
'displaynumber' => $this->structure->get_displayed_number_for_slot($slot->slot),
'displayname' => $editrenderer->get_question_name_for_slot(
$this->structure, $slot->slot, $PAGE->url),
'maxmark' => $this->structure->formatted_question_grade($slot->slot),
'choices' => array_values($choices),
];
}
return [
'quizid' => $this->structure->get_quizid(),
'hasgradeitems' => !empty($gradeitems),
'gradeitems' => $gradeitems,
'hasslots' => $this->structure->has_questions(),
'sections' => array_values($sections),
'hasmultiplesections' => count($sections) > 1,
'nogradeitems' => ['message' => get_string('gradeitemsnoneyet', 'quiz')],
'noslots' => ['message' => get_string('gradeitemnoslots', 'quiz')],
];
}
}
@@ -0,0 +1,78 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\output;
use moodle_url;
use renderable;
use renderer_base;
use templatable;
use url_select;
/**
* Represents the tertiary navigation around the quiz edit pages.
*
* @package mod_quiz
* @copyright 2023 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class edit_nav_actions implements renderable, templatable {
/** @var string option for $whichpage argument to the constructor. */
const SUMMARY = 'summary';
/** @var string option for $whichpage argument to the constructor. */
const GRADING = 'grading';
/**
* overrides_action constructor.
*
* @param int $cmid The course module id.
* @param string $whichpage self::SUMMARY (edit.php) or self::GRADING (editgrading.php).
*/
public function __construct(
/** @var int The course module ID. */
protected readonly int $cmid,
/** @var string which page this is. Either self::SUMMARY (edit.php) or self::GRADING (editgrading.php). */
protected readonly string $whichpage,
) {
}
public function export_for_template(renderer_base $output): array {
// Build the navigation drop-down.
$questionsurl = new moodle_url('/mod/quiz/edit.php', ['cmid' => $this->cmid]);
$gradeitemsetupurl = new moodle_url('/mod/quiz/editgrading.php', ['cmid' => $this->cmid]);
$menu = [
$questionsurl->out(false) => get_string('questions', 'quiz'),
$gradeitemsetupurl->out(false) => get_string('gradeitemsetup', 'quiz'),
];
$overridesnav = new url_select(
$menu,
$this->whichpage === self::SUMMARY ? $questionsurl->out(false) : $gradeitemsetupurl->out(false),
null
);
$overridesnav->set_label(get_string('quizsetupnavigation', 'quiz'), ['class' => 'sr-only']);
return [
'navmenu' => $overridesnav->export_for_template($output),
];
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,112 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\output\grades;
use html_writer;
use renderable;
use stdClass;
/**
* Represents a grade out of a give total, that wants to be output in a particular way.
*
* @package mod_quiz
* @category output
* @copyright 2024 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class grade_out_of implements renderable {
/** @var string Indicates we want a short rendering. Also the lang string used. */
const SHORT = 'outofshort';
/** @var string Indicates we want the default rendering. Also the lang string used. */
const NORMAL = 'outof';
/** @var string like normal, but with the percent equivalent in brackets. Also the lang string used */
const WITH_PERCENT = 'outofpercent';
/**
* Constructor.
*
* @param stdClass $quiz Quiz settings.
* @param float $grade the mark to show.
* @param float $maxgrade the total to show it out of.
* @param string|null $name optional, a name for what this grade is.
* @param string $style which format to use, grade_out_of::NORMAL (default), ::SHORT or ::WITH_PERCENT.
*/
public function __construct(
/** @var stdClass Quiz settings (so we can access the settings like decimal places). */
public readonly stdClass $quiz,
/** @var float the grade to show. */
public float $grade,
/** @var float the total the grade is out of. */
public float $maxgrade,
/** @var string|null optional, a name for what this grade is. Must be output via format_string. */
public readonly ?string $name = null,
/** @var string The display style, one of the consts above. */
public readonly string $style = self::NORMAL,
) {
}
/**
* Get the lang string to use to display the grade in the requested style.
*
* @return string lang string key from the mod_quiz lang pack.
*/
public function get_string_key(): string {
return $this->style;
}
/**
* Get the formatted values to be inserted into the {@see get_string_key()} string placeholders.
*
* Values are not styled. To apply the recommended styling, call {@see style_formatted_values()}
*
* @return stdClass to be passed as the third argument to get_string().
*/
public function get_formatted_values(): stdClass {
$a = new stdClass();
$a->grade = quiz_format_grade($this->quiz, $this->grade);
$a->maxgrade = quiz_format_grade($this->quiz, $this->maxgrade);
if ($this->style === self::WITH_PERCENT) {
$a->percent = format_float($this->grade * 100 / $this->maxgrade,
$this->quiz->decimalpoints, true, true);
}
return $a;
}
/**
* Apply the normal styling to the values returned by {@see get_formatted_values()}.
*
* @param stdClass $a formatted values, as returned by get_formatted_values.
* @return stdClass same structure, with some values wrapped in &lt;b> tags.
*/
public function style_formatted_values(stdClass $a): stdClass {
if ($this->style !== self::SHORT) {
$a->grade = html_writer::tag('b', $a->grade);
}
if ($this->style === self::WITH_PERCENT) {
$a->percent = html_writer::tag('b', $a->percent);
}
return $a;
}
}

Some files were not shown because too many files have changed in this diff Show More