Merge branch 'MDL-87492_MOODLE_501_STABLE' of https://github.com/cwarwicker/moodle into MOODLE_501_STABLE

This commit is contained in:
Mihail Geshoski
2026-02-24 14:32:44 +08:00
5 changed files with 214 additions and 1 deletions
+3
View File
@@ -1642,6 +1642,7 @@ class quiz_attempt {
* @param bool $studentisonline is the student currently interacting with Moodle?
*/
public function handle_if_time_expired($timestamp, $studentisonline) {
global $DB;
$timeclose = $this->get_access_manager($timestamp)->get_end_time($this->attempt);
@@ -1676,8 +1677,10 @@ class quiz_attempt {
// Transition to the appropriate state.
switch ($this->quizobj->get_quiz()->overduehandling) {
case 'autosubmit':
$transaction = $DB->start_delegated_transaction();
$this->process_submit($timestamp, false, $studentisonline ? $timestamp : $timeclose, $studentisonline);
$this->process_grade_submission($studentisonline ? $timestamp : $timeclose);
$transaction->allow_commit();
return;
case 'graceperiod':
@@ -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/>.
namespace mod_quiz\task;
use core\task\adhoc_task;
use mod_quiz\quiz_attempt;
/**
* Ad-hoc task to grade a submitted attempt.
*
* This is used (currently) for fixing quiz attempts which are stuck in "submitted" state, and
* in the future will also support the asynchronous grading of quiz attempts.
*
* @package mod_quiz
* @copyright 2024 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class grade_submission extends adhoc_task {
/**
* Return an instance of the task, with the attempt ID stored in custom data.
*
* @param int $attemptid
* @return self
*/
public static function instance(int $attemptid): self {
$task = new self();
$task->set_custom_data((object)['attemptid' => $attemptid]);
return $task;
}
/**
* Perform grading for the referenced submitted attempt.
*/
public function execute(): void {
global $DB;
$data = $this->get_custom_data();
if ($DB->record_exists('quiz_attempts', ['id' => $data->attemptid, 'state' => quiz_attempt::SUBMITTED])) {
$attempt = quiz_attempt::create($data->attemptid);
mtrace(
'Grading attempt for user ID ' .
$attempt->get_userid() . ' for quiz ' .
$attempt->get_quiz_name() . ' on course ' .
$attempt->get_course()->shortname
);
$attempt->process_grade_submission(time());
} else {
mtrace('Attempt ID ' . $data->attemptid . ' not found, or not in submitted state.');
}
}
}
+18
View File
@@ -149,6 +149,24 @@ function xmldb_quiz_upgrade($oldversion) {
upgrade_mod_savepoint(true, 2025041401, 'quiz');
}
if ($oldversion < 2026011600) {
// Queue tasks to process stuck quiz attempts (state = 'submitted').
$attemptids = $DB->get_fieldset_select(
'quiz_attempts',
'id',
'state = ?',
[\mod_quiz\quiz_attempt::SUBMITTED],
);
foreach ($attemptids as $attemptid) {
$task = \mod_quiz\task\grade_submission::instance($attemptid);
\core\task\manager::queue_adhoc_task($task, true);
}
// Quiz savepoint reached.
upgrade_mod_savepoint(true, 2026011600, 'quiz');
}
// Automatically generated Moodle v5.1.0 release upgrade line.
// Put any upgrade step following this.
@@ -0,0 +1,127 @@
<?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 advanced_testcase;
use mod_quiz\quiz_attempt;
use mod_quiz\quiz_settings;
use mod_quiz\task\grade_submission;
/**
* Unit tests for grade_submission task.
*
* @package mod_quiz
* @copyright 2026 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Conn Warwicker <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \mod_quiz\task\grade_submission
*/
final class grade_submission_test extends advanced_testcase {
/**
* Create a quiz attempt to use for testing
* @return quiz_attempt
*/
private function create_attempt(): quiz_attempt {
$this->resetAfterTest(true);
// Make a user to do the quiz.
$user = $this->getDataGenerator()->create_user();
$course = $this->getDataGenerator()->create_course();
// Make a quiz.
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quiz = $quizgenerator->create_instance(['course' => $course->id, 'grade' => 100.0, 'sumgrades' => 2]);
$quizobj = quiz_settings::create($quiz->id, $user->id);
// Add a question.
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $questiongenerator->create_question_category();
$question = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
quiz_add_quiz_question($question->id, $quiz, 1);
$attempt = quiz_prepare_and_start_new_attempt($quizobj, 1, null, false, [], [], $user->id);
return quiz_attempt::create($attempt->id);
}
/**
* Test that passing through an invalid attempt id does not break the task in any way
*/
public function test_invalid_attempt(): void {
// Try a negative number which will always be invalid as an ID.
$task = grade_submission::instance(-1);
// Execute the task and confirm that nothing untoward happens.
$task->execute();
$this->expectOutputString("Attempt ID -1 not found, or not in submitted state.\n");
}
/**
* Test executing the task on an inprogress attempt.
*/
public function test_attempt_state_inprogress(): void {
// Try and inprogress attempt.
$attempt = $this->create_attempt();
$this->assertEquals(quiz_attempt::IN_PROGRESS, $attempt->get_state());
// Execute the task for this attempt.
$task = grade_submission::instance($attempt->get_attemptid());
$task->execute();
// This should not be executed by the task.
$this->expectOutputString("Attempt ID " . $attempt->get_attemptid() . " not found, or not in submitted state.\n");
}
/**
* Test executing the task on a submitted attempt.
*/
public function test_attempt_state_submitted(): void {
// Try a "submitted" attempt, which is the expected state to be processed by the task.
$attempt = $this->create_attempt();
$attempt->process_submit(time(), false);
$this->assertEquals(quiz_attempt::SUBMITTED, $attempt->get_state());
// Execute the task for this attempt.
$task = grade_submission::instance($attempt->get_attemptid());
$task->execute();
// This should grade the attempt and mark it as finished.
$this->expectOutputRegex('/Grading attempt for user ID \d+ for quiz Quiz 1 on course tc_1/');
// Reload the attempt, as the `attempt` record on the quiz_attempt object doesn't seem to get updated.
$attempt = quiz_attempt::create($attempt->get_attemptid());
$this->assertEquals(quiz_attempt::FINISHED, $attempt->get_state());
}
/**
* Test executing the task on a finished attempt.
*/
public function test_attempt_state_finished(): void {
// Try a "finished" attempt.
$attempt = $this->create_attempt();
$attempt->process_grade_submission(time());
// At this point it should be finished.
$this->assertEquals(quiz_attempt::FINISHED, $attempt->get_state());
// Now try the task again, on the finished attempt.
$task = grade_submission::instance($attempt->get_attemptid());
$task->execute();
// This should not be executed by the task.
$this->expectOutputString("Attempt ID {$attempt->get_attemptid()} not found, or not in submitted state.\n");
}
}
+1 -1
View File
@@ -24,6 +24,6 @@
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2025100600;
$plugin->version = 2026011600;
$plugin->requires = 2025092600;
$plugin->component = 'mod_quiz';