From 54bf1cf7dcfbaaf307ffdb2c1c4dfe296a9f6533 Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Mon, 4 Jul 2011 18:58:34 +0100 Subject: [PATCH] MDL-28166 send quiz event notifications asynchronously. This avoids the problem that you cannot send messages in transactions. It also means that the quiz submission will not be prevented, and the message will still be sent eventually, if any part of the messaging system is giving intermittent errors when the student wants to submit their quiz. --- mod/quiz/attemptlib.php | 26 +++-- mod/quiz/db/access.php | 4 +- mod/quiz/db/events.php | 61 ++++++++++ mod/quiz/lang/en/quiz.php | 6 +- mod/quiz/locallib.php | 215 ++++++++++++++++++------------------ mod/quiz/processattempt.php | 14 --- mod/quiz/startattempt.php | 13 ++- mod/quiz/version.php | 2 +- 8 files changed, 196 insertions(+), 145 deletions(-) create mode 100755 mod/quiz/db/events.php diff --git a/mod/quiz/attemptlib.php b/mod/quiz/attemptlib.php index adf1182387591..aedd15c973d43 100644 --- a/mod/quiz/attemptlib.php +++ b/mod/quiz/attemptlib.php @@ -995,14 +995,6 @@ public function check_file_access($slot, $reviewing, $contextid, $component, $component, $filearea, $args, $forcedownload); } - /** - * Triggers the sending of the notification emails at the end of this attempt. - */ - public function quiz_send_notification_emails() { - quiz_send_notification_emails($this->get_course(), $this->get_quiz(), $this->attempt, - $this->quizobj->get_context(), $this->get_cm()); - } - /** * Get the navigation panel object for this attempt. * @@ -1080,7 +1072,7 @@ public function save_question_flags() { } public function finish_attempt($timestamp) { - global $DB; + global $DB, $USER; $this->quba->process_all_actions($timestamp); $this->quba->finish_all_questions($timestamp); @@ -1093,7 +1085,21 @@ public function finish_attempt($timestamp) { if (!$this->is_preview()) { quiz_save_best_grade($this->get_quiz()); - $this->quiz_send_notification_emails(); + + // Trigger event + $eventdata = new stdClass(); + $eventdata->component = 'mod_quiz'; + $eventdata->attemptid = $this->attempt->id; + $eventdata->timefinish = $this->attempt->timefinish; + $eventdata->userid = $this->attempt->userid; + $eventdata->submitterid = $USER->id; + $eventdata->quizid = $this->get_quizid(); + $eventdata->cmid = $this->get_cmid(); + $eventdata->courseid = $this->get_courseid(); + events_trigger('quiz_attempt_submitted', $eventdata); + + // Clear the password check flag in the session. + $this->get_access_manager($timestamp)->clear_password_access(); } } diff --git a/mod/quiz/db/access.php b/mod/quiz/db/access.php index a792b6ac0aa8d..485cc246a44e3 100644 --- a/mod/quiz/db/access.php +++ b/mod/quiz/db/access.php @@ -149,14 +149,14 @@ 'archetypes' => array() ), - // Receive email confirmation of own quiz submission + // Receive a confirmation message of own quiz submission. 'mod/quiz:emailconfirmsubmission' => array( 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array() ), - // Receive email notification of other peoples quiz submissions + // Receive a notification message of other peoples' quiz submissions. 'mod/quiz:emailnotifysubmission' => array( 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, diff --git a/mod/quiz/db/events.php b/mod/quiz/db/events.php new file mode 100755 index 0000000000000..bfb7b5c3527b1 --- /dev/null +++ b/mod/quiz/db/events.php @@ -0,0 +1,61 @@ +. + +/** + * Post-install code for the quiz module. + * + * @package mod + * @subpackage quiz + * @copyright 2011 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +defined('MOODLE_INTERNAL') || die(); + + +$handlers = array( + // Handle our own quiz_attempt_submitted event, as a way to send confirmation + // messages asynchronously. + 'quiz_attempt_submitted' => array ( + 'handlerfile' => '/mod/quiz/locallib.php', + 'handlerfunction' => 'quiz_attempt_submitted_handler', + 'schedule' => 'cron', + ), +); + +/* List of events generated by the quiz module, with the fields on the event object. + +quiz_attempt_started + ->component = 'mod_quiz'; + ->attemptid = // The id of the new quiz attempt. + ->timestart = // The timestamp of when the attempt was started. + ->userid = // The user id that the attempt belongs to. + ->quizid = // The quiz id of the quiz the attempt belongs to. + ->cmid = // The course_module id of the quiz the attempt belongs to. + ->courseid = // The course id of the course the quiz belongs to. + +quiz_attempt_submitted + ->component = 'mod_quiz'; + ->attemptid = // The id of the quiz attempt that was submitted. + ->timefinish = // The timestamp of when the attempt was submitted. + ->userid = // The user id that the attempt belongs to. + ->submitterid = // The user id of the user who sumitted the attempt. + ->quizid = // The quiz id of the quiz the attempt belongs to. + ->cmid = // The course_module id of the quiz the attempt belongs to. + ->courseid = // The course id of the course the quiz belongs to. + +*/ diff --git a/mod/quiz/lang/en/quiz.php b/mod/quiz/lang/en/quiz.php index f788ef5daeb94..deaf6a7ac0331 100644 --- a/mod/quiz/lang/en/quiz.php +++ b/mod/quiz/lang/en/quiz.php @@ -256,7 +256,7 @@ in course \'{$a->coursename}\' at {$a->submissiontime}. -This email confirms that we have safely received your answers. +This message confirms that we have safely received your answers. You can access this quiz at {$a->quizurl}.'; $string['emailconfirmsmall'] = 'Thank you for submitting your answers to \'{$a->quizname}\''; @@ -550,8 +550,8 @@ $string['quizcloses'] = 'Quiz closes'; $string['quizcloseson'] = 'This quiz will close at {$a}'; $string['quiz:deleteattempts'] = 'Delete quiz attempts'; -$string['quiz:emailconfirmsubmission'] = 'Get email confirmation when submitting'; -$string['quiz:emailnotifysubmission'] = 'Get email notification of submissions'; +$string['quiz:emailconfirmsubmission'] = 'Get a confirmation message when submitting'; +$string['quiz:emailnotifysubmission'] = 'Get a notification message when an attempt is submitted'; $string['quiz:grade'] = 'Grade quizzes manually'; $string['quiz:ignoretimelimits'] = 'Ignores time limit on quizzes'; $string['quizisclosed'] = 'This quiz is closed'; diff --git a/mod/quiz/locallib.php b/mod/quiz/locallib.php index 0e5718215db21..8cad96ad9cda9 100644 --- a/mod/quiz/locallib.php +++ b/mod/quiz/locallib.php @@ -1092,38 +1092,33 @@ function quiz_get_slot_for_question($quiz, $questionid) { return null; } -/// FUNCTIONS FOR SENDING NOTIFICATION EMAILS /////////////////////////////// +/// FUNCTIONS FOR SENDING NOTIFICATION MESSAGES /////////////////////////////// /** - * Sends confirmation email to the student taking the course + * Sends a confirmation message to the student confirming that the attempt was processed. * - * @param object $a associative array of replaceable fields for the templates + * @param object $a lots of useful information that can be used in the message + * subject and body. * - * @return bool + * @return int|false as for {@link message_send()}. */ -function quiz_send_confirmation($a) { - - global $USER; - - // recipient is self - $a->useridnumber = $USER->idnumber; - $a->username = fullname($USER); - $a->userusername = $USER->username; +function quiz_send_confirmation($recipient, $a) { - // fetch the subject and body from strings - $subject = get_string('emailconfirmsubject', 'quiz', $a); - $body = get_string('emailconfirmbody', 'quiz', $a); + // Add information about the recipient to $a + // Don't do idnumber. we want idnumber to be the submitter's idnumber. + $a->username = fullname($recipient); + $a->userusername = $recipient->username; - // send email and analyse result + // Prepare message $eventdata = new stdClass(); - $eventdata->component = 'mod_quiz'; - $eventdata->name = 'confirmation'; + $eventdata->component = 'mod_quiz'; + $eventdata->name = 'confirmation'; $eventdata->notification = 1; $eventdata->userfrom = get_admin(); - $eventdata->userto = $USER; - $eventdata->subject = $subject; - $eventdata->fullmessage = $body; + $eventdata->userto = $recipient; + $eventdata->subject = get_string('emailconfirmsubject', 'quiz', $a); + $eventdata->fullmessage = get_string('emailconfirmbody', 'quiz', $a); $eventdata->fullmessageformat = FORMAT_PLAIN; $eventdata->fullmessagehtml = ''; @@ -1131,7 +1126,8 @@ function quiz_send_confirmation($a) { $eventdata->contexturl = $a->quizurl; $eventdata->contexturlname = $a->quizname; - return (bool)message_send($eventdata); // returns message id or false + // ... and send it. + return message_send($eventdata); } /** @@ -1140,30 +1136,27 @@ function quiz_send_confirmation($a) { * @param object $recipient user object of the intended recipient * @param object $a associative array of replaceable fields for the templates * - * @return bool + * @return int|false as for {@link message_send()}. */ -function quiz_send_notification($recipient, $a) { +function quiz_send_notification($recipient, $submitter, $a) { global $USER; - // recipient info for template - $a->username = fullname($recipient); + // Recipient info for template + $a->useridnumber = $recipient->idnumber; + $a->username = fullname($recipient); $a->userusername = $recipient->username; - // fetch the subject and body from strings - $subject = get_string('emailnotifysubject', 'quiz', $a); - $body = get_string('emailnotifybody', 'quiz', $a); - - // send email and analyse result + // Prepare message $eventdata = new stdClass(); - $eventdata->component = 'mod_quiz'; - $eventdata->name = 'submission'; + $eventdata->component = 'mod_quiz'; + $eventdata->name = 'submission'; $eventdata->notification = 1; - $eventdata->userfrom = $USER; + $eventdata->userfrom = $submitter; $eventdata->userto = $recipient; - $eventdata->subject = $subject; - $eventdata->fullmessage = $body; + $eventdata->subject = get_string('emailnotifysubject', 'quiz', $a); + $eventdata->fullmessage = get_string('emailnotifybody', 'quiz', $a); $eventdata->fullmessageformat = FORMAT_PLAIN; $eventdata->fullmessagehtml = ''; @@ -1171,12 +1164,12 @@ function quiz_send_notification($recipient, $a) { $eventdata->contexturl = $a->quizreviewurl; $eventdata->contexturlname = $a->quizname; - return (bool)message_send($eventdata); + // ... and send it. + return message_send($eventdata); } /** - * Takes a bunch of information to format into an email and send - * to the specified recipient. + * Send all the requried messages when a quiz attempt is submitted. * * @param object $course the course * @param object $quiz the quiz @@ -1184,39 +1177,35 @@ function quiz_send_notification($recipient, $a) { * @param object $context the quiz context * @param object $cm the coursemodule for this quiz * - * @return int number of emails sent + * @return bool true if all necessary messages were sent successfully, else false. */ -function quiz_send_notification_emails($course, $quiz, $attempt, $context, $cm) { - global $CFG, $USER; - // we will count goods and bads for error logging - $emailresult = array('good' => 0, 'fail' => 0); +function quiz_send_notification_messages($course, $quiz, $attempt, $context, $cm) { + global $CFG, $DB; - // do nothing if required objects not present + // Do nothing if required objects not present if (empty($course) or empty($quiz) or empty($attempt) or empty($context)) { - debugging('quiz_send_notification_emails: Email(s) not sent due to program error.', - DEBUG_DEVELOPER); - return $emailresult['fail']; + throw new coding_exception('$course, $quiz, $attempt, $context and $cm must all be set.'); } - // check for confirmation required + $submitter = $DB->get_record('user', array('id' => $attempt->userid), '*', MUST_EXIST); + + // Check for confirmation required $sendconfirm = false; $notifyexcludeusers = ''; - if (has_capability('mod/quiz:emailconfirmsubmission', $context, null, false)) { - // exclude from notify emails later - $notifyexcludeusers = $USER->id; - // send the email + if (has_capability('mod/quiz:emailconfirmsubmission', $context, $submitter, false)) { + $notifyexcludeusers = $submitter->id; $sendconfirm = true; } // check for notifications required - $notifyfields = 'u.id, u.username, u.firstname, u.lastname, u.email, u.lang, ' . - 'u.timezone, u.mailformat, u.maildisplay'; - $groups = groups_get_all_groups($course->id, $USER->id); + $notifyfields = 'u.id, u.username, u.firstname, u.lastname, u.idnumber, u.email, ' . + 'u.lang, u.timezone, u.mailformat, u.maildisplay'; + $groups = groups_get_all_groups($course->id, $submitter->id); if (is_array($groups) && count($groups) > 0) { $groups = array_keys($groups); } else if (groups_get_activity_groupmode($cm, $course) != NOGROUPS) { // If the user is not in a group, and the quiz is set to group mode, - // then set $gropus to a non-existant id so that only users with + // then set $groups to a non-existant id so that only users with // 'moodle/site:accessallgroups' get notified. $groups = -1; } else { @@ -1225,67 +1214,75 @@ function quiz_send_notification_emails($course, $quiz, $attempt, $context, $cm) $userstonotify = get_users_by_capability($context, 'mod/quiz:emailnotifysubmission', $notifyfields, '', '', '', $groups, $notifyexcludeusers, false, false, true); - // if something to send, then build $a - if (! empty($userstonotify) or $sendconfirm) { - $a = new stdClass(); - // course info - $a->coursename = $course->fullname; - $a->courseshortname = $course->shortname; - // quiz info - $a->quizname = $quiz->name; - $a->quizreporturl = $CFG->wwwroot . '/mod/quiz/report.php?id=' . $cm->id; - $a->quizreportlink = '' . - format_string($quiz->name) . ' report'; - $a->quizreviewurl = $CFG->wwwroot . '/mod/quiz/review.php?attempt=' . $attempt->id; - $a->quizreviewlink = '' . - format_string($quiz->name) . ' review'; - $a->quizurl = $CFG->wwwroot . '/mod/quiz/view.php?id=' . $cm->id; - $a->quizlink = '' . format_string($quiz->name) . ''; - // attempt info - $a->submissiontime = userdate($attempt->timefinish); - $a->timetaken = format_time($attempt->timefinish - $attempt->timestart); - // student who sat the quiz info - $a->studentidnumber = $USER->idnumber; - $a->studentname = fullname($USER); - $a->studentusername = $USER->username; + if (empty($userstonotify) && !$sendconfirm) { + return true; // Nothing to do. } - // send confirmation if required - if ($sendconfirm) { - // send the email and update stats - switch (quiz_send_confirmation($a)) { - case true: - $emailresult['good']++; - break; - case false: - $emailresult['fail']++; - break; - } - } - - // send notifications if required + $a = new stdClass(); + // Course info + $a->coursename = $course->fullname; + $a->courseshortname = $course->shortname; + // Quiz info + $a->quizname = $quiz->name; + $a->quizreporturl = $CFG->wwwroot . '/mod/quiz/report.php?id=' . $cm->id; + $a->quizreportlink = '' . + format_string($quiz->name) . ' report'; + $a->quizreviewurl = $CFG->wwwroot . '/mod/quiz/review.php?attempt=' . $attempt->id; + $a->quizreviewlink = '' . + format_string($quiz->name) . ' review'; + $a->quizurl = $CFG->wwwroot . '/mod/quiz/view.php?id=' . $cm->id; + $a->quizlink = '' . format_string($quiz->name) . ''; + // Attempt info + $a->submissiontime = userdate($attempt->timefinish); + $a->timetaken = format_time($attempt->timefinish - $attempt->timestart); + // Student who sat the quiz info + $a->studentidnumber = $submitter->idnumber; + $a->studentname = fullname($submitter); + $a->studentusername = $submitter->username; + + $allok = true; + + // Send notifications if required if (!empty($userstonotify)) { - // loop through recipients and send an email to each and update stats foreach ($userstonotify as $recipient) { - switch (quiz_send_notification($recipient, $a)) { - case true: - $emailresult['good']++; - break; - case false: - $emailresult['fail']++; - break; - } + $allok = $allok && quiz_send_notification($recipient, $submitter, $a); } } - // log errors sending emails if any - if (! empty($emailresult['fail'])) { - debugging('quiz_send_notification_emails:: ' . $emailresult['fail'] . - ' email(s) failed to be sent.', DEBUG_DEVELOPER); + // Send confirmation if required. We send the student confirmation last, so + // that if message sending is being intermittently buggy, which means we send + // some but not all messages, and then try again later, then teachers may get + // duplicate messages, but the student will always get exactly one. + if ($sendconfirm) { + $allok = $allok && quiz_send_confirmation($submitter, $a); + } + + return $allok; +} + +/** + * Handle the quiz_attempt_submitted event. + * + * This sends the confirmation and notification messages, if required. + * + * @param object $event the event object. + */ +function quiz_attempt_submitted_handler($event) { + global $DB; + + $course = $DB->get_record('course', array('id' => $event->courseid)); + $quiz = $DB->get_record('quiz', array('id' => $event->quizid)); + $cm = get_coursemodule_from_id('quiz', $event->cmid, $event->courseid); + $attempt = $DB->get_record('quiz_attempts', array('id' => $event->attemptid)); + + if (!($course && $quiz && $cm && $attempt)) { + // Something has been deleted since the event was raised. Therefore, the + // event is no longer relevant. + return true; } - // return the number of successfully sent emails - return $emailresult['good']; + return quiz_send_notification_messages($course, $quiz, $attempt, + get_context_instance(CONTEXT_MODULE, $cm->id), $cm); } /** diff --git a/mod/quiz/processattempt.php b/mod/quiz/processattempt.php index 7d74e19432ca8..79da301051279 100644 --- a/mod/quiz/processattempt.php +++ b/mod/quiz/processattempt.php @@ -112,20 +112,6 @@ // Update the quiz attempt record. $attemptobj->finish_attempt($timenow); -// Trigger event -$eventdata = new stdClass(); -$eventdata->component = 'mod_quiz'; -$eventdata->course = $attemptobj->get_courseid(); -$eventdata->quiz = $attemptobj->get_quizid(); -$eventdata->cm = $attemptobj->get_cmid(); -$eventdata->user = $USER; -$eventdata->attempt = $attemptobj->get_attemptid(); -events_trigger('quiz_attempt_processed', $eventdata); - -// Clear the password check flag in the session. -$accessmanager = $attemptobj->get_access_manager($timenow); -$accessmanager->clear_password_access(); - // Send the user to the review page. $transaction->allow_commit(); redirect($attemptobj->review_url()); diff --git a/mod/quiz/startattempt.php b/mod/quiz/startattempt.php index f44ecc54443de..4f85d373ca311 100644 --- a/mod/quiz/startattempt.php +++ b/mod/quiz/startattempt.php @@ -210,12 +210,13 @@ // Trigger event $eventdata = new stdClass(); -$eventdata->component = 'mod_quiz'; -$eventdata->course = $quizobj->get_courseid(); -$eventdata->quiz = $quizobj->get_quizid(); -$eventdata->cm = $quizobj->get_cmid(); -$eventdata->user = $USER; -$eventdata->attempt = $attempt->id; +$eventdata->component = 'mod_quiz'; +$eventdata->attemptid = $attempt->id; +$eventdata->timestart = $attempt->timestart; +$eventdata->userid = $attempt->userid; +$eventdata->quizid = $quizobj->get_quizid(); +$eventdata->cmid = $quizobj->get_cmid(); +$eventdata->courseid = $quizobj->get_courseid(); events_trigger('quiz_attempt_started', $eventdata); $transaction->allow_commit(); diff --git a/mod/quiz/version.php b/mod/quiz/version.php index 9b7fd7edc314a..1381c23225b34 100644 --- a/mod/quiz/version.php +++ b/mod/quiz/version.php @@ -25,6 +25,6 @@ defined('MOODLE_INTERNAL') || die(); -$module->version = 2011051250; +$module->version = 2011070100; $module->requires = 2011060313; $module->cron = 0;