Skip to content

Commit

Permalink
MDL-52456 Quiz: notification message for students.
Browse files Browse the repository at this point in the history
Notification message for students after questions have been manually graded.
  • Loading branch information
JBThong committed Oct 19, 2021
1 parent 385938b commit 46b8832
Show file tree
Hide file tree
Showing 18 changed files with 706 additions and 15 deletions.
37 changes: 36 additions & 1 deletion mod/quiz/attemptlib.php
Original file line number Diff line number Diff line change
Expand Up @@ -1007,6 +1007,15 @@ public function has_response_to_at_least_one_graded_question() {
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.
*
Expand Down Expand Up @@ -2202,6 +2211,14 @@ public function process_finish($timestamp, $processsubmitted, $timefinish = null
$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()) {
Expand Down Expand Up @@ -2651,6 +2668,25 @@ public function fire_attempt_reviewed_event() {
$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.
*
Expand All @@ -2668,7 +2704,6 @@ public function set_offline_modified_time($time) {
}
return false;
}

}


Expand Down
3 changes: 2 additions & 1 deletion mod/quiz/backup/moodle2/backup_quiz_stepslib.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ protected function define_structure() {

$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.
Expand Down
6 changes: 6 additions & 0 deletions mod/quiz/backup/moodle2/restore_quiz_stepslib.php
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,12 @@ protected function process_quiz_attempt($data) {
$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) {
Expand Down
69 changes: 69 additions & 0 deletions mod/quiz/classes/event/attempt_manual_grading_completed.php
Original file line number Diff line number Diff line change
@@ -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 changes: 2 additions & 0 deletions mod/quiz/classes/external.php
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,8 @@ private static function attempt_structure() {
'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),
)
);
}
Expand Down
24 changes: 14 additions & 10 deletions mod/quiz/classes/privacy/provider.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,16 +69,17 @@ public static function get_metadata(collection $items) : collection {
// 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
Expand Down Expand Up @@ -543,6 +544,9 @@ protected static function export_quiz_attempts(approved_contextlist $contextlist
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);
Expand Down
156 changes: 156 additions & 0 deletions mod/quiz/classes/task/quiz_notify_attempt_manual_grading_completed.php
Original file line number Diff line number Diff line change
@@ -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 changes: 7 additions & 0 deletions mod/quiz/db/access.php
Original file line number Diff line number Diff line change
Expand Up @@ -192,5 +192,12 @@
'contextlevel' => CONTEXT_MODULE,
'archetypes' => []
],

// Receive a notification message when a quiz attempt manual graded.
'mod/quiz:emailnotifyattemptgraded' => [
'captype' => 'read',
'contextlevel' => CONTEXT_MODULE,
'archetypes' => []
],
];

5 changes: 3 additions & 2 deletions mod/quiz/db/install.xml
Original file line number Diff line number Diff line change
@@ -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"
>
Expand Down Expand Up @@ -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"/>
Expand Down Expand Up @@ -199,4 +200,4 @@
</KEYS>
</TABLE>
</TABLES>
</XMLDB>
</XMLDB>
Loading

0 comments on commit 46b8832

Please sign in to comment.