This commit is contained in:
Ilya Tregubov
2021-10-20 11:33:14 +02:00
18 changed files with 706 additions and 15 deletions
+36 -1
View File
@@ -1007,6 +1007,15 @@ class quiz_attempt {
return false;
}
/**
* Do any questions in this attempt need to be graded manually?
*
* @return bool True if we have at least one question still needs manual grading.
*/
public function requires_manual_grading(): bool {
return $this->quba->get_total_mark() === null;
}
/**
* Get extra summary information about this attempt.
*
@@ -2202,6 +2211,14 @@ class quiz_attempt {
$this->attempt->sumgrades = $this->quba->get_total_mark();
$this->attempt->state = self::FINISHED;
$this->attempt->timecheckstate = null;
$this->attempt->gradednotificationsenttime = null;
if (!$this->requires_manual_grading() ||
!has_capability('mod/quiz:emailnotifyattemptgraded', $this->get_quizobj()->get_context(),
$this->get_userid())) {
$this->attempt->gradednotificationsenttime = $this->attempt->timefinish;
}
$DB->update_record('quiz_attempts', $this->attempt);
if (!$this->is_preview()) {
@@ -2651,6 +2668,25 @@ class quiz_attempt {
$event->trigger();
}
/**
* Trigger the attempt manual grading completed event.
*/
public function fire_attempt_manual_grading_completed_event() {
$params = [
'objectid' => $this->get_attemptid(),
'relateduserid' => $this->get_userid(),
'courseid' => $this->get_courseid(),
'context' => context_module::instance($this->get_cmid()),
'other' => [
'quizid' => $this->get_quizid()
]
];
$event = \mod_quiz\event\attempt_manual_grading_completed::create($params);
$event->add_record_snapshot('quiz_attempts', $this->get_attempt());
$event->trigger();
}
/**
* Update the timemodifiedoffline attempt field.
*
@@ -2668,7 +2704,6 @@ class quiz_attempt {
}
return false;
}
}
@@ -88,7 +88,8 @@ class backup_quiz_activity_structure_step extends backup_questions_activity_stru
$attempt = new backup_nested_element('attempt', array('id'), array(
'userid', 'attemptnum', 'uniqueid', 'layout', 'currentpage', 'preview',
'state', 'timestart', 'timefinish', 'timemodified', 'timemodifiedoffline', 'timecheckstate', 'sumgrades'));
'state', 'timestart', 'timefinish', 'timemodified', 'timemodifiedoffline',
'timecheckstate', 'sumgrades', 'gradednotificationsenttime'));
// This module is using questions, so produce the related question states and sessions
// attaching them to the $attempt element based in 'uniqueid' matching.
@@ -467,6 +467,12 @@ class restore_quiz_activity_structure_step extends restore_questions_activity_st
$data->timecheckstate = 0;
}
if (!isset($data->gradednotificationsenttime)) {
// For attempts restored from old Moodle sites before this field
// existed, we never want to send emails.
$data->gradednotificationsenttime = $data->timefinish;
}
// Deals with up-grading pre-2.3 back-ups to 2.3+.
if (!isset($data->state)) {
if ($data->timefinish > 0) {
@@ -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_OTHER;
}
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;
}
}
+2
View File
@@ -466,6 +466,8 @@ class mod_quiz_external extends external_api {
'timecheckstate' => new external_value(PARAM_INT, 'Next time quiz cron should check attempt for
state changes. NULL means never check.', VALUE_OPTIONAL),
'sumgrades' => new external_value(PARAM_FLOAT, 'Total marks for this attempt.', VALUE_OPTIONAL),
'gradednotificationsenttime' => new external_value(PARAM_INT,
'Time when the student was notified that manual grading of their attempt was complete.', VALUE_OPTIONAL),
)
);
}
+14 -10
View File
@@ -69,16 +69,17 @@ class provider implements
// The table 'quiz_attempts' stores a record of each quiz attempt.
// It contains a userid which links to the user making the attempt and contains information about that attempt.
$items->add_database_table('quiz_attempts', [
'attempt' => 'privacy:metadata:quiz_attempts:attempt',
'currentpage' => 'privacy:metadata:quiz_attempts:currentpage',
'preview' => 'privacy:metadata:quiz_attempts:preview',
'state' => 'privacy:metadata:quiz_attempts:state',
'timestart' => 'privacy:metadata:quiz_attempts:timestart',
'timefinish' => 'privacy:metadata:quiz_attempts:timefinish',
'timemodified' => 'privacy:metadata:quiz_attempts:timemodified',
'timemodifiedoffline' => 'privacy:metadata:quiz_attempts:timemodifiedoffline',
'timecheckstate' => 'privacy:metadata:quiz_attempts:timecheckstate',
'sumgrades' => 'privacy:metadata:quiz_attempts:sumgrades',
'attempt' => 'privacy:metadata:quiz_attempts:attempt',
'currentpage' => 'privacy:metadata:quiz_attempts:currentpage',
'preview' => 'privacy:metadata:quiz_attempts:preview',
'state' => 'privacy:metadata:quiz_attempts:state',
'timestart' => 'privacy:metadata:quiz_attempts:timestart',
'timefinish' => 'privacy:metadata:quiz_attempts:timefinish',
'timemodified' => 'privacy:metadata:quiz_attempts:timemodified',
'timemodifiedoffline' => 'privacy:metadata:quiz_attempts:timemodifiedoffline',
'timecheckstate' => 'privacy:metadata:quiz_attempts:timecheckstate',
'sumgrades' => 'privacy:metadata:quiz_attempts:sumgrades',
'gradednotificationsenttime' => 'privacy:metadata:quiz_attempts:gradednotificationsenttime',
], 'privacy:metadata:quiz_attempts');
// The table 'quiz_feedback' contains the feedback responses which will be shown to users depending upon the
@@ -543,6 +544,9 @@ class provider implements
if (!empty($attempt->timecheckstate)) {
$data->timecheckstate = transform::datetime($attempt->timecheckstate);
}
if (!empty($attempt->gradednotificationsenttime)) {
$data->gradednotificationsenttime = transform::datetime($attempt->gradednotificationsenttime);
}
if ($options->marks == \question_display_options::MARK_AND_MAX) {
$grade = quiz_rescale_grade($attempt->sumgrades, $quiz, false);
@@ -0,0 +1,156 @@
<?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;
defined('MOODLE_INTERNAL') || die();
use context_course;
use core_user;
use moodle_recordset;
use question_display_options;
use mod_quiz_display_options;
use quiz_attempt;
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
/**
* Cron Quiz Notify Attempts Graded Task.
*
* @package mod_quiz
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*
*/
class quiz_notify_attempt_manual_grading_completed extends \core\task\scheduled_task {
/**
* @var int|null For using in unit testing only. Override the time we consider as now.
*/
protected $forcedtime = null;
/**
* Get name of schedule task.
*
* @return string
*/
public function get_name(): string {
return get_string('notifyattemptsgradedtask', 'mod_quiz');
}
/**
* To let this class be unit tested, we wrap all accesses to the current time in this method.
*
* @return int The current time.
*/
protected function get_time(): int {
if (PHPUNIT_TEST && $this->forcedtime !== null) {
return $this->forcedtime;
}
return time();
}
/**
* For testing only, pretend the current time is different.
*
* @param int $time The time to set as the current time.
*/
public function set_time_for_testing(int $time): void {
if (!PHPUNIT_TEST) {
throw new \coding_exception('set_time_for_testing should only be used in unit tests.');
}
$this->forcedtime = $time;
}
/**
* Execute sending notification for manual graded attempts.
*/
public function execute() {
global $DB;
mtrace('Looking for quiz attempts which may need a graded notification sent...');
$attempts = $this->get_list_of_attempts();
$course = null;
$quiz = null;
$cm = null;
foreach ($attempts as $attempt) {
mtrace('Checking attempt ' . $attempt->id . ' at quiz ' . $attempt->quiz . '.');
if (!$quiz || $attempt->quiz != $quiz->id) {
$quiz = $DB->get_record('quiz', ['id' => $attempt->quiz], '*', MUST_EXIST);
$cm = get_coursemodule_from_instance('quiz', $attempt->quiz);
}
if (!$course || $course->id != $quiz->course) {
$course = $DB->get_record('course', ['id' => $quiz->course], '*', MUST_EXIST);
$coursecontext = context_course::instance($quiz->course);
}
$quiz = quiz_update_effective_access($quiz, $attempt->userid);
$attemptobj = new quiz_attempt($attempt, $quiz, $cm, $course, false);
$options = mod_quiz_display_options::make_from_quiz($quiz, quiz_attempt_state($quiz, $attempt));
if ($options->manualcomment == question_display_options::HIDDEN) {
// User cannot currently see the feedback, so don't message them.
// However, this may change in future, so leave them on the list.
continue;
}
if (!has_capability('mod/quiz:emailnotifyattemptgraded', $coursecontext, $attempt->userid, false)) {
// User not eligible to get a notification. Mark them done while doing nothing.
$DB->set_field('quiz_attempts', 'gradednotificationsenttime', $attempt->timefinish, ['id' => $attempt->id]);
continue;
}
// OK, send notification.
mtrace('Sending email to user ' . $attempt->userid . '...');
$ok = quiz_send_notify_manual_graded_message($attemptobj, core_user::get_user($attempt->userid));
if ($ok) {
mtrace('Send email successfully!');
$attempt->gradednotificationsenttime = $this->get_time();
$DB->set_field('quiz_attempts', 'gradednotificationsenttime', $attempt->gradednotificationsenttime,
['id' => $attempt->id]);
$attemptobj->fire_attempt_manual_grading_completed_event();
}
}
$attempts->close();
}
/**
* Get a number of records as an array of quiz_attempts using a SQL statement.
*
* @return moodle_recordset Of quiz_attempts that need to be processed.
*/
public function get_list_of_attempts(): moodle_recordset {
global $DB;
$delaytime = $this->get_time() - get_config('quiz', 'notifyattemptgradeddelay');
$sql = "SELECT qa.*
FROM {quiz_attempts} qa
JOIN {quiz} quiz ON quiz.id = qa.quiz
WHERE qa.state = 'finished'
AND qa.gradednotificationsenttime IS NULL
AND qa.sumgrades IS NOT NULL
AND qa.timemodified < :delaytime
ORDER BY quiz.course, qa.quiz";
return $DB->get_recordset_sql($sql, ['delaytime' => $delaytime]);
}
}
+7
View File
@@ -192,5 +192,12 @@ $capabilities = [
'contextlevel' => CONTEXT_MODULE,
'archetypes' => []
],
// Receive a notification message when a quiz attempt manual graded.
'mod/quiz:emailnotifyattemptgraded' => [
'captype' => 'read',
'contextlevel' => CONTEXT_MODULE,
'archetypes' => []
],
];
+3 -2
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="mod/quiz/db" VERSION="20200630" COMMENT="XMLDB file for Moodle mod/quiz"
<XMLDB PATH="mod/quiz/db" VERSION="20211019" COMMENT="XMLDB file for Moodle mod/quiz"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd"
>
@@ -143,6 +143,7 @@
<FIELD NAME="timemodifiedoffline" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Last modified time via web services."/>
<FIELD NAME="timecheckstate" TYPE="int" LENGTH="10" NOTNULL="false" DEFAULT="0" SEQUENCE="false" COMMENT="Next time quiz cron should check attempt for state changes. NULL means never check."/>
<FIELD NAME="sumgrades" TYPE="number" LENGTH="10" NOTNULL="false" SEQUENCE="false" DECIMALS="5" COMMENT="Total marks for this attempt."/>
<FIELD NAME="gradednotificationsenttime" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="The timestamp when the 'graded' notification was sent."/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
@@ -199,4 +200,4 @@
</KEYS>
</TABLE>
</TABLES>
</XMLDB>
</XMLDB>
+7
View File
@@ -46,4 +46,11 @@ $messageproviders = array(
'airnotifier' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_LOGGEDIN + MESSAGE_DEFAULT_LOGGEDOFF,
),
),
'attempt_grading_complete' => [
'capability' => 'mod/quiz:emailnotifyattemptgraded',
'defaults' => [
'airnotifier' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_LOGGEDIN + MESSAGE_DEFAULT_LOGGEDOFF,
],
],
);
+9
View File
@@ -52,5 +52,14 @@ $tasks = [
'day' => '*',
'dayofweek' => '*',
'month' => '*'
],
[
'classname' => 'mod_quiz\task\quiz_notify_attempt_manual_grading_completed',
'blocking' => 0,
'minute' => 'R',
'hour' => '*',
'day' => '*',
'dayofweek' => '*',
'month' => '*'
]
];
+17
View File
@@ -102,6 +102,23 @@ function xmldb_quiz_upgrade($oldversion) {
upgrade_mod_savepoint(true, 2021052503, 'quiz');
}
if ($oldversion < 2021101900) {
// Define field gradednotificationsenttime to be added to quiz_attempts.
$table = new xmldb_table('quiz_attempts');
$field = new xmldb_field('gradednotificationsenttime', XMLDB_TYPE_INTEGER, '10', null, null, null, null, 'sumgrades');
// Conditionally launch add field gradednotificationsenttime.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
$DB->execute('UPDATE {quiz_attempts} SET gradednotificationsenttime = timefinish');
}
// Quiz savepoint reached.
upgrade_mod_savepoint(true, 2021101900, 'quiz');
}
return true;
}
+14
View File
@@ -94,6 +94,8 @@ $string['attempterrorinvalid'] = 'Invalid quiz attempt ID';
$string['attempterrorcontentchange'] = 'This quiz preview no longer exists. (When a quiz is edited, any in-progress previews are automatically deleted.)';
$string['attempterrorcontentchangeforuser'] = 'This quiz attempt no longer exists.';
$string['attemptfirst'] = 'First attempt';
$string['attemptgradeddelay'] = 'Delay before sending attempt graded notifications';
$string['attemptgradeddelay_desc'] = 'A delay is applied before emailing the student to tell them their quiz attempt has now been graded. This is a trade-off. We want to notify the student as soon as possible. However, the teacher may go back and edit the grade again, and we don\'t want to notify the student before that happens';
$string['attemptincomplete'] = 'That attempt (by {$a}) is not yet completed.';
$string['attemptlast'] = 'Last attempt';
$string['attemptnumber'] = 'Attempt';
@@ -325,6 +327,13 @@ $string['emailnotifybody'] = 'Hi {$a->username},
You can review this attempt at {$a->quizreviewurl}.';
$string['emailnotifysmall'] = '{$a->studentname} has completed {$a->quizname}. See {$a->quizreviewurl}';
$string['emailnotifysubject'] = '{$a->studentname} has completed {$a->quizname}';
$string['emailmanualgradedbody'] = 'Hi {$a->studentname},
Your answers to \'{$a->quizname}\' in course \'{$a->coursename}\' at {$a->attempttimefinish} have now been graded.
You will be able to view your score and feedback by visiting \'{$a->quizurl}\' and reviewing your attempt.';
$string['emailmanualgradedsubject'] = 'Your attempt at {$a->quizname} has been graded';
$string['emailoverduebody'] = 'Hi {$a->studentname},
You started an attempt at \'{$a->quizname}\' in course \'{$a->coursename}\', but you never submitted it. It should have been submitted by {$a->attemptduedate}.
@@ -344,6 +353,7 @@ $string['essay'] = 'Essay';
$string['essayquestions'] = 'Questions';
$string['eventattemptautosaved'] = 'Quiz attempt auto-saved';
$string['eventattemptdeleted'] = 'Quiz attempt deleted';
$string['eventattemptmanualgradingcomplete'] = 'Quiz attempt manual grading complete';
$string['eventattemptpreviewstarted'] = 'Quiz attempt preview started';
$string['eventattemptquestionrestarted'] = 'Quiz attempt question restarted';
$string['eventattemptreviewed'] = 'Quiz attempt reviewed';
@@ -501,6 +511,7 @@ $string['matchanswer'] = 'Matching answer';
$string['matchanswerno'] = 'Matching answer {$a}';
$string['messageprovider:attempt_overdue'] = 'Warning when your quiz attempt becomes overdue';
$string['messageprovider:confirmation'] = 'Confirmation of your own quiz submissions';
$string['messageprovider:attempt_grading_complete'] = 'Notification that your attempt has been graded';
$string['messageprovider:submission'] = 'Notification of your students\' quiz submissions';
$string['max'] = 'Max';
$string['maxmark'] = 'Maximum mark';
@@ -579,6 +590,7 @@ $string['noscript'] = 'JavaScript must be enabled to continue!';
$string['notavailabletostudents'] = 'Note: This quiz is not currently available to your students';
$string['notenoughrandomquestions'] = 'There are not enough questions in category {$a->category} to create the question {$a->name} ({$a->id}).';
$string['notenoughsubquestions'] = 'Not enough sub-questions have been defined!<br />Do you want to go back and fix this question?';
$string['notifyattemptsgradedtask'] = 'Send notifications about quiz attempts that are now fully graded';
$string['notimedependentitems'] = 'Time dependent items are not currently supported by the quiz module. As a work around, set a time limit for the whole quiz. Do you wish to choose a different item (or use the current item regardless)?';
$string['notyetgraded'] = 'Not yet graded';
$string['notyetviewed'] = 'Not yet viewed';
@@ -669,6 +681,7 @@ $string['privacy:metadata:quiz'] = 'The quiz activity makes use of quiz reports.
$string['privacy:metadata:quiz_attempts'] = 'Details about each attempt on a quiz.';
$string['privacy:metadata:quiz_attempts:attempt'] = 'The attempt number.';
$string['privacy:metadata:quiz_attempts:currentpage'] = 'The current page that the user is on.';
$string['privacy:metadata:quiz_attempts:gradednotificationsenttime'] = 'The time the user was notified that manual grading of their attempt was complete';
$string['privacy:metadata:quiz_attempts:preview'] = 'Whether this is a preview of the quiz.';
$string['privacy:metadata:quiz_attempts:state'] = 'The current state of the attempt.';
$string['privacy:metadata:quiz_attempts:sumgrades'] = 'The sum of grades in the attempt.';
@@ -736,6 +749,7 @@ $string['quizcloseson'] = 'This quiz will close on {$a}.';
$string['quiz:deleteattempts'] = 'Delete quiz attempts';
$string['quiz:emailconfirmsubmission'] = 'Receive confirmation of your own quiz submissions';
$string['quiz:emailnotifysubmission'] = 'Receive notification of your students\' quiz submissions';
$string['quiz:emailnotifyattemptgraded'] = 'Receive notification when your attempt has been graded';
$string['quiz:emailwarnoverdue'] = 'Receive warning when your quiz attempt becomes overdue';
$string['quiz:grade'] = 'Grade quizzes manually';
$string['quiz:ignoretimelimits'] = 'Ignore quiz time limit';
+46
View File
@@ -126,6 +126,7 @@ function quiz_create_attempt(quiz $quizobj, $attemptnumber, $lastattempt, $timen
$attempt->state = quiz_attempt::IN_PROGRESS;
$attempt->currentpage = 0;
$attempt->sumgrades = null;
$attempt->gradednotificationsenttime = null;
// If this is a preview, mark it as such.
if ($ispreview) {
@@ -1884,6 +1885,51 @@ function quiz_attempt_submitted_handler($event) {
context_module::instance($cm->id), $cm, $eventdata['other']['studentisonline']);
}
/**
* Send the notification message when a quiz attempt has been manual graded.
*
* @param quiz_attempt $attemptobj Some data about the quiz attempt.
* @param object $userto
* @return int|false As for message_send.
*/
function quiz_send_notify_manual_graded_message(quiz_attempt $attemptobj, object $userto): ?int {
global $CFG;
$quizname = format_string($attemptobj->get_quiz_name());
$a = new stdClass();
// Course info.
$a->courseid = $attemptobj->get_courseid();
$a->coursename = format_string($attemptobj->get_course()->fullname);
// Quiz info.
$a->quizname = $quizname;
$a->quizurl = $CFG->wwwroot . '/mod/quiz/view.php?id=' . $attemptobj->get_cmid();
// Attempt info.
$a->attempttimefinish = userdate($attemptobj->get_attempt()->timefinish);
// Student's info.
$a->studentidnumber = $userto->idnumber;
$a->studentname = fullname($userto);
$eventdata = new \core\message\message();
$eventdata->component = 'mod_quiz';
$eventdata->name = 'attempt_grading_complete';
$eventdata->userfrom = core_user::get_noreply_user();
$eventdata->userto = $userto;
$eventdata->subject = get_string('emailmanualgradedsubject', 'quiz', $a);
$eventdata->fullmessage = get_string('emailmanualgradedbody', 'quiz', $a);
$eventdata->fullmessageformat = FORMAT_PLAIN;
$eventdata->fullmessagehtml = '';
$eventdata->notification = 1;
$eventdata->contexturl = $a->quizurl;
$eventdata->contexturlname = $a->quizname;
// Send the message.
return message_send($eventdata);
}
/**
* Handle groups_member_added event
*
+4
View File
@@ -66,6 +66,10 @@ if ($ADMIN->fulltree) {
$setting->set_advanced_flag_options(admin_setting_flag::ENABLED, false);
$quizsettings->add($setting);
// Delay to notify graded attempts.
$quizsettings->add(new admin_setting_configduration('quiz/notifyattemptgradeddelay',
get_string('attemptgradeddelay', 'quiz'), get_string('attemptgradeddelay_desc', 'quiz'), 5 * HOURSECS, HOURSECS));
// What to do with overdue attempts.
$quizsettings->add(new mod_quiz_admin_setting_overduehandling('quiz/overduehandling',
get_string('overduehandling', 'quiz'), get_string('overduehandling_desc', 'quiz'),
+38
View File
@@ -888,4 +888,42 @@ class mod_quiz_events_testcase extends advanced_testcase {
$this->assertEquals(context_module::instance($quiz->cmid), $event->get_context());
$this->assertEventContextNotUsed($event);
}
/**
* Test the attempt notify manual graded event.
* There is no external API for notification email when manual grading of user's attempt is completed,
* so the unit test will simply create and trigger the event and ensure the event data is returned as expected.
*/
public function test_attempt_manual_grading_completed() {
$this->resetAfterTest();
list($quizobj, $quba, $attempt) = $this->prepare_quiz_data();
$attemptobj = quiz_attempt::create($attempt->id);
$params = [
'objectid' => $attemptobj->get_attemptid(),
'relateduserid' => $attemptobj->get_userid(),
'courseid' => $attemptobj->get_course()->id,
'context' => context_module::instance($attemptobj->get_cmid()),
'other' => [
'quizid' => $attemptobj->get_quizid()
]
];
$event = \mod_quiz\event\attempt_manual_grading_completed::create($params);
// Catch the event.
$sink = $this->redirectEvents();
$event->trigger();
$events = $sink->get_events();
$sink->close();
// Validate the event.
$this->assertCount(1, $events);
$event = reset($events);
$this->assertInstanceOf('\mod_quiz\event\attempt_manual_grading_completed', $event);
$this->assertEquals('quiz_attempts', $event->objecttable);
$this->assertEquals($quizobj->get_context(), $event->get_context());
$this->assertEquals($attempt->userid, $event->relateduserid);
$this->assertNotEmpty($event->get_description());
$this->assertEventContextNotUsed($event);
}
}
@@ -0,0 +1,275 @@
<?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 containing unit tests for the quiz notify attempt manual grading completed cron task.
*
* @package mod_quiz
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_quiz;
use advanced_testcase;
use context_course;
use context_module;
use mod_quiz\task\quiz_notify_attempt_manual_grading_completed;
use question_engine;
use quiz;
use quiz_attempt;
use stdClass;
defined('MOODLE_INTERNAL') || die();
/**
* Class containing unit tests for the quiz notify attempt manual grading completed cron task.
*
* @package mod_quiz
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class quiz_notify_attempt_manual_grading_completed_test extends advanced_testcase {
/** @var \stdClass $course Test course to contain quiz. */
protected $course;
/** @var \stdClass $quiz A test quiz. */
protected $quiz;
/** @var context The quiz context. */
protected $context;
/** @var stdClass The course_module. */
protected $cm;
/** @var stdClass The student test. */
protected $student;
/** @var stdClass The teacher test. */
protected $teacher;
/** @var quiz Object containing the quiz settings. */
protected $quizobj;
/** @var question_usage_by_activity The question usage for this quiz attempt. */
protected $quba;
/**
* Standard test setup.
*
* Create a course with a quiz and a student and a(n editing) teacher.
* the quiz has a truefalse question and an essay question.
*
* Also create some bits of a quiz attempt to be used later.
*/
public function setUp(): void {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
// Setup test data.
$this->course = $this->getDataGenerator()->create_course();
$this->quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $this->course->id]);
$this->context = context_module::instance($this->quiz->cmid);
$this->cm = get_coursemodule_from_instance('quiz', $this->quiz->id);
// Create users.
$this->student = self::getDataGenerator()->create_user();
$this->teacher = self::getDataGenerator()->create_user();
// Users enrolments.
$studentrole = $DB->get_record('role', ['shortname' => 'student']);
$teacherrole = $DB->get_record('role', ['shortname' => 'editingteacher']);
// Allow student to receive messages.
$coursecontext = context_course::instance($this->course->id);
assign_capability('mod/quiz:emailnotifyattemptgraded', CAP_ALLOW, $studentrole->id, $coursecontext, true);
$this->getDataGenerator()->enrol_user($this->student->id, $this->course->id, $studentrole->id);
$this->getDataGenerator()->enrol_user($this->teacher->id, $this->course->id, $teacherrole->id);
// Make a quiz.
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$this->quiz = $quizgenerator->create_instance(['course' => $this->course->id, 'questionsperpage' => 0,
'grade' => 100.0, 'sumgrades' => 2]);
// Create a truefalse question and an essay question.
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $questiongenerator->create_question_category();
$truefalse = $questiongenerator->create_question('truefalse', null, ['category' => $cat->id]);
$essay = $questiongenerator->create_question('essay', null, ['category' => $cat->id]);
// Add them to the quiz.
quiz_add_quiz_question($truefalse->id, $this->quiz);
quiz_add_quiz_question($essay->id, $this->quiz);
$this->quizobj = quiz::create($this->quiz->id);
$this->quba = question_engine::make_questions_usage_by_activity('mod_quiz', $this->quizobj->get_context());
$this->quba->set_preferred_behaviour($this->quizobj->get_quiz()->preferredbehaviour);
}
/**
* Test SQL querry get list attempt in condition.
*/
public function test_get_list_of_attempts_within_conditions() {
global $DB;
$timenow = time();
// Create an attempt to be completely graded (one hour ago).
$attempt1 = quiz_create_attempt($this->quizobj, 1, null, $timenow - HOURSECS, false, $this->student->id);
quiz_start_new_attempt($this->quizobj, $this->quba, $attempt1, 1, $timenow - HOURSECS);
quiz_attempt_save_started($this->quizobj, $this->quba, $attempt1);
// Process some responses from the student (30 mins ago) and submit (20 mins ago).
$attemptobj1 = quiz_attempt::create($attempt1->id);
$tosubmit = [2 => ['answer' => 'Student 1 answer', 'answerformat' => FORMAT_HTML]];
$attemptobj1->process_submitted_actions($timenow - 30 * MINSECS, false, $tosubmit);
$attemptobj1->process_finish($timenow - 20 * MINSECS, false);
// Finish the attempt of student (now).
$attemptobj1->get_question_usage()->manual_grade(2, 'Good!', 1, FORMAT_HTML);
question_engine::save_questions_usage_by_activity($attemptobj1->get_question_usage());
$update = new stdClass();
$update->id = $attemptobj1->get_attemptid();
$update->timemodified = $timenow;
$update->sumgrades = $attemptobj1->get_question_usage()->get_total_mark();
$DB->update_record('quiz_attempts', $update);
quiz_save_best_grade($attemptobj1->get_quiz(), $this->student->id);
// Not quite time to send yet.
$task = new quiz_notify_attempt_manual_grading_completed();
$task->set_time_for_testing($timenow + 5 * HOURSECS - 1);
$attempts = $task->get_list_of_attempts();
$this->assertEquals(0, iterator_count($attempts));
// After time to send.
$task->set_time_for_testing($timenow + 5 * HOURSECS + 1);
$attempts = $task->get_list_of_attempts();
$this->assertEquals(1, iterator_count($attempts));
}
/**
* Test SQL query does not return attempts if the grading is not complete yet.
*/
public function test_get_list_of_attempts_without_manual_graded() {
$timenow = time();
// Create an attempt which won't be graded (1 hour ago).
$attempt2 = quiz_create_attempt($this->quizobj, 2, null, $timenow - HOURSECS, false, $this->student->id);
quiz_start_new_attempt($this->quizobj, $this->quba, $attempt2, 2, $timenow - HOURSECS);
quiz_attempt_save_started($this->quizobj, $this->quba, $attempt2);
// Process some responses from the student (30 mins ago) and submit (now).
$attemptobj2 = quiz_attempt::create($attempt2->id);
$tosubmit = [2 => ['answer' => 'Answer of student 2.', 'answerformat' => FORMAT_HTML]];
$attemptobj2->process_submitted_actions($timenow - 30 * MINSECS, false, $tosubmit);
$attemptobj2->process_finish($timenow, false);
// After time to notify, except attempt not graded, so it won't appear.
$task = new quiz_notify_attempt_manual_grading_completed();
$task->set_time_for_testing($timenow + 5 * HOURSECS + 1);
$attempts = $task->get_list_of_attempts();
$this->assertEquals(0, iterator_count($attempts));
}
/**
* Test notify manual grading completed task which the user attempt has not capability.
*/
public function test_notify_manual_grading_completed_task_without_capability() {
global $DB;
// Create an attempt for a user without the capability.
$timenow = time();
$attempt = quiz_create_attempt($this->quizobj, 3, null, $timenow, false, $this->teacher->id);
quiz_start_new_attempt($this->quizobj, $this->quba, $attempt, 3, $timenow - HOURSECS);
quiz_attempt_save_started($this->quizobj, $this->quba, $attempt);
// Process some responses and submit.
$attemptobj = quiz_attempt::create($attempt->id);
$tosubmit = [2 => ['answer' => 'Answer of teacher.', 'answerformat' => FORMAT_HTML]];
$attemptobj->process_submitted_actions($timenow - 30 * MINSECS, false, $tosubmit);
$attemptobj->process_finish($timenow - 20 * MINSECS, false);
// Grade the attempt.
$attemptobj->get_question_usage()->manual_grade(2, 'Good!', 1, FORMAT_HTML);
question_engine::save_questions_usage_by_activity($attemptobj->get_question_usage());
$update = new stdClass();
$update->id = $attemptobj->get_attemptid();
$update->timemodified = $timenow;
$update->sumgrades = $attemptobj->get_question_usage()->get_total_mark();
$DB->update_record('quiz_attempts', $update);
quiz_save_best_grade($attemptobj->get_quiz(), $this->student->id);
// Run the quiz notify attempt manual graded task.
ob_start();
$task = new quiz_notify_attempt_manual_grading_completed();
$task->set_time_for_testing($timenow + 5 * HOURSECS + 1);
$task->execute();
ob_get_clean();
$attemptobj = quiz_attempt::create($attempt->id);
$this->assertEquals($attemptobj->get_attempt()->timefinish, $attemptobj->get_attempt()->gradednotificationsenttime);
}
/**
* Test notify manual grading completed task which the user attempt has capability.
*/
public function test_notify_manual_grading_completed_task_with_capability() {
global $DB;
// Create an attempt with capability.
$timenow = time();
$attempt = quiz_create_attempt($this->quizobj, 4, null, $timenow, false, $this->student->id);
quiz_start_new_attempt($this->quizobj, $this->quba, $attempt, 4, $timenow - HOURSECS);
quiz_attempt_save_started($this->quizobj, $this->quba, $attempt);
// Process some responses from the student.
$attemptobj = quiz_attempt::create($attempt->id);
$tosubmit = [2 => ['answer' => 'Answer of student.', 'answerformat' => FORMAT_HTML]];
$attemptobj->process_submitted_actions($timenow - 30 * MINSECS, false, $tosubmit);
$attemptobj->process_finish($timenow - 20 * MINSECS, false);
// Finish the attempt of student.
$attemptobj->get_question_usage()->manual_grade(2, 'Good!', 1, FORMAT_HTML);
question_engine::save_questions_usage_by_activity($attemptobj->get_question_usage());
$update = new stdClass();
$update->id = $attemptobj->get_attemptid();
$update->timemodified = $timenow;
$update->sumgrades = $attemptobj->get_question_usage()->get_total_mark();
$DB->update_record('quiz_attempts', $update);
quiz_save_best_grade($attemptobj->get_quiz(), $this->student->id);
// Run the quiz notify attempt manual graded task.
ob_start();
$task = new quiz_notify_attempt_manual_grading_completed();
$task->set_time_for_testing($timenow + 5 * HOURSECS + 1);
$task->execute();
ob_get_clean();
$attemptobj = quiz_attempt::create($attempt->id);
$this->assertNotEquals(null, $attemptobj->get_attempt()->gradednotificationsenttime);
$this->assertNotEquals($attemptobj->get_attempt()->timefinish, $attemptobj->get_attempt()->gradednotificationsenttime);
}
}
+1 -1
View File
@@ -24,6 +24,6 @@
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2021052503;
$plugin->version = 2021101900;
$plugin->requires = 2021052500;
$plugin->component = 'mod_quiz';