3 * Library of functions used by the quiz module.
5 * This contains functions that are called from within the quiz module only
6 * Functions that are also called by core Moodle are in {@link lib.php}
7 * This script also loads the code in {@link questionlib.php} which holds
8 * the module-indpendent code for handling questions and which in turn
9 * initialises all the questiontype classes.
11 * @author Martin Dougiamas and many others. This has recently been completely
12 * rewritten by Alex Smith, Julian Sedding and Gustav Delius as part of
13 * the Serving Mathematics project
14 * {@link http://maths.york.ac.uk/serving_maths}
15 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
20 * Include those library functions that are also used by core Moodle or other modules
22 require_once($CFG->dirroot
. '/mod/quiz/lib.php');
23 require_once($CFG->dirroot
. '/question/editlib.php');
25 /// Constants ///////////////////////////////////////////////////////////////////
28 * Options determining how the grades from individual attempts are combined to give
29 * the overall grade for a user
31 define("QUIZ_GRADEHIGHEST", "1");
32 define("QUIZ_GRADEAVERAGE", "2");
33 define("QUIZ_ATTEMPTFIRST", "3");
34 define("QUIZ_ATTEMPTLAST", "4");
35 $QUIZ_GRADE_METHOD = array(
36 QUIZ_GRADEHIGHEST
=> get_string("gradehighest", "quiz"),
37 QUIZ_GRADEAVERAGE
=> get_string("gradeaverage", "quiz"),
38 QUIZ_ATTEMPTFIRST
=> get_string("attemptfirst", "quiz"),
39 QUIZ_ATTEMPTLAST
=> get_string("attemptlast", "quiz")
43 /// Functions related to attempts /////////////////////////////////////////
46 * Creates an object to represent a new attempt at a quiz
48 * Creates an attempt object to represent an attempt at the quiz by the current
49 * user starting at the current time. The ->id field is not set. The object is
50 * NOT written to the database.
51 * @return object The newly created attempt object.
52 * @param object $quiz The quiz to create an attempt for.
53 * @param integer $attemptnumber The sequence number for the attempt.
55 function quiz_create_attempt($quiz, $attemptnumber) {
58 if (!$attemptnumber > 1 or !$quiz->attemptonlast
or !$attempt = get_record('quiz_attempts', 'quiz', $quiz->id
, 'userid', $USER->id
, 'attempt', $attemptnumber-1)) {
59 // we are not building on last attempt so create a new attempt
60 $attempt->quiz
= $quiz->id
;
61 $attempt->userid
= $USER->id
;
62 $attempt->preview
= 0;
63 if ($quiz->shufflequestions
) {
64 $attempt->layout
= quiz_repaginate($quiz->questions
, $quiz->questionsperpage
, true);
66 $attempt->layout
= $quiz->questions
;
71 $attempt->attempt
= $attemptnumber;
72 $attempt->sumgrades
= 0.0;
73 $attempt->timestart
= $timenow;
74 $attempt->timefinish
= 0;
75 $attempt->timemodified
= $timenow;
76 $attempt->uniqueid
= question_new_attempt_uniqueid();
82 * Returns an unfinished attempt (if there is one) for the given
83 * user on the given quiz. This function does not return preview attempts.
85 * @param integer $quizid the id of the quiz.
86 * @param integer $userid the id of the user.
88 * @return mixed the unfinished attempt if there is one, false if not.
90 function quiz_get_user_attempt_unfinished($quizid, $userid) {
91 $attempts = quiz_get_user_attempts($quizid, $userid, 'unfinished', true);
93 return array_shift($attempts);
100 * @param integer $quizid the quiz id.
101 * @param integer $userid the userid.
102 * @param string $status 'all', 'finished' or 'unfinished' to control
103 * @return an array of all the user's attempts at this quiz. Returns an empty array if there are none.
105 function quiz_get_user_attempts($quizid, $userid, $status = 'finished', $includepreviews = false) {
106 $status_condition = array(
108 'finished' => ' AND timefinish > 0',
109 'unfinished' => ' AND timefinish = 0'
112 if (!$includepreviews) {
113 $previewclause = ' AND preview = 0';
115 if ($attempts = get_records_select('quiz_attempts',
116 "quiz = '$quizid' AND userid = '$userid'" . $previewclause . $status_condition[$status],
125 * Delete a quiz attempt.
127 function quiz_delete_attempt($attempt, $quiz) {
128 if (is_numeric($attempt)) {
129 if (!$attempt = get_record('quiz_attempts', 'id', $attempt)) {
134 if ($attempt->quiz
!= $quiz->id
) {
135 debugging("Trying to delete attempt $attempt->id which belongs to quiz $attempt->quiz " .
136 "but was passed quiz $quiz->id.");
140 delete_records('quiz_attempts', 'id', $attempt->id
);
141 delete_attempt($attempt->uniqueid
);
143 // Search quiz_attempts for other instances by this user.
144 // If none, then delete record for this quiz, this user from quiz_grades
145 // else recalculate best grade
147 $userid = $attempt->userid
;
148 if (!record_exists('quiz_attempts', 'userid', $userid, 'quiz', $quiz->id
)) {
149 delete_records('quiz_grades', 'userid', $userid,'quiz', $quiz->id
);
151 quiz_save_best_grade($quiz, $userid);
155 /// Functions to do with quiz layout and pages ////////////////////////////////
158 * Returns a comma separated list of question ids for the current page
160 * @return string Comma separated list of question ids
161 * @param string $layout The string representing the quiz layout. Each page is represented as a
162 * comma separated list of question ids and 0 indicating page breaks.
163 * So 5,2,0,3,0 means questions 5 and 2 on page 1 and question 3 on page 2
164 * @param integer $page The number of the current page.
166 function quiz_questions_on_page($layout, $page) {
167 $pages = explode(',0', $layout);
168 return trim($pages[$page], ',');
172 * Returns a comma separated list of question ids for the quiz
174 * @return string Comma separated list of question ids
175 * @param string $layout The string representing the quiz layout. Each page is represented as a
176 * comma separated list of question ids and 0 indicating page breaks.
177 * So 5,2,0,3,0 means questions 5 and 2 on page 1 and question 3 on page 2
179 function quiz_questions_in_quiz($layout) {
180 return str_replace(',0', '', $layout);
184 * Returns the number of pages in the quiz layout
186 * @return integer Comma separated list of question ids
187 * @param string $layout The string representing the quiz layout.
189 function quiz_number_of_pages($layout) {
190 return substr_count($layout, ',0');
194 * Returns the first question number for the current quiz page
196 * @return integer The number of the first question
197 * @param string $quizlayout The string representing the layout for the whole quiz
198 * @param string $pagelayout The string representing the layout for the current page
200 function quiz_first_questionnumber($quizlayout, $pagelayout) {
201 // this works by finding all the questions from the quizlayout that
202 // come before the current page and then adding up their lengths.
204 $start = strpos($quizlayout, ','.$pagelayout.',')-2;
206 $prevlist = substr($quizlayout, 0, $start);
207 return get_field_sql("SELECT sum(length)+1 FROM {$CFG->prefix}question
208 WHERE id IN ($prevlist)");
215 * Re-paginates the quiz layout
217 * @return string The new layout string
218 * @param string $layout The string representing the quiz layout.
219 * @param integer $perpage The number of questions per page
220 * @param boolean $shuffle Should the questions be reordered randomly?
222 function quiz_repaginate($layout, $perpage, $shuffle=false) {
223 $layout = str_replace(',0', '', $layout); // remove existing page breaks
224 $questions = explode(',', $layout);
226 srand((float)microtime() * 1000000); // for php < 4.2
231 foreach ($questions as $question) {
232 if ($perpage and $i > $perpage) {
236 $layout .= $question.',';
243 * Print navigation panel for quiz attempt and review pages
245 * @param integer $page The number of the current page (counting from 0).
246 * @param integer $pages The total number of pages.
248 function quiz_print_navigation_panel($page, $pages) {
250 echo '<div class="pagingbar">';
251 echo '<span class="title">' . get_string('page') . ':</span>';
253 // Print previous link
254 $strprev = get_string('previous');
255 echo '<a href="javascript:navigate(' . ($page - 1) . ');" title="'
256 . $strprev . '">(' . $strprev . ')</a>';
258 for ($i = 0; $i < $pages; $i++
) {
260 echo '<span class="thispage">'.($i+
1).'</span>';
262 echo '<a href="javascript:navigate(' . ($i) . ');">'.($i+
1).'</a>';
266 if ($page < $pages - 1) {
268 $strnext = get_string('next');
269 echo '<a href="javascript:navigate(' . ($page +
1) . ');" title="'
270 . $strnext . '">(' . $strnext . ')</a>';
275 /// Functions to do with quiz grades //////////////////////////////////////////
278 * Creates an array of maximum grades for a quiz
280 * The grades are extracted from the quiz_question_instances table.
281 * @return array Array of grades indexed by question id
282 * These are the maximum possible grades that
283 * students can achieve for each of the questions
284 * @param integer $quiz The quiz object
286 function quiz_get_all_question_grades($quiz) {
289 $questionlist = quiz_questions_in_quiz($quiz->questions
);
290 if (empty($questionlist)) {
294 $instances = get_records_sql("SELECT question,grade,id
295 FROM {$CFG->prefix}quiz_question_instances
296 WHERE quiz = '$quiz->id'" .
297 (is_null($questionlist) ?
'' :
298 "AND question IN ($questionlist)"));
300 $list = explode(",", $questionlist);
303 foreach ($list as $qid) {
304 if (isset($instances[$qid])) {
305 $grades[$qid] = $instances[$qid]->grade
;
314 * Get the best current grade for a particular user in a quiz.
316 * @param object $quiz the quiz object.
317 * @param integer $userid the id of the user.
318 * @return float the user's current grade for this quiz.
320 function quiz_get_best_grade($quiz, $userid) {
321 $grade = get_field('quiz_grades', 'grade', 'quiz', $quiz->id
, 'userid', $userid);
323 // Need to detect errors/no result, without catching 0 scores.
324 if (is_numeric($grade)) {
325 return round($grade,$quiz->decimalpoints
);
332 * Convert the raw grade stored in $attempt into a grade out of the maximum
333 * grade for this quiz.
335 * @param float $rawgrade the unadjusted grade, fof example $attempt->sumgrades
336 * @param object $quiz the quiz object. Only the fields grade, sumgrades and decimalpoints are used.
337 * @return float the rescaled grade.
339 function quiz_rescale_grade($rawgrade, $quiz) {
340 if ($quiz->sumgrades
) {
341 return round($rawgrade*$quiz->grade
/$quiz->sumgrades
, $quiz->decimalpoints
);
348 * Get the feedback text that should be show to a student who
349 * got this grade on this quiz.
351 * @param float $grade a grade on this quiz.
352 * @param integer $quizid the id of the quiz object.
353 * @return string the comment that corresponds to this grade (empty string if there is not one.
355 function quiz_feedback_for_grade($grade, $quizid) {
356 $feedback = get_field_select('quiz_feedback', 'feedbacktext',
357 "quizid = $quizid AND mingrade <= $grade AND $grade < maxgrade");
359 if (empty($feedback)) {
367 * @param integer $quizid the id of the quiz object.
368 * @return boolean Whether this quiz has any non-blank feedback text.
370 function quiz_has_feedback($quizid) {
371 static $cache = array();
372 if (!array_key_exists($quizid, $cache)) {
373 $cache[$quizid] = record_exists_select('quiz_feedback',
374 "quizid = $quizid AND feedbacktext <> ''");
376 return $cache[$quizid];
380 * The quiz grade is the score that student's results are marked out of. When it
381 * changes, the corresponding data in quiz_grades and quiz_feedback needs to be
384 * @param float $newgrade the new maximum grade for the quiz.
385 * @param object $quiz the quiz we are updating. Passed by reference so its grade field can be updated too.
386 * @return boolean indicating success or failure.
388 function quiz_set_grade($newgrade, &$quiz) {
389 // This is potentially expensive, so only do it if necessary.
390 if (abs($quiz->grade
- $newgrade) < 1e-7) {
395 // Use a transaction, so that on those databases that support it, this is safer.
398 // Update the quiz table.
399 $success = set_field('quiz', 'grade', $newgrade, 'id', $quiz->instance
);
401 // Rescaling the other data is only possible if the old grade was non-zero.
402 if ($quiz->grade
> 1e-7) {
405 $factor = $newgrade/$quiz->grade
;
406 $quiz->grade
= $newgrade;
408 // Update the quiz_grades table.
409 $timemodified = time();
410 $success = $success && execute_sql("
411 UPDATE {$CFG->prefix}quiz_grades
412 SET grade = $factor * grade, timemodified = $timemodified
413 WHERE quiz = $quiz->id
416 // Update the quiz_grades table.
417 $success = $success && execute_sql("
418 UPDATE {$CFG->prefix}quiz_feedback
419 SET mingrade = $factor * mingrade, maxgrade = $factor * maxgrade
420 WHERE quizid = $quiz->id
424 // update grade item and send all grades to gradebook
425 quiz_grade_item_update($quiz);
426 quiz_update_grades($quiz);
437 * Save the overall grade for a user at a quiz in the quiz_grades table
439 * @param object $quiz The quiz for which the best grade is to be calculated and then saved.
440 * @param integer $userid The userid to calculate the grade for. Defaults to the current user.
441 * @return boolean Indicates success or failure.
443 function quiz_save_best_grade($quiz, $userid = null) {
446 if (empty($userid)) {
450 // Get all the attempts made by the user
451 if (!$attempts = quiz_get_user_attempts($quiz->id
, $userid)) {
452 notify('Could not find any user attempts');
456 // Calculate the best grade
457 $bestgrade = quiz_calculate_best_grade($quiz, $attempts);
458 $bestgrade = quiz_rescale_grade($bestgrade, $quiz);
460 // Save the best grade in the database
461 if ($grade = get_record('quiz_grades', 'quiz', $quiz->id
, 'userid', $userid)) {
462 $grade->grade
= $bestgrade;
463 $grade->timemodified
= time();
464 if (!update_record('quiz_grades', $grade)) {
465 notify('Could not update best grade');
469 $grade->quiz
= $quiz->id
;
470 $grade->userid
= $userid;
471 $grade->grade
= $bestgrade;
472 $grade->timemodified
= time();
473 if (!insert_record('quiz_grades', $grade)) {
474 notify('Could not insert new best grade');
479 quiz_update_grades($quiz, $userid);
484 * Calculate the overall grade for a quiz given a number of attempts by a particular user.
486 * @return float The overall grade
487 * @param object $quiz The quiz for which the best grade is to be calculated
488 * @param array $attempts An array of all the attempts of the user at the quiz
490 function quiz_calculate_best_grade($quiz, $attempts) {
492 switch ($quiz->grademethod
) {
494 case QUIZ_ATTEMPTFIRST
:
495 foreach ($attempts as $attempt) {
496 return $attempt->sumgrades
;
500 case QUIZ_ATTEMPTLAST
:
501 foreach ($attempts as $attempt) {
502 $final = $attempt->sumgrades
;
506 case QUIZ_GRADEAVERAGE
:
509 foreach ($attempts as $attempt) {
510 $sum +
= $attempt->sumgrades
;
513 return (float)$sum/$count;
516 case QUIZ_GRADEHIGHEST
:
518 foreach ($attempts as $attempt) {
519 if ($attempt->sumgrades
> $max) {
520 $max = $attempt->sumgrades
;
528 * Return the attempt with the best grade for a quiz
530 * Which attempt is the best depends on $quiz->grademethod. If the grade
531 * method is GRADEAVERAGE then this function simply returns the last attempt.
532 * @return object The attempt with the best grade
533 * @param object $quiz The quiz for which the best grade is to be calculated
534 * @param array $attempts An array of all the attempts of the user at the quiz
536 function quiz_calculate_best_attempt($quiz, $attempts) {
538 switch ($quiz->grademethod
) {
540 case QUIZ_ATTEMPTFIRST
:
541 foreach ($attempts as $attempt) {
546 case QUIZ_GRADEAVERAGE
: // need to do something with it :-)
547 case QUIZ_ATTEMPTLAST
:
548 foreach ($attempts as $attempt) {
554 case QUIZ_GRADEHIGHEST
:
556 foreach ($attempts as $attempt) {
557 if ($attempt->sumgrades
> $max) {
558 $max = $attempt->sumgrades
;
559 $maxattempt = $attempt;
566 /// Other quiz functions ////////////////////////////////////////////////////
569 * Print a box with quiz start and due dates
571 * @param object $quiz
573 function quiz_view_dates($quiz) {
574 if (!$quiz->timeopen
&& !$quiz->timeclose
) {
578 print_simple_box_start('center', '', '', '', 'generalbox', 'dates');
580 if ($quiz->timeopen
) {
581 echo '<tr><td class="c0">'.get_string("quizopen", "quiz").':</td>';
582 echo ' <td class="c1">'.userdate($quiz->timeopen
).'</td></tr>';
584 if ($quiz->timeclose
) {
585 echo '<tr><td class="c0">'.get_string("quizclose", "quiz").':</td>';
586 echo ' <td class="c1">'.userdate($quiz->timeclose
).'</td></tr>';
589 print_simple_box_end();
593 * Parse field names used for the replace options on question edit forms
595 function quiz_parse_fieldname($name, $nameprefix='question') {
597 if (preg_match("/$nameprefix(\\d+)(\w+)/", $name, $reg)) {
598 return array('mode' => $reg[2], 'id' => (int)$reg[1]);
605 * Upgrade states for an attempt to Moodle 1.5 model
607 * Any state that does not yet have its timestamp set to nonzero has not yet been upgraded from Moodle 1.4
608 * The reason these are still around is that for large sites it would have taken too long to
609 * upgrade all states at once. This function sets the timestamp field and creates an entry in the
610 * question_sessions table.
611 * @param object $attempt The attempt whose states need upgrading
613 function quiz_upgrade_states($attempt) {
615 // The old quiz model only allowed a single response per quiz attempt so that there will be
616 // only one state record per question for this attempt.
618 // We set the timestamp of all states to the timemodified field of the attempt.
619 execute_sql("UPDATE {$CFG->prefix}question_states SET timestamp = '$attempt->timemodified' WHERE attempt = '$attempt->uniqueid'", false);
621 // For each state we create an entry in the question_sessions table, with both newest and
622 // newgraded pointing to this state.
623 // Actually we only do this for states whose question is actually listed in $attempt->layout.
624 // We do not do it for states associated to wrapped questions like for example the questions
625 // used by a RANDOM question
626 $session = new stdClass
;
627 $session->attemptid
= $attempt->uniqueid
;
628 $questionlist = quiz_questions_in_quiz($attempt->layout
);
629 if ($questionlist and $states = get_records_select('question_states', "attempt = '$attempt->uniqueid' AND question IN ($questionlist)")) {
630 foreach ($states as $state) {
631 $session->newgraded
= $state->id
;
632 $session->newest
= $state->id
;
633 $session->questionid
= $state->question
;
634 insert_record('question_sessions', $session, false);
640 * @param object $quiz the quiz
641 * @param object $question the question
642 * @return the HTML for a preview question icon.
644 function quiz_question_preview_button($quiz, $question) {
646 $strpreview = get_string('previewquestion', 'quiz');
647 return link_to_popup_window('/question/preview.php?id=' . $question->id
. '&quizid=' . $quiz->id
, 'questionpreview',
648 "<img src=\"$CFG->pixpath/t/preview.gif\" class=\"iconsmall\" alt=\"$strpreview\" />",
649 0, 0, $strpreview, QUESTION_PREVIEW_POPUP_OPTIONS
, true);
653 * Determine render options
655 * @param int $reviewoptions
656 * @param object $state
658 function quiz_get_renderoptions($reviewoptions, $state) {
659 $options = new stdClass
;
661 // Show the question in readonly (review) mode if the question is in
663 $options->readonly
= question_state_is_closed($state);
665 // Show feedback once the question has been graded (if allowed by the quiz)
666 $options->feedback
= question_state_is_graded($state) && ($reviewoptions & QUIZ_REVIEW_FEEDBACK
& QUIZ_REVIEW_IMMEDIATELY
);
668 // Show validation only after a validation event
669 $options->validation
= QUESTION_EVENTVALIDATE
=== $state->event
;
671 // Show correct responses in readonly mode if the quiz allows it
672 $options->correct_responses
= $options->readonly
&& ($reviewoptions & QUIZ_REVIEW_ANSWERS
& QUIZ_REVIEW_IMMEDIATELY
);
674 // Show general feedback if the question has been graded and the quiz allows it.
675 $options->generalfeedback
= question_state_is_graded($state) && ($reviewoptions & QUIZ_REVIEW_GENERALFEEDBACK
& QUIZ_REVIEW_IMMEDIATELY
);
677 // Show overallfeedback once the attempt is over.
678 $options->overallfeedback
= false;
680 // Always show responses and scores
681 $options->responses
= true;
682 $options->scores
= true;
688 * Determine review options
690 * @param object $quiz the quiz instance.
691 * @param object $attempt the attempt in question.
692 * @param $context the roles and permissions context,
693 * normally the context for the quiz module instance.
695 * @return object an object with boolean fields responses, scores, feedback,
696 * correct_responses, solutions and general feedback
698 function quiz_get_reviewoptions($quiz, $attempt, $context=null) {
700 $options = new stdClass
;
701 $options->readonly
= true;
702 // Provide the links to the question review and comment script
703 $options->questionreviewlink
= '/mod/quiz/reviewquestion.php';
705 if ($context && has_capability('mod/quiz:viewreports', $context) and !$attempt->preview
) {
706 // The teacher should be shown everything except during preview when the teachers
707 // wants to see just what the students see
708 $options->responses
= true;
709 $options->scores
= true;
710 $options->feedback
= true;
711 $options->correct_responses
= true;
712 $options->solutions
= false;
713 $options->generalfeedback
= true;
714 $options->overallfeedback
= true;
716 // Show a link to the comment box only for closed attempts
717 if ($attempt->timefinish
) {
718 $options->questioncommentlink
= '/mod/quiz/comment.php';
721 if (((time() - $attempt->timefinish
) < 120) ||
$attempt->timefinish
==0) {
722 $quiz_state_mask = QUIZ_REVIEW_IMMEDIATELY
;
723 } else if (!$quiz->timeclose
or time() < $quiz->timeclose
) {
724 $quiz_state_mask = QUIZ_REVIEW_OPEN
;
726 $quiz_state_mask = QUIZ_REVIEW_CLOSED
;
728 $options->responses
= ($quiz->review
& $quiz_state_mask & QUIZ_REVIEW_RESPONSES
) ?
1 : 0;
729 $options->scores
= ($quiz->review
& $quiz_state_mask & QUIZ_REVIEW_SCORES
) ?
1 : 0;
730 $options->feedback
= ($quiz->review
& $quiz_state_mask & QUIZ_REVIEW_FEEDBACK
) ?
1 : 0;
731 $options->correct_responses
= ($quiz->review
& $quiz_state_mask & QUIZ_REVIEW_ANSWERS
) ?
1 : 0;
732 $options->solutions
= ($quiz->review
& $quiz_state_mask & QUIZ_REVIEW_SOLUTIONS
) ?
1 : 0;
733 $options->generalfeedback
= ($quiz->review
& $quiz_state_mask & QUIZ_REVIEW_GENERALFEEDBACK
) ?
1 : 0;
734 $options->overallfeedback
= $attempt->timefinish
&& ($quiz->review
& $quiz_state_mask & QUIZ_REVIEW_FEEDBACK
);
741 * Combines the review options from a number of different quiz attempts.
742 * Returns an array of two ojects, so he suggested way of calling this
744 * list($someoptions, $alloptions) = quiz_get_combined_reviewoptions(...)
746 * @param object $quiz the quiz instance.
747 * @param array $attempts an array of attempt objects.
748 * @param $context the roles and permissions context,
749 * normally the context for the quiz module instance.
751 * @return array of two options objects, one showing which options are true for
752 * at least one of the attempts, the other showing which options are true
755 function quiz_get_combined_reviewoptions($quiz, $attempts, $context=null) {
756 $fields = array('readonly', 'scores', 'feedback', 'correct_responses', 'solutions', 'generalfeedback', 'overallfeedback');
757 $someoptions = new stdClass
;
758 $alloptions = new stdClass
;
759 foreach ($fields as $field) {
760 $someoptions->$field = false;
761 $alloptions->$field = true;
763 foreach ($attempts as $attempt) {
764 $attemptoptions = quiz_get_reviewoptions($quiz, $attempt, $context);
765 foreach ($fields as $field) {
766 $someoptions->$field = $someoptions->$field ||
$attemptoptions->$field;
767 $alloptions->$field = $alloptions->$field && $attemptoptions->$field;
770 return array($someoptions, $alloptions);
773 /// FUNCTIONS FOR SENDING NOTIFICATION EMAILS ///////////////////////////////
776 * Sends confirmation email to the student taking the course
778 * @param stdClass $a associative array of replaceable fields for the templates
780 * @return bool|string result of email_to_user()
782 function quiz_send_confirmation($a) {
787 $a->useridnumber
= $USER->idnumber
;
788 $a->username
= fullname($USER);
789 $a->userusername
= $USER->username
;
791 // fetch the subject and body from strings
792 $subject = get_string('emailconfirmsubject', 'quiz', $a);
793 $body = get_string('emailconfirmbody', 'quiz', $a);
795 // send email and analyse result
796 return email_to_user($USER, get_admin(), $subject, $body);
800 * Sends notification email to the interested parties that assign the role capability
802 * @param object $recipient user object of the intended recipient
803 * @param stdClass $a associative array of replaceable fields for the templates
805 * @return bool|string result of email_to_user()
807 function quiz_send_notification($recipient, $a) {
811 // recipient info for template
812 $a->username
= fullname($recipient);
813 $a->userusername
= $recipient->username
;
814 $a->userusername
= $recipient->username
;
816 // fetch the subject and body from strings
817 $subject = get_string('emailnotifysubject', 'quiz', $a);
818 $body = get_string('emailnotifybody', 'quiz', $a);
820 // send email and analyse result
821 return email_to_user($recipient, $USER, $subject, $body);
825 * Takes a bunch of information to format into an email and send
826 * to the specified recipient.
828 * @param object $course the course
829 * @param object $quiz the quiz
830 * @param object $attempt this attempt just finished
831 * @param object $context the quiz context
832 * @param object $cm the coursemodule for this quiz
834 * @return int number of emails sent
836 function quiz_send_notification_emails($course, $quiz, $attempt, $context, $cm) {
838 // we will count goods and bads for error logging
839 $emailresult = array('good' => 0, 'block' => 0, 'fail' => 0);
841 // do nothing if required objects not present
842 if (empty($course) or empty($quiz) or empty($attempt) or empty($context)) {
843 debugging('quiz_send_notification_emails: Email(s) not sent due to program error.',
845 return $emailresult['fail'];
848 // check for confirmation required
849 $sendconfirm = false;
850 $notifyexcludeusers = '';
851 if (has_capability('mod/quiz:emailconfirmsubmission', $context, NULL, false)) {
852 // exclude from notify emails later
853 $notifyexcludeusers = $USER->id
;
858 // check for notifications required
859 $notifyfields = 'u.id, u.username, u.firstname, u.lastname, u.email, u.emailstop, u.lang, u.timezone, u.mailformat, u.maildisplay';
860 $userstonotify = get_users_by_capability($context, 'mod/quiz:emailnotifysubmission',
861 $notifyfields, '', '', '', groups_m_get_groups_for_user($cm, $USER->id
),
862 $notifyexcludeusers, false, false, true);
864 // if something to send, then build $a
865 if (! empty($userstonotify) or $sendconfirm) {
868 $a->coursename
= $course->fullname
;
869 $a->courseshortname
= $course->shortname
;
871 $a->quizname
= $quiz->name
;
872 $a->quizreportlink
= '<a href="report.php?q=' . $quiz->id
. '">' . format_string($quiz->name
) . ' report</a>';
873 $a->quizreporturl
= $CFG->wwwroot
. '/mod/quiz/report.php?q=' . $quiz->id
;
874 $a->quizreviewlink
= '<a href="review.php?attempt=' . $attempt->id
. '">' . format_string($quiz->name
) . ' review</a>';
875 $a->quizreviewurl
= $CFG->wwwroot
. '/mod/quiz/review.php?attempt=' . $attempt->id
;
876 $a->quizlink
= '<a href="view.php?q=' . $quiz->id
. '">' . format_string($quiz->name
) . '</a>';
877 $a->quizurl
= $CFG->wwwroot
. '/mod/quiz/view.php?q=' . $quiz->id
;
879 $a->submissiontime
= userdate($attempt->timefinish
);
880 $a->timetaken
= format_time($attempt->timefinish
- $attempt->timestart
);
881 // student who sat the quiz info
882 $a->studentidnumber
= $USER->idnumber
;
883 $a->studentname
= fullname($USER);
884 $a->studentusername
= $USER->username
;
887 // send confirmation if required
889 // send the email and update stats
890 switch (quiz_send_confirmation($a)) {
892 $emailresult['good']++
;
895 $emailresult['fail']++
;
898 $emailresult['block']++
;
903 // send notifications if required
904 if (!empty($userstonotify)) {
905 // loop through recipients and send an email to each and update stats
906 foreach ($userstonotify as $recipient) {
907 switch (quiz_send_notification($recipient, $a)) {
909 $emailresult['good']++
;
912 $emailresult['fail']++
;
915 $emailresult['block']++
;
921 // log errors sending emails if any
922 if (! empty($emailresult['fail'])) {
923 debugging('quiz_send_notification_emails:: '.$emailresult['fail'].' email(s) failed to be sent.', DEBUG_DEVELOPER
);
925 if (! empty($emailresult['block'])) {
926 debugging('quiz_send_notification_emails:: '.$emailresult['block'].' email(s) were blocked by the user.', DEBUG_DEVELOPER
);
929 // return the number of successfully sent emails
930 return $emailresult['good'];