Merge branch 'MDL-86378-502' of https://github.com/catalyst/moodle

This commit is contained in:
Safat
2026-03-13 13:45:13 +11:00
44 changed files with 1032 additions and 19 deletions
@@ -176,8 +176,17 @@ class backup_assign_activity_structure_step extends backup_activity_structure_st
'value'));
$overrides = new backup_nested_element('overrides');
$override = new backup_nested_element('override', array('id'), array(
'groupid', 'userid', 'sortorder', 'allowsubmissionsfromdate', 'duedate', 'cutoffdate', 'timelimit'));
$override = new backup_nested_element('override', ['id'], [
'groupid',
'userid',
'sortorder',
'allowsubmissionsfromdate',
'duedate',
'cutoffdate',
'timelimit',
'reason',
'reasonformat',
]);
// Build the tree.
$assign->add_child($userflags);
+2 -2
View File
@@ -74,7 +74,7 @@ class overrides implements data_source_interface {
$override = $DB->get_record(
'assign_overrides',
['assignid' => $assignid, 'userid' => $userid],
'duedate, cutoffdate, allowsubmissionsfromdate'
'duedate, cutoffdate, allowsubmissionsfromdate, reason, reasonformat'
);
break;
case 'g':
@@ -82,7 +82,7 @@ class overrides implements data_source_interface {
$override = $DB->get_record(
'assign_overrides',
['assignid' => $assignid, 'groupid' => $groupid],
'sortorder, duedate, cutoffdate, allowsubmissionsfromdate'
'sortorder, duedate, cutoffdate, allowsubmissionsfromdate, reason, reasonformat'
);
break;
default:
@@ -83,7 +83,8 @@ class provider implements
'userid' => 'privacy:metadata:userid',
'allowsubmissionsfromdate' => 'allowsubmissionsfromdate',
'duedate' => 'duedate',
'cutoffdate' => 'cutoffdate'
'cutoffdate' => 'cutoffdate',
'reason' => 'privacy:metadata:assignoverrides:reason',
];
$assignsubmission = [
'userid' => 'privacy:metadata:userid',
@@ -701,6 +702,10 @@ class provider implements
if (!empty($overrides->allowsubmissionsfromdate)) {
$data->allowsubmissionsfromdate = transform::datetime($overrides->allowsubmissionsfromdate);
}
if (!empty($overrides->reason)) {
$format = $overrides->reasonformat ?? FORMAT_MOODLE;
$data->reason = format_text($overrides->reason, $format, ['context' => $context]);
}
if (!empty($data)) {
writer::with_context($context)->export_data([get_string('overrides', 'mod_assign')], $data);
}
+3 -1
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="mod/assign/db" VERSION="20250414" COMMENT="XMLDB file for Moodle mod/assign"
<XMLDB PATH="mod/assign/db" VERSION="20251126" COMMENT="XMLDB file for Moodle mod/assign"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd"
>
@@ -162,6 +162,8 @@
<FIELD NAME="duedate" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Time by which students must have completed their attempt. Can be null, in which case the assign default is used."/>
<FIELD NAME="cutoffdate" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Time by which students must have completed their attempt. Can be null, in which case the assign default is used."/>
<FIELD NAME="timelimit" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Time limit in seconds. Can be null, in which case the quiz default is used."/>
<FIELD NAME="reason" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="An optional reason explaining why this override was granted."/>
<FIELD NAME="reasonformat" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The internal format for the override reason."/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+22
View File
@@ -217,5 +217,27 @@ function xmldb_assign_upgrade($oldversion) {
upgrade_mod_savepoint(true, 2026030900, 'assign');
}
if ($oldversion < 2026031300) {
// Define field reason to be added to assign_overrides.
$table = new xmldb_table('assign_overrides');
$field = new xmldb_field('reason', XMLDB_TYPE_TEXT, null, null, null, null, null, 'timelimit');
// Conditionally launch add field reason.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// Define field reasonformat to be added to assign_overrides.
$formatfield = new xmldb_field('reasonformat', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0', 'reason');
// Conditionally launch add field reasonformat.
if (!$dbman->field_exists($table, $formatfield)) {
$dbman->add_field($table, $formatfield);
}
// Assign savepoint reached.
upgrade_mod_savepoint(true, 2026031300, 'assign');
}
return true;
}
+3
View File
@@ -507,6 +507,8 @@ $string['overridedeletegroupsure'] = 'Are you sure you want to delete the overri
$string['overridedeleteusersure'] = 'Are you sure you want to delete the override for user {$a}?';
$string['overridegroup'] = 'Override group';
$string['overridegroupeventname'] = '{$a->assign} - {$a->group}';
$string['overridereason'] = 'Reason for override';
$string['overridereason_help'] = 'Optionally record the reason for this override.';
$string['overriderecalculatepenalty'] = 'Recalculate penalty for user(s) in the override';
$string['overrides'] = 'Overrides';
$string['overrideuser'] = 'Override user';
@@ -538,6 +540,7 @@ $string['privacy:metadata:assignmarkerfilter'] = 'Filter the assign summary by t
$string['privacy:metadata:assignmentid'] = 'Assignment ID';
$string['privacy:metadata:assignmessageexplanation'] = 'Messages are sent to students through the messaging system.';
$string['privacy:metadata:assignoverrides'] = 'Stores override information for the assignment';
$string['privacy:metadata:assignoverrides:reason'] = 'Optional notes documenting the reason for an assignment override.';
$string['privacy:metadata:assignperpage'] = 'Number of assignments shown per page.';
$string['privacy:metadata:assignquickgrading'] = 'A preference as to whether quick grading is used or not.';
$string['privacy:metadata:assignsubmissiondetail'] = 'Stores user submission information';
+10
View File
@@ -286,6 +286,16 @@ class assign_override_form extends moodleform {
$mform->setDefault('timelimit', $assigninstance->timelimit);
}
// Reason for override.
$editoroptions = [
'maxfiles' => 0,
'noclean' => false,
'context' => $this->context,
];
$mform->addElement('editor', 'reason_editor', get_string('overridereason', 'assign'), null, $editoroptions);
$mform->setType('reason_editor', PARAM_RAW);
$mform->addHelpButton('reason_editor', 'overridereason', 'assign');
// Submit buttons.
$mform->addElement('submit', 'resetbutton',
get_string('reverttodefaults', 'assign'));
+15
View File
@@ -103,6 +103,14 @@ foreach ($keys as $key) {
}
}
// Prepare reason editor data for existing overrides.
if (!empty($override) && isset($override->reason)) {
$data->reason_editor = [
'text' => $override->reason,
'format' => $override->reasonformat ?? FORMAT_MOODLE,
];
}
// True if group-based override.
$groupmode = !empty($data->groupid) || ($action === 'addgroup' && empty($overrideid));
@@ -142,6 +150,13 @@ if ($mform->is_cancelled()) {
// Process the data.
$fromform->assignid = $assigninstance->id;
// Extract reason and reasonformat from editor field.
if (isset($fromform->reason_editor)) {
$fromform->reason = $fromform->reason_editor['text'] ?? '';
$fromform->reasonformat = $fromform->reason_editor['format'] ?? FORMAT_MOODLE;
unset($fromform->reason_editor);
}
// Replace unchanged values with null.
foreach ($keys as $key) {
if (!isset($fromform->{$key}) || $fromform->{$key} == $assigninstance->{$key}) {
+14
View File
@@ -219,6 +219,20 @@ foreach ($overrides as $override) {
$values[] = $override->timelimit > 0 ? format_time($override->timelimit) : get_string('none', 'assign');
}
// Format reason.
if (isset($override->reason) && $override->reason !== '') {
$formattedreason = format_text(
$override->reason,
$override->reasonformat ?? FORMAT_MOODLE,
['context' => $context],
);
if ($formattedreason !== '') {
$fields[] = get_string('overridereason', 'assign');
$values[] = $formattedreason;
}
}
// Icons.
$iconstr = '';
@@ -0,0 +1,65 @@
<?php
// This file is part of Moodle - https://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 <https://www.gnu.org/licenses/>.
namespace mod_assign\backup;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->libdir . "/phpunit/classes/restore_date_testcase.php");
/**
* Restore override tests.
*
* @package mod_assign
* @author Alexander Van der Bellen <[email protected]>
* @copyright 2025 Catalyst IT Australia Pty Ltd
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
#[\PHPUnit\Framework\Attributes\CoversClass(\restore_assign_activity_structure_step::class)]
final class restore_override_test extends \restore_date_testcase {
/**
* Test restore overrides with reason.
*/
public function test_restore_overrides_with_reason(): void {
global $DB, $USER;
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course();
$assign = $this->getDataGenerator()->create_module('assign', ['course' => $course->id]);
$useroverride = (object) [
'assignid' => $assign->id,
'userid' => $USER->id,
'sortorder' => 1,
'allowsubmissionsfromdate' => 100,
'reason' => 'This is a reason',
'reasonformat' => FORMAT_HTML,
];
$DB->insert_record('assign_overrides', $useroverride);
// Back up and restore.
$newcourseid = $this->backup_and_restore($course);
$newassign = $DB->get_record('assign', ['course' => $newcourseid]);
$overrides = $DB->get_records('assign_overrides', ['assignid' => $newassign->id]);
$this->assertEquals(1, count($overrides));
$restoredoverride = reset($overrides);
$this->assertEquals($useroverride->reason, $restoredoverride->reason);
$this->assertEquals($useroverride->reasonformat, $restoredoverride->reasonformat);
}
}
@@ -0,0 +1,64 @@
@mod @mod_assign
Feature: Assign override reason
In order to explain why an override was granted
As a teacher
I need to be able to add a reason to an override
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
| student1 | Student | 1 | student1@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
And the following "groups" exist:
| name | course | idnumber |
| Group 1 | C1 | G1 |
And the following "group members" exist:
| user | group |
| student1 | G1 |
And the following "activities" exist:
| activity | name | intro | course | assignsubmission_onlinetext_enabled |
| assign | Test assignment name | Submit your online text | C1 | 1 |
@javascript
Scenario: Add a user override with a reason
Given I am on the "Test assignment name" Activity page logged in as teacher1
When I navigate to "Overrides" in current page administration
And I press "Add user override"
And I set the following fields to these values:
| Override user | Student 1 |
| Due date | ##1 Jan 2030 08:00## |
| Reason | Extension granted for medical reasons |
And I press "Save"
And I should see "Extension granted for medical reasons" in the "Reason for override" "table_row"
And I click on "Edit" "link" in the "Student 1" "table_row"
And the field "Reason for override" matches value "Extension granted for medical reasons"
And I set the following fields to these values:
| Reason | Updated reason after review |
And I press "Save"
Then I should see "Updated reason after review" in the "Reason for override" "table_row"
@javascript
Scenario: Add a group override with a reason
Given I am on the "Test assignment name" Activity page logged in as teacher1
When I navigate to "Overrides" in current page administration
And I select "Group overrides" from the "jump" singleselect
And I press "Add group override"
And I set the following fields to these values:
| Override group | Group 1 |
| Due date | ##1 Jan 2030 08:00## |
| Reason | Additional time approved for this group |
And I press "Save"
And I should see "Additional time approved for this group" in the "Reason for override" "table_row"
And I click on "Edit" "link" in the "Group 1" "table_row"
And the field "Reason for override" matches value "Additional time approved for this group"
And I set the following fields to these values:
| Reason | Updated group reason after review |
And I press "Save"
Then I should see "Updated group reason after review" in the "Reason for override" "table_row"
@@ -4205,6 +4205,8 @@ Anchor link 2:<a title=\"bananas\" href=\"../logo-240x60.gif\">Link text</a>
'duedate' => 2,
'cutoffdate' => 3,
'timelimit' => null,
'reason' => null,
'reasonformat' => 0,
],
(object) [
// Override for group 2, lower priority (numerically higher sortorder).
@@ -4216,6 +4218,8 @@ Anchor link 2:<a title=\"bananas\" href=\"../logo-240x60.gif\">Link text</a>
'duedate' => 6,
'cutoffdate' => 6,
'timelimit' => null,
'reason' => null,
'reasonformat' => 0,
],
(object) [
// User override.
@@ -4227,6 +4231,8 @@ Anchor link 2:<a title=\"bananas\" href=\"../logo-240x60.gif\">Link text</a>
'duedate' => 8,
'cutoffdate' => 9,
'timelimit' => null,
'reason' => null,
'reasonformat' => 0,
],
];
@@ -229,6 +229,8 @@ final class provider_test extends provider_testcase {
$overridedata->duedate = time();
$overridedata->allowsubmissionsfromdate = time();
$overridedata->cutoffdate = time();
$overridedata->reason = 'This is a reason';
$overridedata->reasonformat = FORMAT_MOODLE;
$DB->insert_record('assign_overrides', $overridedata);
$grade1 = '67.00';
@@ -301,6 +303,10 @@ final class provider_test extends provider_testcase {
$this->assertEquals($user->id, $writer->get_data(['Marker allocations'])->data[0]['student']);
$this->assertEquals($teacher->id, $writer->get_data(['Marks'])->data[0]['marker']);
$this->assertEquals(99.9, $writer->get_data(['Marks'])->data[0]['mark']);
$this->assertEquals(
format_text($overridedata->reason, $overridedata->reasonformat, ['context' => $context]),
$overrideexport->reason
);
}
/**
+1 -1
View File
@@ -25,5 +25,5 @@
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'mod_assign'; // Full name of the plugin (used for diagnostics).
$plugin->version = 2026030900; // The current module version (Date: YYYYMMDDXX).
$plugin->version = 2026031300; // The current module version (Date: YYYYMMDDXX).
$plugin->requires = 2025092600; // Requires this Moodle version.
@@ -137,7 +137,7 @@ class backup_lesson_activity_structure_step extends backup_activity_structure_st
$overrides = new backup_nested_element('overrides');
$override = new backup_nested_element('override', array('id'), array(
'groupid', 'userid', 'available', 'deadline', 'timelimit',
'review', 'maxattempts', 'retake', 'password'));
'review', 'maxattempts', 'retake', 'password', 'reason', 'reasonformat'));
// Now that we have all of the elements created we've got to put them
// together correctly.
+2 -2
View File
@@ -66,7 +66,7 @@ class overrides implements data_source_interface {
$override = $DB->get_record(
'lesson_overrides',
['lessonid' => $lessonid, 'userid' => $userid],
'available, deadline, timelimit, review, maxattempts, retake, password'
'available, deadline, timelimit, review, maxattempts, retake, password, reason, reasonformat'
);
break;
case 'g':
@@ -74,7 +74,7 @@ class overrides implements data_source_interface {
$override = $DB->get_record(
'lesson_overrides',
['lessonid' => $lessonid, 'groupid' => $groupid],
'available, deadline, timelimit, review, maxattempts, retake, password'
'available, deadline, timelimit, review, maxattempts, retake, password, reason, reasonformat'
);
break;
default:
+13 -1
View File
@@ -107,6 +107,7 @@ class provider implements
'maxattempts' => 'privacy:metadata:overrides:maxattempts',
'retake' => 'privacy:metadata:overrides:retake',
'password' => 'privacy:metadata:overrides:password',
'reason' => 'privacy:metadata:overrides:reason',
], 'privacy:metadata:overrides');
$collection->add_user_preference('lesson_view', 'privacy:metadata:userpref:lessonview');
@@ -257,7 +258,7 @@ class provider implements
$recordset = $DB->get_recordset_select('lesson_overrides', $sqluserlesson, $paramsuserlesson);
static::recordset_loop_and_export($recordset, 'lessonid', null, function($carry, $record) {
// We know that there is only one row per lesson, so no need to use $carry.
return (object) [
$data = (object) [
'available' => $record->available !== null ? transform::datetime($record->available) : null,
'deadline' => $record->deadline !== null ? transform::datetime($record->deadline) : null,
'timelimit' => $record->timelimit !== null ? format_time($record->timelimit) : null,
@@ -266,8 +267,19 @@ class provider implements
'retake' => $record->retake !== null ? transform::yesno($record->retake) : null,
'password' => $record->password,
];
if (!empty($record->reason)) {
$data->reason = $record->reason;
$data->reasonformat = $record->reasonformat ?? FORMAT_MOODLE;
}
return $data;
}, function($lessonid, $data) use ($lessonidstocmids) {
$context = context_module::instance($lessonidstocmids[$lessonid]);
if (isset($data->reason)) {
$data->reason = format_text($data->reason, $data->reasonformat, ['context' => $context]);
unset($data->reasonformat);
}
writer::with_context($context)->export_related_data([], 'overrides', $data);
});
+3 -1
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="mod/lesson/db" VERSION="20250414" COMMENT="XMLDB file for Moodle mod/lesson"
<XMLDB PATH="mod/lesson/db" VERSION="20251126" COMMENT="XMLDB file for Moodle mod/lesson"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd"
>
@@ -189,6 +189,8 @@
<FIELD NAME="maxattempts" TYPE="int" LENGTH="3" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="retake" TYPE="int" LENGTH="3" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="password" TYPE="char" LENGTH="32" NOTNULL="false" SEQUENCE="false" COMMENT="Lesson password. Can be null, in which case the lesson default is used."/>
<FIELD NAME="reason" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="An optional reason explaining why this override was granted."/>
<FIELD NAME="reasonformat" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The internal format for the override reason."/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+22
View File
@@ -75,5 +75,27 @@ function xmldb_lesson_upgrade($oldversion) {
upgrade_mod_savepoint(true, 2026022300, 'lesson');
}
if ($oldversion < 2026030600) {
// Define field reason to be added to lesson_overrides.
$table = new xmldb_table('lesson_overrides');
$field = new xmldb_field('reason', XMLDB_TYPE_TEXT, null, null, null, null, null, 'password');
// Conditionally launch add field reason.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// Define field reasonformat to be added to lesson_overrides.
$formatfield = new xmldb_field('reasonformat', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0', 'reason');
// Conditionally launch add field reasonformat.
if (!$dbman->field_exists($table, $formatfield)) {
$dbman->add_field($table, $formatfield);
}
// Lesson savepoint reached.
upgrade_mod_savepoint(true, 2026030600, 'lesson');
}
return true;
}
+3
View File
@@ -434,6 +434,8 @@ $string['overridedeletegroupsure'] = 'Are you sure you want to delete the overri
$string['overridedeleteusersure'] = 'Are you sure you want to delete the override for user {$a}?';
$string['overridegroup'] = 'Override group';
$string['overridegroupeventname'] = '{$a->lesson} - {$a->group}';
$string['overridereason'] = 'Reason for override';
$string['overridereason_help'] = 'Optionally record the reason for this override.';
$string['overrides'] = 'Overrides';
$string['overrideuser'] = 'Override user';
$string['overrideusereventname'] = '{$a->lesson} - Override';
@@ -500,6 +502,7 @@ $string['privacy:metadata:overrides:maxattempts'] = 'The maximium number of atte
$string['privacy:metadata:overrides:retake'] = 'Whether re-takes are allowed';
$string['privacy:metadata:overrides:password'] = 'The password to access the lesson';
$string['privacy:metadata:overrides'] = 'A record of overrides per lesson';
$string['privacy:metadata:overrides:reason'] = 'Optional notes documenting the reason for a lesson override.';
$string['privacy:metadata:userpref:lessonview'] = 'The preferred display mode when editing lessons';
$string['privacy:path:essayresponses'] = 'Essay responses';
$string['privacy:path:essayanswers'] = 'Essay answers';
+10
View File
@@ -230,6 +230,16 @@ class lesson_override_form extends moodleform {
$mform->addHelpButton('retake', 'retakesallowed', 'lesson');
$mform->setDefault('retake', $this->lesson->retake);
// Reason for override.
$editoroptions = [
'maxfiles' => 0,
'noclean' => false,
'context' => $this->context,
];
$mform->addElement('editor', 'reason_editor', get_string('overridereason', 'lesson'), null, $editoroptions);
$mform->setType('reason_editor', PARAM_RAW);
$mform->addHelpButton('reason_editor', 'overridereason', 'lesson');
// Submit buttons.
$mform->addElement('submit', 'resetbutton',
get_string('reverttodefaults', 'lesson'));
+15
View File
@@ -99,6 +99,14 @@ foreach ($keys as $key) {
}
}
// Prepare reason editor data for existing overrides.
if (!empty($override) && isset($override->reason)) {
$data->reason_editor = [
'text' => $override->reason,
'format' => $override->reasonformat ?? FORMAT_MOODLE,
];
}
// True if group-based override.
$groupmode = !empty($data->groupid) || ($action === 'addgroup' && empty($overrideid));
@@ -130,6 +138,13 @@ if ($mform->is_cancelled()) {
// Process the data.
$fromform->lessonid = $lesson->id;
// Extract reason and reasonformat from editor field.
if (isset($fromform->reason_editor)) {
$fromform->reason = $fromform->reason_editor['text'] ?? '';
$fromform->reasonformat = $fromform->reason_editor['format'] ?? FORMAT_MOODLE;
unset($fromform->reason_editor);
}
// Replace unchanged values with null.
foreach ($keys as $key) {
if ($fromform->{$key} == $lesson->{$key}) {
+14
View File
@@ -265,6 +265,20 @@ foreach ($overrides as $override) {
get_string('enabled', 'lesson') : get_string('none', 'lesson');
}
// Format reason.
if (isset($override->reason) && $override->reason !== '') {
$formattedreason = format_text(
$override->reason,
$override->reasonformat ?? FORMAT_MOODLE,
['context' => $context],
);
if ($formattedreason !== '') {
$fields[] = get_string('overridereason', 'lesson');
$values[] = $formattedreason;
}
}
// Icons.
$iconstr = '';
@@ -28,6 +28,7 @@ require_once($CFG->libdir . "/phpunit/classes/restore_date_testcase.php");
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
#[\PHPUnit\Framework\Attributes\CoversClass(\restore_lesson_activity_structure_step::class)]
final class restore_override_test extends \restore_date_testcase {
/**
@@ -95,4 +96,37 @@ final class restore_override_test extends \restore_date_testcase {
// 1 user override.
$this->assertEquals(1, count($overrides));
}
/**
* Test restore overrides with reason.
*/
public function test_restore_overrides_with_reason(): void {
global $DB, $USER;
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course();
$lessongen = $this->getDataGenerator()->get_plugin_generator('mod_lesson');
$lesson = $lessongen->create_instance(['course' => $course->id]);
$useroverride = (object) [
'lessonid' => $lesson->id,
'userid' => $USER->id,
'sortorder' => 1,
'available' => 100,
'reason' => 'This is a reason',
'reasonformat' => FORMAT_MOODLE,
];
$DB->insert_record('lesson_overrides', $useroverride);
// Back up and restore.
$newcourseid = $this->backup_and_restore($course);
$newlesson = $DB->get_record('lesson', ['course' => $newcourseid]);
$overrides = $DB->get_records('lesson_overrides', ['lessonid' => $newlesson->id]);
$this->assertEquals(1, count($overrides));
$restoredoverride = reset($overrides);
$this->assertEquals($useroverride->reason, $restoredoverride->reason);
$this->assertEquals($useroverride->reasonformat, $restoredoverride->reasonformat);
}
}
@@ -0,0 +1,69 @@
@mod @mod_lesson
Feature: Lesson override reason
In order to explain why an override was granted
As a teacher
I need to be able to add a reason to an override
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
| student1 | Student | 1 | student1@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
And the following "groups" exist:
| name | course | idnumber |
| Group 1 | C1 | G1 |
And the following "group members" exist:
| user | group |
| student1 | G1 |
And the following "activities" exist:
| activity | name | intro | course |
| lesson | Test lesson | Test lesson intro | C1 |
@javascript
Scenario: Add a user override with a reason
Given I am on the "Test lesson" "lesson activity" page logged in as teacher1
When I navigate to "Overrides" in current page administration
And I follow "Add user override"
And I set the following fields to these values:
| Override user | Student 1 |
| Deadline | ##1 Jan 2030 08:00## |
| Reason | Extension granted for medical reasons |
And I press "Save"
And I should see "Extension granted for medical reasons" in the "Reason for override" "table_row"
And I click on "Edit" "link" in the "Student 1" "table_row"
And the field "Reason for override" matches value "Extension granted for medical reasons"
And I set the following fields to these values:
| Reason | Updated reason after review |
And I press "Save"
Then I should see "Updated reason after review" in the "Reason for override" "table_row"
@javascript
Scenario: Add a group override with a reason
Given I am on the "Test lesson" "lesson activity" page logged in as teacher1
When I navigate to "Overrides" in current page administration
And I select "Group overrides" from the "jump" singleselect
And I follow "Add group override"
And I set the following fields to these values:
| Override group | Group 1 |
| id_deadline_enabled | 1 |
| deadline[day] | 1 |
| deadline[month] | January |
| deadline[year] | 2030 |
| deadline[hour] | 08 |
| deadline[minute] | 00 |
| Reason | Additional time approved for this group |
And I press "Save"
And I should see "Additional time approved for this group" in the "Reason for override" "table_row"
And I click on "Edit" "link" in the "Group 1" "table_row"
And the field "Reason for override" matches value "Additional time approved for this group"
And I set the following fields to these values:
| Reason | Updated group reason after review |
And I press "Save"
Then I should see "Updated group reason after review" in the "Reason for override" "table_row"
@@ -449,7 +449,9 @@ final class provider_test extends provider_testcase {
'review' => 1,
'maxattempts' => 1,
'retake' => 0,
'password' => '1337 5p34k'
'password' => '1337 5p34k',
'reason' => 'This is a reason',
'reasonformat' => FORMAT_MOODLE,
]);
$this->create_override($cm1, $u2, [
'available' => $now - 1230,
@@ -480,6 +482,7 @@ final class provider_test extends provider_testcase {
$this->assertEquals(1, $data->maxattempts);
$this->assertEquals(transform::yesno(false), $data->retake);
$this->assertEquals('1337 5p34k', $data->password);
$this->assertEquals(format_text('This is a reason', FORMAT_MOODLE, ['context' => $cm2ctx]), $data->reason);
writer::reset();
provider::export_user_data(new approved_contextlist($u2, 'mod_lesson', [$cm1ctx->id, $cm2ctx->id]));
+1 -1
View File
@@ -24,7 +24,7 @@
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2026022300; // The current module version (Date: YYYYMMDDXX).
$plugin->version = 2026030600; // The current module version (Date: YYYYMMDDXX).
$plugin->requires = 2025092600; // Requires this Moodle version.
$plugin->component = 'mod_lesson'; // Full name of the plugin (used for diagnostics)
$plugin->cron = 0;
@@ -73,7 +73,7 @@ class backup_quiz_activity_structure_step extends backup_questions_activity_stru
$override = new backup_nested_element('override', ['id'], [
'userid', 'groupid', 'timeopen', 'timeclose',
'timelimit', 'attempts', 'password']);
'timelimit', 'attempts', 'password', 'reason', 'reasonformat']);
$grades = new backup_nested_element('grades');
+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 <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
declare(strict_types=1);
namespace mod_quiz\cache;
use core_cache\data_source_interface;
use core_cache\definition;
/**
* Class quiz_overrides
*
* @package mod_quiz
* @copyright 2021 Shamim Rezaie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class overrides implements data_source_interface {
/** @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 definition $definition
* @return overrides
*/
public static function get_instance_for_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 \core_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, reason, reasonformat'
);
break;
case 'g':
$groupid = (int) $ugid;
$override = $DB->get_record(
'quiz_overrides',
['quiz' => $quizid, 'groupid' => $groupid],
'timeopen, timeclose, timelimit, attempts, password, reason, reasonformat'
);
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;
}
}
+3
View File
@@ -17,6 +17,7 @@
namespace mod_quiz\external;
use core_external\external_api;
use core_external\external_format_value;
use core_external\external_function_parameters;
use core_external\external_multiple_structure;
use core_external\external_single_structure;
@@ -85,6 +86,8 @@ class get_overrides extends external_api {
'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),
'reason' => new external_value(PARAM_RAW, 'Override reason', VALUE_DEFAULT, null),
'reasonformat' => new external_format_value('reason', VALUE_DEFAULT, null),
]);
return new external_single_structure([
+11 -1
View File
@@ -17,6 +17,7 @@
namespace mod_quiz\external;
use core_external\external_api;
use core_external\external_format_value;
use core_external\external_function_parameters;
use core_external\external_multiple_structure;
use core_external\external_single_structure;
@@ -46,6 +47,8 @@ class save_overrides extends external_api {
'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),
'reason' => new external_value(PARAM_RAW, 'Quiz override reason', VALUE_OPTIONAL),
'reasonformat' => new external_format_value('reason', VALUE_OPTIONAL),
]);
return new external_function_parameters([
@@ -82,7 +85,14 @@ class save_overrides extends external_api {
);
// Iterate over and save all overrides.
$ids = array_map(fn($override) => $manager->save_override($override), $overrides);
$ids = array_map(function (array $override) use ($manager): int {
// Ensure reasonformat is set on create when a reason is supplied but no format given.
if (empty($override['id']) && isset($override['reason']) && !isset($override['reasonformat'])) {
$override['reasonformat'] = FORMAT_MOODLE;
}
return $manager->save_override($override);
}, $overrides);
return ['ids' => $ids];
}
@@ -224,6 +224,16 @@ class edit_override_form extends moodleform {
$mform->addHelpButton('attempts', 'attempts', 'quiz');
$mform->setDefault('attempts', $this->quiz->attempts);
// Reason for override.
$editoroptions = [
'maxfiles' => 0,
'noclean' => false,
'context' => $this->context,
];
$mform->addElement('editor', 'reason_editor', get_string('overridereason', 'quiz'), null, $editoroptions);
$mform->setType('reason_editor', PARAM_RAW);
$mform->addHelpButton('reason_editor', 'overridereason', 'quiz');
// Submit buttons.
$mform->addElement('submit', 'resetbutton',
get_string('reverttodefaults', 'quiz'));
@@ -94,7 +94,14 @@ class override_manager {
return isset($formdata->$key) && !is_null($formdata->$key);
}, self::OVERRIDEABLE_QUIZ_SETTINGS);
if (!in_array(true, $keysthatareset)) {
$hasoverridevalues = in_array(true, $keysthatareset, true);
// If updating, we can also just update the reason.
if (!empty($formdata->id) && (property_exists($formdata, 'reason') || property_exists($formdata, 'reasonformat'))) {
$hasoverridevalues = true;
}
if (!$hasoverridevalues) {
$errors['general'][] = new \lang_string('nooverridedata', 'quiz');
}
@@ -224,6 +231,14 @@ class override_manager {
// Remove values that are the same as currently in the quiz.
$settings = $this->clear_unused_values($settings);
// Pass through the optional reason fields unchanged.
if (array_key_exists('reason', $formdata)) {
$settings['reason'] = $formdata['reason'];
}
if (array_key_exists('reasonformat', $formdata) && $formdata['reasonformat'] !== null) {
$settings['reasonformat'] = $formdata['reasonformat'];
}
// Add the user / group back as applicable.
$userorgroupdata = array_intersect_key($formdata, array_flip(['userid', 'groupid', 'quiz', 'id']));
@@ -104,6 +104,7 @@ class provider implements
'timeopen' => 'privacy:metadata:quiz_overrides:timeopen',
'timeclose' => 'privacy:metadata:quiz_overrides:timeclose',
'timelimit' => 'privacy:metadata:quiz_overrides:timelimit',
'reason' => 'privacy:metadata:quiz_overrides:reason',
], 'privacy:metadata:quiz_overrides');
// These define the structure of the quiz.
@@ -255,6 +256,8 @@ class provider implements
qo.timeopen AS override_timeopen,
qo.timeclose AS override_timeclose,
qo.timelimit AS override_timelimit,
qo.reason AS override_reason,
qo.reasonformat AS override_reasonformat,
c.id AS contextid,
cm.id AS cmid
FROM {context} c
@@ -305,6 +308,10 @@ class provider implements
if (!empty($quizdata->override_timelimit)) {
$quizdata->override->timelimit = $quiz->override_timelimit;
}
if (!empty($quiz->override_reason)) {
$format = $quiz->override_reasonformat ?? FORMAT_MOODLE;
$quizdata->override->reason = format_text($quiz->override_reason, $format, ['context' => $context]);
}
}
$quizdata->accessdata = (object) [];
+3 -1
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="mod/quiz/db" VERSION="20250414" COMMENT="XMLDB file for Moodle mod/quiz"
<XMLDB PATH="mod/quiz/db" VERSION="20251126" COMMENT="XMLDB file for Moodle mod/quiz"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd"
>
@@ -133,6 +133,8 @@
<FIELD NAME="timelimit" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Time limit in seconds. Can be null, in which case the quiz default is used."/>
<FIELD NAME="attempts" TYPE="int" LENGTH="6" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="password" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false" COMMENT="Quiz password. Can be null, in which case the quiz default is used."/>
<FIELD NAME="reason" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="An optional reason explaining why this override was granted."/>
<FIELD NAME="reasonformat" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The internal format for the override reason."/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+22
View File
@@ -84,5 +84,27 @@ function xmldb_quiz_upgrade($oldversion) {
upgrade_mod_savepoint(true, 2026022400, 'quiz');
}
if ($oldversion < 2026030600) {
// Define field reason to be added to quiz_overrides.
$table = new xmldb_table('quiz_overrides');
$field = new xmldb_field('reason', XMLDB_TYPE_TEXT, null, null, null, null, null, 'password');
// Conditionally launch add field reason.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// Define field reasonformat to be added to quiz_overrides.
$formatfield = new xmldb_field('reasonformat', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0', 'reason');
// Conditionally launch add field reasonformat.
if (!$dbman->field_exists($table, $formatfield)) {
$dbman->add_field($table, $formatfield);
}
// Quiz savepoint reached.
upgrade_mod_savepoint(true, 2026030600, 'quiz');
}
return true;
}
+3
View File
@@ -685,6 +685,8 @@ $string['overridedeletegroupsure'] = 'Are you sure you want to delete the overri
$string['overridedeleteusersure'] = 'Are you sure you want to delete the override for user {$a}?';
$string['overridegroup'] = 'Override group';
$string['overridegroupeventname'] = '{$a->quiz} - {$a->group}';
$string['overridereason'] = 'Reason for override';
$string['overridereason_help'] = 'Optionally record the reason for this override.';
$string['overrideinvalidattempts'] = 'Attempts value must be greater than zero.';
$string['overrideinvalidexistingid'] = 'Existing override doesn\'t exist.';
$string['overrideinvalidgroup'] = 'Group given doesn\'t exist.';
@@ -765,6 +767,7 @@ $string['privacy:metadata:quiz_grades:timemodified'] = 'The time that the grade
$string['privacy:metadata:quiz_grades:userid'] = 'The user who was graded.';
$string['privacy:metadata:quiz_overrides'] = 'Details about overrides for this quiz';
$string['privacy:metadata:quiz_overrides:quiz'] = 'The quiz with override information';
$string['privacy:metadata:quiz_overrides:reason'] = 'Optional notes documenting the reason for a quiz override.';
$string['privacy:metadata:quiz_overrides:timeclose'] = 'The new close time for the quiz.';
$string['privacy:metadata:quiz_overrides:timelimit'] = 'The new time limit for the quiz.';
$string['privacy:metadata:quiz_overrides:timeopen'] = 'The new open time for the quiz.';
+15
View File
@@ -88,6 +88,14 @@ foreach ($keys as $key) {
}
}
// Prepare reason editor data.
if (isset($override->reason)) {
$data->reason_editor = [
'text' => $override->reason,
'format' => $override->reasonformat ?? FORMAT_MOODLE,
];
}
// If we are duplicating an override, then clear the user/group and override id
// since they will change.
if ($action === 'duplicate') {
@@ -121,6 +129,13 @@ if ($mform->is_cancelled()) {
$fromform->id = $overrideid;
}
// Extract reason and reasonformat from editor field.
if (isset($fromform->reason_editor)) {
$fromform->reason = $fromform->reason_editor['text'] ?? '';
$fromform->reasonformat = $fromform->reason_editor['format'] ?? FORMAT_MOODLE;
unset($fromform->reason_editor);
}
// Process the data.
$id = $manager->save_override((array) $fromform);
+14
View File
@@ -229,6 +229,20 @@ foreach ($overrides as $override) {
get_string('enabled', 'quiz') : get_string('none', 'quiz');
}
// Format reason.
if (isset($override->reason) && $override->reason !== '') {
$formattedreason = format_text(
$override->reason,
$override->reasonformat ?? FORMAT_MOODLE,
['context' => $context],
);
if ($formattedreason !== '') {
$fields[] = get_string('overridereason', 'quiz');
$values[] = $formattedreason;
}
}
// Prepare the information about who this override applies to.
$extranamebit = $active ? '' : '*';
$usercells = [];
@@ -0,0 +1,64 @@
<?php
// This file is part of Moodle - https://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 <https://www.gnu.org/licenses/>.
namespace mod_quiz\backup;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->libdir . "/phpunit/classes/restore_date_testcase.php");
/**
* Restore override tests.
*
* @package mod_quiz
* @author Alexander Van der Bellen <[email protected]>
* @copyright 2025 Catalyst IT Australia Pty Ltd
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
#[\PHPUnit\Framework\Attributes\CoversClass(\restore_quiz_activity_structure_step::class)]
final class restore_override_test extends \restore_date_testcase {
/**
* Test restore overrides with reason.
*/
public function test_restore_overrides_with_reason(): void {
global $DB, $USER;
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course();
$quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id]);
$useroverride = (object) [
'quiz' => $quiz->id,
'userid' => $USER->id,
'timeopen' => 100,
'reason' => 'This is a reason',
'reasonformat' => FORMAT_HTML,
];
$DB->insert_record('quiz_overrides', $useroverride);
// Back up and restore.
$newcourseid = $this->backup_and_restore($course);
$newquiz = $DB->get_record('quiz', ['course' => $newcourseid]);
$overrides = $DB->get_records('quiz_overrides', ['quiz' => $newquiz->id]);
$this->assertEquals(1, count($overrides));
$restoredoverride = reset($overrides);
$this->assertEquals($useroverride->reason, $restoredoverride->reason);
$this->assertEquals($useroverride->reasonformat, $restoredoverride->reasonformat);
}
}
@@ -0,0 +1,64 @@
@mod @mod_quiz
Feature: Quiz override reason
In order to explain why an override was granted
As a teacher
I need to be able to add a reason to an override
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
| student1 | Student | 1 | student1@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
And the following "groups" exist:
| name | course | idnumber |
| Group 1 | C1 | G1 |
And the following "group members" exist:
| user | group |
| student1 | G1 |
And the following "activities" exist:
| activity | name | intro | course |
| quiz | Test quiz | Test quiz intro | C1 |
@javascript
Scenario: Add a user override with a reason
Given I am on the "Test quiz" "mod_quiz > View" page logged in as "teacher1"
When I navigate to "Overrides" in current page administration
And I press "Add user override"
And I set the following fields to these values:
| Override user | Student 1 |
| Close the quiz | ##1 Jan 2030 08:00## |
| Reason | Extension granted for medical reasons |
And I press "Save"
And I should see "Extension granted for medical reasons" in the "Reason for override" "table_row"
And I click on "Edit" "link" in the "Student 1" "table_row"
And the field "Reason for override" matches value "Extension granted for medical reasons"
And I set the following fields to these values:
| Reason | Updated reason after review |
And I press "Save"
Then I should see "Updated reason after review" in the "Reason for override" "table_row"
@javascript
Scenario: Add a group override with a reason
Given I am on the "Test quiz" "mod_quiz > View" page logged in as "teacher1"
When I navigate to "Overrides" in current page administration
And I select "Group overrides" from the "jump" singleselect
And I press "Add group override"
And I set the following fields to these values:
| Override group | Group 1 |
| Close the quiz | ##1 Jan 2030 08:00## |
| Reason | Additional time approved for this group |
And I press "Save"
And I should see "Additional time approved for this group" in the "Reason for override" "table_row"
And I click on "Edit" "link" in the "Group 1" "table_row"
And the field "Reason for override" matches value "Additional time approved for this group"
And I set the following fields to these values:
| Reason | Updated group reason after review |
And I press "Save"
Then I should see "Updated group reason after review" in the "Reason for override" "table_row"
+261
View File
@@ -216,4 +216,265 @@ final class override_test extends \core_external\tests\externallib_testcase {
$this->assertNotEmpty($result['ids']);
$this->assertContains($id, $result['ids']);
}
/**
* Provides values to test_save_reason_overrides
*
* @return array
*/
public static function save_reason_overrides_provider(): array {
return [
'create with reason' => [
'data' => [
'reason' => 'This is a reason',
'reasonformat' => FORMAT_HTML,
'timeopen' => 999,
],
'expectedreason' => 'This is a reason',
'expectedformat' => FORMAT_HTML,
],
'create with reason no format' => [
'data' => [
'reason' => 'This is a reason',
'timeopen' => 999,
],
'expectedreason' => 'This is a reason',
'expectedformat' => FORMAT_MOODLE,
],
'create with reason and different format' => [
'data' => [
'reason' => 'This is a reason',
'reasonformat' => FORMAT_MOODLE,
'timeopen' => 999,
],
'expectedreason' => 'This is a reason',
'expectedformat' => FORMAT_MOODLE,
],
'create with reason and null format' => [
'data' => [
'reason' => 'This is a reason',
'reasonformat' => null,
'timeopen' => 999,
],
'expectedreason' => 'This is a reason',
'expectedformat' => FORMAT_MOODLE,
],
'create with reason only (fail)' => [
'data' => [
'reason' => 'This is a reason',
],
'expectedreason' => null,
'expectedformat' => FORMAT_HTML,
'expectedexception' => \invalid_parameter_exception::class,
],
'create with format only' => [
'data' => [
'reasonformat' => FORMAT_MOODLE,
],
'expectedreason' => null,
'expectedformat' => FORMAT_MOODLE,
'expectedexception' => \invalid_parameter_exception::class,
],
'create with null reason' => [
'data' => [
'reason' => null,
],
'expectedreason' => null,
'expectedformat' => FORMAT_HTML,
'expectedexception' => \invalid_parameter_exception::class,
],
'create with no reason or format' => [
'data' => [],
'expectedreason' => null,
'expectedformat' => FORMAT_HTML,
'expectedexception' => \invalid_parameter_exception::class,
],
];
}
/**
* Tests save_overrides with reason
*
* @dataProvider save_reason_overrides_provider
* @param array $data
* @param string|null $expectedreason
* @param int|string|null $expectedformat
* @param string|null $expectedexception
*/
public function test_save_reason_overrides(
array $data,
?string $expectedreason,
int|string|null $expectedformat,
?string $expectedexception = null
): void {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
$quiz = $this->create_quiz();
$user = $this->getDataGenerator()->create_user();
$data = array_merge($data, ['userid' => $user->id]);
$payload = [
'quizid' => $quiz->id,
'overrides' => [
$data,
],
];
if ($expectedexception) {
$this->expectException($expectedexception);
}
$result = save_overrides::execute($payload);
if ($expectedexception) {
return;
}
$this->assertNotEmpty($result['ids']);
$this->assertCount(1, $result['ids']);
$overrideid = reset($result['ids']);
$override = $DB->get_record('quiz_overrides', ['id' => $overrideid]);
$this->assertEquals($expectedreason, $override->reason);
$this->assertEquals($expectedformat, $override->reasonformat);
}
/**
* Provides values to test_update_reason_overrides
*
* @return array
*/
public static function update_reason_overrides_provider(): array {
return [
'update reason' => [
'initialreason' => 'Initial reason',
'initialformat' => FORMAT_HTML,
'data' => [
'reason' => 'Updated reason',
'reasonformat' => FORMAT_HTML,
],
'expectedreason' => 'Updated reason',
'expectedformat' => FORMAT_HTML,
],
'update reason format' => [
'initialreason' => 'Initial reason',
'initialformat' => FORMAT_HTML,
'data' => [
'reason' => 'Initial reason',
'reasonformat' => FORMAT_MOODLE,
],
'expectedreason' => 'Initial reason',
'expectedformat' => FORMAT_MOODLE,
],
'update reason only' => [
'initialreason' => 'Initial reason',
'initialformat' => FORMAT_HTML,
'data' => [
'reason' => 'Updated reason',
],
'expectedreason' => 'Updated reason',
'expectedformat' => FORMAT_HTML,
],
'update format only' => [
'initialreason' => 'Initial reason',
'initialformat' => FORMAT_HTML,
'data' => [
'reasonformat' => FORMAT_MOODLE,
],
'expectedreason' => 'Initial reason',
'expectedformat' => FORMAT_MOODLE,
],
'update reason to null' => [
'initialreason' => 'Initial reason',
'initialformat' => FORMAT_HTML,
'data' => [
'reason' => null,
],
'expectedreason' => null,
'expectedformat' => FORMAT_HTML,
],
'update format to null' => [
'initialreason' => 'Initial reason',
'initialformat' => FORMAT_HTML,
'data' => [
'reasonformat' => null,
],
'expectedreason' => null,
'expectedformat' => null,
'expectedexception' => \invalid_parameter_exception::class,
],
'update no changes' => [
'initialreason' => 'Initial reason',
'initialformat' => FORMAT_HTML,
'data' => [],
'expectedreason' => null,
'expectedformat' => null,
'expectedexception' => \invalid_parameter_exception::class,
],
];
}
/**
* Tests update_overrides with reason
*
* @dataProvider update_reason_overrides_provider
* @param string|null $initialreason
* @param int|string|null $initialformat
* @param array $data
* @param string|null $expectedreason
* @param int|string|null $expectedformat
* @param string|null $expectedexception
*/
public function test_update_reason_overrides(
?string $initialreason,
int|string|null $initialformat,
array $data,
?string $expectedreason,
int|string|null $expectedformat,
?string $expectedexception = null
): void {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
$quiz = $this->create_quiz();
$user = $this->getDataGenerator()->create_user();
// Create initial override.
$overrideid = $DB->insert_record('quiz_overrides', [
'quiz' => $quiz->id,
'userid' => $user->id,
'reason' => $initialreason,
'reasonformat' => $initialformat,
]);
$data['id'] = $overrideid;
$data['userid'] = $user->id;
$payload = [
'quizid' => $quiz->id,
'overrides' => [
$data,
],
];
if ($expectedexception) {
$this->expectException($expectedexception);
}
save_overrides::execute($payload);
if ($expectedexception) {
return;
}
$override = $DB->get_record('quiz_overrides', ['id' => $overrideid]);
$this->assertEquals($expectedreason, $override->reason);
$this->assertEquals($expectedformat, $override->reasonformat);
}
}
@@ -148,6 +148,8 @@ final class provider_test extends \core_privacy\tests\provider_testcase {
'userid' => $user->id,
'timeclose' => 1300,
'timelimit' => null,
'reason' => 'This is a reason',
'reasonformat' => FORMAT_MOODLE,
]);
// Run as the user and make an attempt on the quiz.
@@ -177,6 +179,7 @@ final class provider_test extends \core_privacy\tests\provider_testcase {
$quizdata = $writer->get_data([]);
$this->assertEquals($quizobj->get_quiz_name(), $quizdata->name);
$this->assertEquals(format_text('This is a reason', FORMAT_MOODLE, ['context' => $context]), $quizdata->override->reason);
// Every module has an intro.
$this->assertTrue(isset($quizdata->intro));
+1 -1
View File
@@ -24,6 +24,6 @@
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2026022400;
$plugin->version = 2026030600;
$plugin->requires = 2025092600;
$plugin->component = 'mod_quiz';