MDL-10383 - groups/groupings refactoring nearly finished ;-)
[moodle-pu.git] / mod / quiz / locallib.php
blob69eabbae7747f2331fd3ca89685ee8e5d3894998
1 <?php // $Id$
2 /**
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.
10 * @version $Id$
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
16 * @package quiz
19 /**
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 ///////////////////////////////////////////////////////////////////
27 /**#@+
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 /**#@-*/
37 /// Functions related to attempts /////////////////////////////////////////
39 /**
40 * Creates an object to represent a new attempt at a quiz
42 * Creates an attempt object to represent an attempt at the quiz by the current
43 * user starting at the current time. The ->id field is not set. The object is
44 * NOT written to the database.
45 * @return object The newly created attempt object.
46 * @param object $quiz The quiz to create an attempt for.
47 * @param integer $attemptnumber The sequence number for the attempt.
49 function quiz_create_attempt($quiz, $attemptnumber) {
50 global $USER, $CFG;
52 if (!$attemptnumber > 1 or !$quiz->attemptonlast or !$attempt = get_record('quiz_attempts', 'quiz', $quiz->id, 'userid', $USER->id, 'attempt', $attemptnumber-1)) {
53 // we are not building on last attempt so create a new attempt
54 $attempt->quiz = $quiz->id;
55 $attempt->userid = $USER->id;
56 $attempt->preview = 0;
57 if ($quiz->shufflequestions) {
58 $attempt->layout = quiz_repaginate($quiz->questions, $quiz->questionsperpage, true);
59 } else {
60 $attempt->layout = $quiz->questions;
64 $timenow = time();
65 $attempt->attempt = $attemptnumber;
66 $attempt->sumgrades = 0.0;
67 $attempt->timestart = $timenow;
68 $attempt->timefinish = 0;
69 $attempt->timemodified = $timenow;
70 $attempt->uniqueid = question_new_attempt_uniqueid();
72 return $attempt;
75 /**
76 * Returns an unfinished attempt (if there is one) for the given
77 * user on the given quiz. This function does not return preview attempts.
79 * @param integer $quizid the id of the quiz.
80 * @param integer $userid the id of the user.
82 * @return mixed the unfinished attempt if there is one, false if not.
84 function quiz_get_user_attempt_unfinished($quizid, $userid) {
85 $attempts = quiz_get_user_attempts($quizid, $userid, 'unfinished', true);
86 if ($attempts) {
87 return array_shift($attempts);
88 } else {
89 return false;
93 /**
94 * @param integer $quizid the quiz id.
95 * @param integer $userid the userid.
96 * @param string $status 'all', 'finished' or 'unfinished' to control
97 * @return an array of all the user's attempts at this quiz. Returns an empty array if there are none.
99 function quiz_get_user_attempts($quizid, $userid, $status = 'finished', $includepreviews = false) {
100 $status_condition = array(
101 'all' => '',
102 'finished' => ' AND timefinish > 0',
103 'unfinished' => ' AND timefinish = 0'
105 $previewclause = '';
106 if (!$includepreviews) {
107 $previewclause = ' AND preview = 0';
109 if ($attempts = get_records_select('quiz_attempts',
110 "quiz = '$quizid' AND userid = '$userid'" . $previewclause . $status_condition[$status],
111 'attempt ASC')) {
112 return $attempts;
113 } else {
114 return array();
119 * Delete a quiz attempt.
121 function quiz_delete_attempt($attempt, $quiz) {
122 if (is_numeric($attempt)) {
123 if (!$attempt = get_record('quiz_attempts', 'id', $attempt)) {
124 return;
128 if ($attempt->quiz != $quiz->id) {
129 debugging("Trying to delete attempt $attempt->id which belongs to quiz $attempt->quiz " .
130 "but was passed quiz $quiz->id.");
131 return;
134 delete_records('quiz_attempts', 'id', $attempt->id);
135 delete_attempt($attempt->uniqueid);
137 // Search quiz_attempts for other instances by this user.
138 // If none, then delete record for this quiz, this user from quiz_grades
139 // else recalculate best grade
141 $userid = $attempt->userid;
142 if (!record_exists('quiz_attempts', 'userid', $userid, 'quiz', $quiz->id)) {
143 delete_records('quiz_grades', 'userid', $userid,'quiz', $quiz->id);
144 } else {
145 quiz_save_best_grade($quiz, $userid);
149 /// Functions to do with quiz layout and pages ////////////////////////////////
152 * Returns a comma separated list of question ids for the current page
154 * @return string Comma separated list of question ids
155 * @param string $layout The string representing the quiz layout. Each page is represented as a
156 * comma separated list of question ids and 0 indicating page breaks.
157 * So 5,2,0,3,0 means questions 5 and 2 on page 1 and question 3 on page 2
158 * @param integer $page The number of the current page.
160 function quiz_questions_on_page($layout, $page) {
161 $pages = explode(',0', $layout);
162 return trim($pages[$page], ',');
166 * Returns a comma separated list of question ids for the quiz
168 * @return string Comma separated list of question ids
169 * @param string $layout The string representing the quiz layout. Each page is represented as a
170 * comma separated list of question ids and 0 indicating page breaks.
171 * So 5,2,0,3,0 means questions 5 and 2 on page 1 and question 3 on page 2
173 function quiz_questions_in_quiz($layout) {
174 return str_replace(',0', '', $layout);
178 * Returns the number of pages in the quiz layout
180 * @return integer Comma separated list of question ids
181 * @param string $layout The string representing the quiz layout.
183 function quiz_number_of_pages($layout) {
184 return substr_count($layout, ',0');
188 * Returns the first question number for the current quiz page
190 * @return integer The number of the first question
191 * @param string $quizlayout The string representing the layout for the whole quiz
192 * @param string $pagelayout The string representing the layout for the current page
194 function quiz_first_questionnumber($quizlayout, $pagelayout) {
195 // this works by finding all the questions from the quizlayout that
196 // come before the current page and then adding up their lengths.
197 global $CFG;
198 $start = strpos($quizlayout, ','.$pagelayout.',')-2;
199 if ($start > 0) {
200 $prevlist = substr($quizlayout, 0, $start);
201 return get_field_sql("SELECT sum(length)+1 FROM {$CFG->prefix}question
202 WHERE id IN ($prevlist)");
203 } else {
204 return 1;
209 * Re-paginates the quiz layout
211 * @return string The new layout string
212 * @param string $layout The string representing the quiz layout.
213 * @param integer $perpage The number of questions per page
214 * @param boolean $shuffle Should the questions be reordered randomly?
216 function quiz_repaginate($layout, $perpage, $shuffle=false) {
217 $layout = str_replace(',0', '', $layout); // remove existing page breaks
218 $questions = explode(',', $layout);
219 if ($shuffle) {
220 srand((float)microtime() * 1000000); // for php < 4.2
221 shuffle($questions);
223 $i = 1;
224 $layout = '';
225 foreach ($questions as $question) {
226 if ($perpage and $i > $perpage) {
227 $layout .= '0,';
228 $i = 1;
230 $layout .= $question.',';
231 $i++;
233 return $layout.'0';
237 * Print navigation panel for quiz attempt and review pages
239 * @param integer $page The number of the current page (counting from 0).
240 * @param integer $pages The total number of pages.
242 function quiz_print_navigation_panel($page, $pages) {
243 //$page++;
244 echo '<div class="pagingbar">';
245 echo '<span class="title">' . get_string('page') . ':</span>';
246 if ($page > 0) {
247 // Print previous link
248 $strprev = get_string('previous');
249 echo '<a href="javascript:navigate(' . ($page - 1) . ');" title="'
250 . $strprev . '">(' . $strprev . ')</a>';
252 for ($i = 0; $i < $pages; $i++) {
253 if ($i == $page) {
254 echo '<span class="thispage">'.($i+1).'</span>';
255 } else {
256 echo '<a href="javascript:navigate(' . ($i) . ');">'.($i+1).'</a>';
260 if ($page < $pages - 1) {
261 // Print next link
262 $strnext = get_string('next');
263 echo '<a href="javascript:navigate(' . ($page + 1) . ');" title="'
264 . $strnext . '">(' . $strnext . ')</a>';
266 echo '</div>';
269 /// Functions to do with quiz grades //////////////////////////////////////////
272 * Creates an array of maximum grades for a quiz
274 * The grades are extracted from the quiz_question_instances table.
275 * @return array Array of grades indexed by question id
276 * These are the maximum possible grades that
277 * students can achieve for each of the questions
278 * @param integer $quiz The quiz object
280 function quiz_get_all_question_grades($quiz) {
281 global $CFG;
283 $questionlist = quiz_questions_in_quiz($quiz->questions);
284 if (empty($questionlist)) {
285 return array();
288 $instances = get_records_sql("SELECT question,grade,id
289 FROM {$CFG->prefix}quiz_question_instances
290 WHERE quiz = '$quiz->id'" .
291 (is_null($questionlist) ? '' :
292 "AND question IN ($questionlist)"));
294 $list = explode(",", $questionlist);
295 $grades = array();
297 foreach ($list as $qid) {
298 if (isset($instances[$qid])) {
299 $grades[$qid] = $instances[$qid]->grade;
300 } else {
301 $grades[$qid] = 1;
304 return $grades;
308 * Get the best current grade for a particular user in a quiz.
310 * @param object $quiz the quiz object.
311 * @param integer $userid the id of the user.
312 * @return float the user's current grade for this quiz.
314 function quiz_get_best_grade($quiz, $userid) {
315 $grade = get_field('quiz_grades', 'grade', 'quiz', $quiz->id, 'userid', $userid);
317 // Need to detect errors/no result, without catching 0 scores.
318 if (is_numeric($grade)) {
319 return round($grade,$quiz->decimalpoints);
320 } else {
321 return NULL;
326 * Convert the raw grade stored in $attempt into a grade out of the maximum
327 * grade for this quiz.
329 * @param float $rawgrade the unadjusted grade, fof example $attempt->sumgrades
330 * @param object $quiz the quiz object. Only the fields grade, sumgrades and decimalpoints are used.
331 * @return float the rescaled grade.
333 function quiz_rescale_grade($rawgrade, $quiz) {
334 if ($quiz->sumgrades) {
335 return round($rawgrade*$quiz->grade/$quiz->sumgrades, $quiz->decimalpoints);
336 } else {
337 return 0;
342 * Get the feedback text that should be show to a student who
343 * got this grade on this quiz. The feedback is processed ready for diplay.
345 * @param float $grade a grade on this quiz.
346 * @param integer $quizid the id of the quiz object.
347 * @return string the comment that corresponds to this grade (empty string if there is not one.
349 function quiz_feedback_for_grade($grade, $quizid) {
350 $feedback = get_field_select('quiz_feedback', 'feedbacktext',
351 "quizid = $quizid AND mingrade <= $grade AND $grade < maxgrade");
353 if (empty($feedback)) {
354 $feedback = '';
357 // Clean the text, ready for display.
358 $formatoptions = new stdClass;
359 $formatoptions->noclean = true;
360 $feedback = format_text($feedback, FORMAT_MOODLE, $formatoptions);
362 return $feedback;
366 * @param integer $quizid the id of the quiz object.
367 * @return boolean Whether this quiz has any non-blank feedback text.
369 function quiz_has_feedback($quizid) {
370 static $cache = array();
371 if (!array_key_exists($quizid, $cache)) {
372 $cache[$quizid] = record_exists_select('quiz_feedback',
373 "quizid = $quizid AND feedbacktext <> ''");
375 return $cache[$quizid];
379 * The quiz grade is the score that student's results are marked out of. When it
380 * changes, the corresponding data in quiz_grades and quiz_feedback needs to be
381 * rescaled.
383 * @param float $newgrade the new maximum grade for the quiz.
384 * @param object $quiz the quiz we are updating. Passed by reference so its grade field can be updated too.
385 * @return boolean indicating success or failure.
387 function quiz_set_grade($newgrade, &$quiz) {
388 // This is potentially expensive, so only do it if necessary.
389 if (abs($quiz->grade - $newgrade) < 1e-7) {
390 // Nothing to do.
391 return true;
394 // Use a transaction, so that on those databases that support it, this is safer.
395 begin_sql();
397 // Update the quiz table.
398 $success = set_field('quiz', 'grade', $newgrade, 'id', $quiz->instance);
400 // Rescaling the other data is only possible if the old grade was non-zero.
401 if ($quiz->grade > 1e-7) {
402 global $CFG;
404 $factor = $newgrade/$quiz->grade;
405 $quiz->grade = $newgrade;
407 // Update the quiz_grades table.
408 $timemodified = time();
409 $success = $success && execute_sql("
410 UPDATE {$CFG->prefix}quiz_grades
411 SET grade = $factor * grade, timemodified = $timemodified
412 WHERE quiz = $quiz->id
413 ", false);
415 // Update the quiz_grades table.
416 $success = $success && execute_sql("
417 UPDATE {$CFG->prefix}quiz_feedback
418 SET mingrade = $factor * mingrade, maxgrade = $factor * maxgrade
419 WHERE quizid = $quiz->id
420 ", false);
423 // update grade item and send all grades to gradebook
424 quiz_grade_item_update($quiz);
425 quiz_update_grades($quiz);
427 if ($success) {
428 return commit_sql();
429 } else {
430 rollback_sql();
431 return false;
436 * Save the overall grade for a user at a quiz in the quiz_grades table
438 * @param object $quiz The quiz for which the best grade is to be calculated and then saved.
439 * @param integer $userid The userid to calculate the grade for. Defaults to the current user.
440 * @return boolean Indicates success or failure.
442 function quiz_save_best_grade($quiz, $userid = null) {
443 global $USER;
445 if (empty($userid)) {
446 $userid = $USER->id;
449 // Get all the attempts made by the user
450 if (!$attempts = quiz_get_user_attempts($quiz->id, $userid)) {
451 notify('Could not find any user attempts');
452 return false;
455 // Calculate the best grade
456 $bestgrade = quiz_calculate_best_grade($quiz, $attempts);
457 $bestgrade = quiz_rescale_grade($bestgrade, $quiz);
459 // Save the best grade in the database
460 if ($grade = get_record('quiz_grades', 'quiz', $quiz->id, 'userid', $userid)) {
461 $grade->grade = $bestgrade;
462 $grade->timemodified = time();
463 if (!update_record('quiz_grades', $grade)) {
464 notify('Could not update best grade');
465 return false;
467 } else {
468 $grade->quiz = $quiz->id;
469 $grade->userid = $userid;
470 $grade->grade = $bestgrade;
471 $grade->timemodified = time();
472 if (!insert_record('quiz_grades', $grade)) {
473 notify('Could not insert new best grade');
474 return false;
478 quiz_update_grades($quiz, $userid);
479 return true;
483 * Calculate the overall grade for a quiz given a number of attempts by a particular user.
485 * @return float The overall grade
486 * @param object $quiz The quiz for which the best grade is to be calculated
487 * @param array $attempts An array of all the attempts of the user at the quiz
489 function quiz_calculate_best_grade($quiz, $attempts) {
491 switch ($quiz->grademethod) {
493 case QUIZ_ATTEMPTFIRST:
494 foreach ($attempts as $attempt) {
495 return $attempt->sumgrades;
497 break;
499 case QUIZ_ATTEMPTLAST:
500 foreach ($attempts as $attempt) {
501 $final = $attempt->sumgrades;
503 return $final;
505 case QUIZ_GRADEAVERAGE:
506 $sum = 0;
507 $count = 0;
508 foreach ($attempts as $attempt) {
509 $sum += $attempt->sumgrades;
510 $count++;
512 return (float)$sum/$count;
514 default:
515 case QUIZ_GRADEHIGHEST:
516 $max = 0;
517 foreach ($attempts as $attempt) {
518 if ($attempt->sumgrades > $max) {
519 $max = $attempt->sumgrades;
522 return $max;
527 * Return the attempt with the best grade for a quiz
529 * Which attempt is the best depends on $quiz->grademethod. If the grade
530 * method is GRADEAVERAGE then this function simply returns the last attempt.
531 * @return object The attempt with the best grade
532 * @param object $quiz The quiz for which the best grade is to be calculated
533 * @param array $attempts An array of all the attempts of the user at the quiz
535 function quiz_calculate_best_attempt($quiz, $attempts) {
537 switch ($quiz->grademethod) {
539 case QUIZ_ATTEMPTFIRST:
540 foreach ($attempts as $attempt) {
541 return $attempt;
543 break;
545 case QUIZ_GRADEAVERAGE: // need to do something with it :-)
546 case QUIZ_ATTEMPTLAST:
547 foreach ($attempts as $attempt) {
548 $final = $attempt;
550 return $final;
552 default:
553 case QUIZ_GRADEHIGHEST:
554 $max = -1;
555 foreach ($attempts as $attempt) {
556 if ($attempt->sumgrades > $max) {
557 $max = $attempt->sumgrades;
558 $maxattempt = $attempt;
561 return $maxattempt;
566 * @return the options for calculating the quiz grade from the individual attempt grades.
568 function quiz_get_grading_options() {
569 return array (
570 QUIZ_GRADEHIGHEST => get_string('gradehighest', 'quiz'),
571 QUIZ_GRADEAVERAGE => get_string('gradeaverage', 'quiz'),
572 QUIZ_ATTEMPTFIRST => get_string('attemptfirst', 'quiz'),
573 QUIZ_ATTEMPTLAST => get_string('attemptlast', 'quiz'));
577 * @param int $option one of the values QUIZ_GRADEHIGHEST, QUIZ_GRADEAVERAGE, QUIZ_ATTEMPTFIRST or QUIZ_ATTEMPTLAST.
578 * @return the lang string for that option.
580 function quiz_get_grading_option_name($option) {
581 $strings = quiz_get_grading_options();
582 return $strings[$option];
585 /// Other quiz functions ////////////////////////////////////////////////////
588 * Print a box with quiz start and due dates
590 * @param object $quiz
592 function quiz_view_dates($quiz) {
593 if (!$quiz->timeopen && !$quiz->timeclose) {
594 return;
597 print_simple_box_start('center', '', '', '', 'generalbox', 'dates');
598 echo '<table>';
599 if ($quiz->timeopen) {
600 echo '<tr><td class="c0">'.get_string("quizopen", "quiz").':</td>';
601 echo ' <td class="c1">'.userdate($quiz->timeopen).'</td></tr>';
603 if ($quiz->timeclose) {
604 echo '<tr><td class="c0">'.get_string("quizclose", "quiz").':</td>';
605 echo ' <td class="c1">'.userdate($quiz->timeclose).'</td></tr>';
607 echo '</table>';
608 print_simple_box_end();
612 * Parse field names used for the replace options on question edit forms
614 function quiz_parse_fieldname($name, $nameprefix='question') {
615 $reg = array();
616 if (preg_match("/$nameprefix(\\d+)(\w+)/", $name, $reg)) {
617 return array('mode' => $reg[2], 'id' => (int)$reg[1]);
618 } else {
619 return false;
624 * Upgrade states for an attempt to Moodle 1.5 model
626 * Any state that does not yet have its timestamp set to nonzero has not yet been upgraded from Moodle 1.4
627 * The reason these are still around is that for large sites it would have taken too long to
628 * upgrade all states at once. This function sets the timestamp field and creates an entry in the
629 * question_sessions table.
630 * @param object $attempt The attempt whose states need upgrading
632 function quiz_upgrade_states($attempt) {
633 global $CFG;
634 // The old quiz model only allowed a single response per quiz attempt so that there will be
635 // only one state record per question for this attempt.
637 // We set the timestamp of all states to the timemodified field of the attempt.
638 execute_sql("UPDATE {$CFG->prefix}question_states SET timestamp = '$attempt->timemodified' WHERE attempt = '$attempt->uniqueid'", false);
640 // For each state we create an entry in the question_sessions table, with both newest and
641 // newgraded pointing to this state.
642 // Actually we only do this for states whose question is actually listed in $attempt->layout.
643 // We do not do it for states associated to wrapped questions like for example the questions
644 // used by a RANDOM question
645 $session = new stdClass;
646 $session->attemptid = $attempt->uniqueid;
647 $questionlist = quiz_questions_in_quiz($attempt->layout);
648 if ($questionlist and $states = get_records_select('question_states', "attempt = '$attempt->uniqueid' AND question IN ($questionlist)")) {
649 foreach ($states as $state) {
650 $session->newgraded = $state->id;
651 $session->newest = $state->id;
652 $session->questionid = $state->question;
653 insert_record('question_sessions', $session, false);
659 * @param object $quiz the quiz
660 * @param object $question the question
661 * @return the HTML for a preview question icon.
663 function quiz_question_preview_button($quiz, $question) {
664 global $CFG, $COURSE;
665 if (!question_has_capability_on($question, 'use', $question->category)){
666 return '';
668 $strpreview = get_string('previewquestion', 'quiz');
669 $quizorcourseid = $quiz->id?('&amp;quizid=' . $quiz->id):('&amp;courseid=' .$COURSE->id);
670 return link_to_popup_window('/question/preview.php?id=' . $question->id . $quizorcourseid, 'questionpreview',
671 "<img src=\"$CFG->pixpath/t/preview.gif\" class=\"iconsmall\" alt=\"$strpreview\" />",
672 0, 0, $strpreview, QUESTION_PREVIEW_POPUP_OPTIONS, true);
676 * Determine render options
678 * @param int $reviewoptions
679 * @param object $state
681 function quiz_get_renderoptions($reviewoptions, $state) {
682 $options = new stdClass;
684 // Show the question in readonly (review) mode if the question is in
685 // the closed state
686 $options->readonly = question_state_is_closed($state);
688 // Show feedback once the question has been graded (if allowed by the quiz)
689 $options->feedback = question_state_is_graded($state) && ($reviewoptions & QUIZ_REVIEW_FEEDBACK & QUIZ_REVIEW_IMMEDIATELY);
691 // Show validation only after a validation event
692 $options->validation = QUESTION_EVENTVALIDATE === $state->event;
694 // Show correct responses in readonly mode if the quiz allows it
695 $options->correct_responses = $options->readonly && ($reviewoptions & QUIZ_REVIEW_ANSWERS & QUIZ_REVIEW_IMMEDIATELY);
697 // Show general feedback if the question has been graded and the quiz allows it.
698 $options->generalfeedback = question_state_is_graded($state) && ($reviewoptions & QUIZ_REVIEW_GENERALFEEDBACK & QUIZ_REVIEW_IMMEDIATELY);
700 // Show overallfeedback once the attempt is over.
701 $options->overallfeedback = false;
703 // Always show responses and scores
704 $options->responses = true;
705 $options->scores = true;
707 return $options;
711 * Determine review options
713 * @param object $quiz the quiz instance.
714 * @param object $attempt the attempt in question.
715 * @param $context the roles and permissions context,
716 * normally the context for the quiz module instance.
718 * @return object an object with boolean fields responses, scores, feedback,
719 * correct_responses, solutions and general feedback
721 function quiz_get_reviewoptions($quiz, $attempt, $context=null) {
723 $options = new stdClass;
724 $options->readonly = true;
725 // Provide the links to the question review and comment script
726 $options->questionreviewlink = '/mod/quiz/reviewquestion.php';
728 if ($context && has_capability('mod/quiz:viewreports', $context) and !$attempt->preview) {
729 // The teacher should be shown everything except during preview when the teachers
730 // wants to see just what the students see
731 $options->responses = true;
732 $options->scores = true;
733 $options->feedback = true;
734 $options->correct_responses = true;
735 $options->solutions = false;
736 $options->generalfeedback = true;
737 $options->overallfeedback = true;
739 // Show a link to the comment box only for closed attempts
740 if ($attempt->timefinish) {
741 $options->questioncommentlink = '/mod/quiz/comment.php';
743 } else {
744 if (((time() - $attempt->timefinish) < 120) || $attempt->timefinish==0) {
745 $quiz_state_mask = QUIZ_REVIEW_IMMEDIATELY;
746 } else if (!$quiz->timeclose or time() < $quiz->timeclose) {
747 $quiz_state_mask = QUIZ_REVIEW_OPEN;
748 } else {
749 $quiz_state_mask = QUIZ_REVIEW_CLOSED;
751 $options->responses = ($quiz->review & $quiz_state_mask & QUIZ_REVIEW_RESPONSES) ? 1 : 0;
752 $options->scores = ($quiz->review & $quiz_state_mask & QUIZ_REVIEW_SCORES) ? 1 : 0;
753 $options->feedback = ($quiz->review & $quiz_state_mask & QUIZ_REVIEW_FEEDBACK) ? 1 : 0;
754 $options->correct_responses = ($quiz->review & $quiz_state_mask & QUIZ_REVIEW_ANSWERS) ? 1 : 0;
755 $options->solutions = ($quiz->review & $quiz_state_mask & QUIZ_REVIEW_SOLUTIONS) ? 1 : 0;
756 $options->generalfeedback = ($quiz->review & $quiz_state_mask & QUIZ_REVIEW_GENERALFEEDBACK) ? 1 : 0;
757 $options->overallfeedback = $attempt->timefinish && ($quiz->review & $quiz_state_mask & QUIZ_REVIEW_OVERALLFEEDBACK);
760 return $options;
764 * Combines the review options from a number of different quiz attempts.
765 * Returns an array of two ojects, so he suggested way of calling this
766 * funciton is:
767 * list($someoptions, $alloptions) = quiz_get_combined_reviewoptions(...)
769 * @param object $quiz the quiz instance.
770 * @param array $attempts an array of attempt objects.
771 * @param $context the roles and permissions context,
772 * normally the context for the quiz module instance.
774 * @return array of two options objects, one showing which options are true for
775 * at least one of the attempts, the other showing which options are true
776 * for all attempts.
778 function quiz_get_combined_reviewoptions($quiz, $attempts, $context=null) {
779 $fields = array('readonly', 'scores', 'feedback', 'correct_responses', 'solutions', 'generalfeedback', 'overallfeedback');
780 $someoptions = new stdClass;
781 $alloptions = new stdClass;
782 foreach ($fields as $field) {
783 $someoptions->$field = false;
784 $alloptions->$field = true;
786 foreach ($attempts as $attempt) {
787 $attemptoptions = quiz_get_reviewoptions($quiz, $attempt, $context);
788 foreach ($fields as $field) {
789 $someoptions->$field = $someoptions->$field || $attemptoptions->$field;
790 $alloptions->$field = $alloptions->$field && $attemptoptions->$field;
793 return array($someoptions, $alloptions);
796 /// FUNCTIONS FOR SENDING NOTIFICATION EMAILS ///////////////////////////////
799 * Sends confirmation email to the student taking the course
801 * @param stdClass $a associative array of replaceable fields for the templates
803 * @return bool|string result of email_to_user()
805 function quiz_send_confirmation($a) {
807 global $USER;
809 // recipient is self
810 $a->useridnumber = $USER->idnumber;
811 $a->username = fullname($USER);
812 $a->userusername = $USER->username;
814 // fetch the subject and body from strings
815 $subject = get_string('emailconfirmsubject', 'quiz', $a);
816 $body = get_string('emailconfirmbody', 'quiz', $a);
818 // send email and analyse result
819 return email_to_user($USER, get_admin(), $subject, $body);
823 * Sends notification email to the interested parties that assign the role capability
825 * @param object $recipient user object of the intended recipient
826 * @param stdClass $a associative array of replaceable fields for the templates
828 * @return bool|string result of email_to_user()
830 function quiz_send_notification($recipient, $a) {
832 global $USER;
834 // recipient info for template
835 $a->username = fullname($recipient);
836 $a->userusername = $recipient->username;
837 $a->userusername = $recipient->username;
839 // fetch the subject and body from strings
840 $subject = get_string('emailnotifysubject', 'quiz', $a);
841 $body = get_string('emailnotifybody', 'quiz', $a);
843 // send email and analyse result
844 return email_to_user($recipient, $USER, $subject, $body);
848 * Takes a bunch of information to format into an email and send
849 * to the specified recipient.
851 * @param object $course the course
852 * @param object $quiz the quiz
853 * @param object $attempt this attempt just finished
854 * @param object $context the quiz context
855 * @param object $cm the coursemodule for this quiz
857 * @return int number of emails sent
859 function quiz_send_notification_emails($course, $quiz, $attempt, $context, $cm) {
860 global $CFG, $USER;
861 // we will count goods and bads for error logging
862 $emailresult = array('good' => 0, 'block' => 0, 'fail' => 0);
864 // do nothing if required objects not present
865 if (empty($course) or empty($quiz) or empty($attempt) or empty($context)) {
866 debugging('quiz_send_notification_emails: Email(s) not sent due to program error.',
867 DEBUG_DEVELOPER);
868 return $emailresult['fail'];
871 // check for confirmation required
872 $sendconfirm = false;
873 $notifyexcludeusers = '';
874 if (has_capability('mod/quiz:emailconfirmsubmission', $context, NULL, false)) {
875 // exclude from notify emails later
876 $notifyexcludeusers = $USER->id;
877 // send the email
878 $sendconfirm = true;
881 // check for notifications required
882 $notifyfields = 'u.id, u.username, u.firstname, u.lastname, u.email, u.emailstop, u.lang, u.timezone, u.mailformat, u.maildisplay';
883 $userstonotify = get_users_by_capability($context, 'mod/quiz:emailnotifysubmission',
884 $notifyfields, '', '', '', groups_get_all_groups($course->id, $USER->id),
885 $notifyexcludeusers, false, false, true);
887 // if something to send, then build $a
888 if (! empty($userstonotify) or $sendconfirm) {
889 $a = new stdClass;
890 // course info
891 $a->coursename = $course->fullname;
892 $a->courseshortname = $course->shortname;
893 // quiz info
894 $a->quizname = $quiz->name;
895 $a->quizreportlink = '<a href="report.php?q=' . $quiz->id . '">' . format_string($quiz->name) . ' report</a>';
896 $a->quizreporturl = $CFG->wwwroot . '/mod/quiz/report.php?q=' . $quiz->id;
897 $a->quizreviewlink = '<a href="review.php?attempt=' . $attempt->id . '">' . format_string($quiz->name) . ' review</a>';
898 $a->quizreviewurl = $CFG->wwwroot . '/mod/quiz/review.php?attempt=' . $attempt->id;
899 $a->quizlink = '<a href="view.php?q=' . $quiz->id . '">' . format_string($quiz->name) . '</a>';
900 $a->quizurl = $CFG->wwwroot . '/mod/quiz/view.php?q=' . $quiz->id;
901 // attempt info
902 $a->submissiontime = userdate($attempt->timefinish);
903 $a->timetaken = format_time($attempt->timefinish - $attempt->timestart);
904 // student who sat the quiz info
905 $a->studentidnumber = $USER->idnumber;
906 $a->studentname = fullname($USER);
907 $a->studentusername = $USER->username;
910 // send confirmation if required
911 if ($sendconfirm) {
912 // send the email and update stats
913 switch (quiz_send_confirmation($a)) {
914 case true:
915 $emailresult['good']++;
916 break;
917 case false:
918 $emailresult['fail']++;
919 break;
920 case 'emailstop':
921 $emailresult['block']++;
922 break;
926 // send notifications if required
927 if (!empty($userstonotify)) {
928 // loop through recipients and send an email to each and update stats
929 foreach ($userstonotify as $recipient) {
930 switch (quiz_send_notification($recipient, $a)) {
931 case true:
932 $emailresult['good']++;
933 break;
934 case false:
935 $emailresult['fail']++;
936 break;
937 case 'emailstop':
938 $emailresult['block']++;
939 break;
944 // log errors sending emails if any
945 if (! empty($emailresult['fail'])) {
946 debugging('quiz_send_notification_emails:: '.$emailresult['fail'].' email(s) failed to be sent.', DEBUG_DEVELOPER);
948 if (! empty($emailresult['block'])) {
949 debugging('quiz_send_notification_emails:: '.$emailresult['block'].' email(s) were blocked by the user.', DEBUG_DEVELOPER);
952 // return the number of successfully sent emails
953 return $emailresult['good'];