Merge commit 'catalyst/MOODLE_19_STABLE' into mdl19-linuxchix
[moodle-linuxchix.git] / mod / quiz / locallib.php
blob6a8ace702fe09ce4df875e45322d87c1ea262f32
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.
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 /**#@+
38 * Constants to describe the various states a quiz attempt can be in.
40 define('QUIZ_STATE_DURING', 'during');
41 define('QUIZ_STATE_IMMEDIATELY', 'immedately');
42 define('QUIZ_STATE_OPEN', 'open');
43 define('QUIZ_STATE_CLOSED', 'closed');
44 define('QUIZ_STATE_TEACHERACCESS', 'teacheraccess'); // State only relevant if you are in a studenty role.
45 /**#@-*/
47 /// Functions related to attempts /////////////////////////////////////////
49 /**
50 * Creates an object to represent a new attempt at a quiz
52 * Creates an attempt object to represent an attempt at the quiz by the current
53 * user starting at the current time. The ->id field is not set. The object is
54 * NOT written to the database.
55 * @return object The newly created attempt object.
56 * @param object $quiz The quiz to create an attempt for.
57 * @param integer $attemptnumber The sequence number for the attempt.
59 function quiz_create_attempt($quiz, $attemptnumber) {
60 global $USER, $CFG;
62 if (!$attemptnumber > 1 or !$quiz->attemptonlast or !$attempt = get_record('quiz_attempts', 'quiz', $quiz->id, 'userid', $USER->id, 'attempt', $attemptnumber-1)) {
63 // we are not building on last attempt so create a new attempt
64 $attempt->quiz = $quiz->id;
65 $attempt->userid = $USER->id;
66 $attempt->preview = 0;
67 if ($quiz->shufflequestions) {
68 $attempt->layout = quiz_repaginate($quiz->questions, $quiz->questionsperpage, true);
69 } else {
70 $attempt->layout = $quiz->questions;
74 $timenow = time();
75 $attempt->attempt = $attemptnumber;
76 $attempt->sumgrades = 0.0;
77 $attempt->timestart = $timenow;
78 $attempt->timefinish = 0;
79 $attempt->timemodified = $timenow;
80 $attempt->uniqueid = question_new_attempt_uniqueid();
82 return $attempt;
85 /**
86 * Returns an unfinished attempt (if there is one) for the given
87 * user on the given quiz. This function does not return preview attempts.
89 * @param integer $quizid the id of the quiz.
90 * @param integer $userid the id of the user.
92 * @return mixed the unfinished attempt if there is one, false if not.
94 function quiz_get_user_attempt_unfinished($quizid, $userid) {
95 $attempts = quiz_get_user_attempts($quizid, $userid, 'unfinished', true);
96 if ($attempts) {
97 return array_shift($attempts);
98 } else {
99 return false;
104 * Delete a quiz attempt.
105 * @param mixed $attempt an integer attempt id or an attempt object (row of the quiz_attempts table).
106 * @param object $quiz the quiz object.
108 function quiz_delete_attempt($attempt, $quiz) {
109 if (is_numeric($attempt)) {
110 if (!$attempt = get_record('quiz_attempts', 'id', $attempt)) {
111 return;
115 if ($attempt->quiz != $quiz->id) {
116 debugging("Trying to delete attempt $attempt->id which belongs to quiz $attempt->quiz " .
117 "but was passed quiz $quiz->id.");
118 return;
121 delete_records('quiz_attempts', 'id', $attempt->id);
122 delete_attempt($attempt->uniqueid);
124 // Search quiz_attempts for other instances by this user.
125 // If none, then delete record for this quiz, this user from quiz_grades
126 // else recalculate best grade
128 $userid = $attempt->userid;
129 if (!record_exists('quiz_attempts', 'userid', $userid, 'quiz', $quiz->id)) {
130 delete_records('quiz_grades', 'userid', $userid,'quiz', $quiz->id);
131 } else {
132 quiz_save_best_grade($quiz, $userid);
135 quiz_update_grades($quiz, $userid);
138 /// Functions to do with quiz layout and pages ////////////////////////////////
141 * Returns a comma separated list of question ids for the current page
143 * @return string Comma separated list of question ids
144 * @param string $layout The string representing the quiz layout. Each page is represented as a
145 * comma separated list of question ids and 0 indicating page breaks.
146 * So 5,2,0,3,0 means questions 5 and 2 on page 1 and question 3 on page 2
147 * @param integer $page The number of the current page.
149 function quiz_questions_on_page($layout, $page) {
150 $pages = explode(',0', $layout);
151 return trim($pages[$page], ',');
155 * Returns a comma separated list of question ids for the quiz
157 * @return string Comma separated list of question ids
158 * @param string $layout The string representing the quiz layout. Each page is represented as a
159 * comma separated list of question ids and 0 indicating page breaks.
160 * So 5,2,0,3,0 means questions 5 and 2 on page 1 and question 3 on page 2
162 function quiz_questions_in_quiz($layout) {
163 return str_replace(',0', '', $layout);
167 * Returns the number of pages in the quiz layout
169 * @return integer Comma separated list of question ids
170 * @param string $layout The string representing the quiz layout.
172 function quiz_number_of_pages($layout) {
173 return substr_count($layout, ',0');
177 * Returns the first question number for the current quiz page
179 * @return integer The number of the first question
180 * @param string $quizlayout The string representing the layout for the whole quiz
181 * @param string $pagelayout The string representing the layout for the current page
183 function quiz_first_questionnumber($quizlayout, $pagelayout) {
184 // this works by finding all the questions from the quizlayout that
185 // come before the current page and then adding up their lengths.
186 global $CFG;
187 $start = strpos($quizlayout, ','.$pagelayout.',')-2;
188 if ($start > 0) {
189 $prevlist = substr($quizlayout, 0, $start);
190 return get_field_sql("SELECT sum(length)+1 FROM {$CFG->prefix}question
191 WHERE id IN ($prevlist)");
192 } else {
193 return 1;
198 * Re-paginates the quiz layout
200 * @return string The new layout string
201 * @param string $layout The string representing the quiz layout.
202 * @param integer $perpage The number of questions per page
203 * @param boolean $shuffle Should the questions be reordered randomly?
205 function quiz_repaginate($layout, $perpage, $shuffle=false) {
206 $layout = str_replace(',0', '', $layout); // remove existing page breaks
207 $questions = explode(',', $layout);
208 if ($shuffle) {
209 srand((float)microtime() * 1000000); // for php < 4.2
210 shuffle($questions);
212 $i = 1;
213 $layout = '';
214 foreach ($questions as $question) {
215 if ($perpage and $i > $perpage) {
216 $layout .= '0,';
217 $i = 1;
219 $layout .= $question.',';
220 $i++;
222 return $layout.'0';
226 * Print navigation panel for quiz attempt and review pages
228 * @param integer $page The number of the current page (counting from 0).
229 * @param integer $pages The total number of pages.
231 function quiz_print_navigation_panel($page, $pages) {
232 //$page++;
233 echo '<div class="paging pagingbar">';
234 echo '<span class="title">' . get_string('page') . ':</span>&nbsp;';
235 if ($page > 0) {
236 // Print previous link
237 $strprev = get_string('previous');
238 echo '&nbsp;<a href="javascript:navigate(' . ($page - 1) . ');" title="'
239 . $strprev . '">(' . $strprev . ')</a>&nbsp;';
241 for ($i = 0; $i < $pages; $i++) {
242 if ($i == $page) {
243 echo '&nbsp;<span class="thispage">'.($i+1).'</span>&nbsp;';
244 } else {
245 echo '&nbsp;<a href="javascript:navigate(' . ($i) . ');">'.($i+1).'</a>&nbsp;';
249 if ($page < $pages - 1) {
250 // Print next link
251 $strnext = get_string('next');
252 echo '&nbsp;<a href="javascript:navigate(' . ($page + 1) . ');" title="'
253 . $strnext . '">(' . $strnext . ')</a>&nbsp;';
255 echo '</div>';
258 /// Functions to do with quiz grades //////////////////////////////////////////
261 * Creates an array of maximum grades for a quiz
263 * The grades are extracted from the quiz_question_instances table.
264 * @return array Array of grades indexed by question id
265 * These are the maximum possible grades that
266 * students can achieve for each of the questions
267 * @param integer $quiz The quiz object
269 function quiz_get_all_question_grades($quiz) {
270 global $CFG;
272 $questionlist = quiz_questions_in_quiz($quiz->questions);
273 if (empty($questionlist)) {
274 return array();
277 $instances = get_records_sql("SELECT question,grade,id
278 FROM {$CFG->prefix}quiz_question_instances
279 WHERE quiz = '$quiz->id'" .
280 (is_null($questionlist) ? '' :
281 "AND question IN ($questionlist)"));
283 $list = explode(",", $questionlist);
284 $grades = array();
286 foreach ($list as $qid) {
287 if (isset($instances[$qid])) {
288 $grades[$qid] = $instances[$qid]->grade;
289 } else {
290 $grades[$qid] = 1;
293 return $grades;
297 * Get the best current grade for a particular user in a quiz.
299 * @param object $quiz the quiz object.
300 * @param integer $userid the id of the user.
301 * @return float the user's current grade for this quiz.
303 function quiz_get_best_grade($quiz, $userid) {
304 $grade = get_field('quiz_grades', 'grade', 'quiz', $quiz->id, 'userid', $userid);
306 // Need to detect errors/no result, without catching 0 scores.
307 if (is_numeric($grade)) {
308 return round($grade, $quiz->decimalpoints);
309 } else {
310 return NULL;
315 * Convert the raw grade stored in $attempt into a grade out of the maximum
316 * grade for this quiz.
318 * @param float $rawgrade the unadjusted grade, fof example $attempt->sumgrades
319 * @param object $quiz the quiz object. Only the fields grade, sumgrades and decimalpoints are used.
320 * @return float the rescaled grade.
322 function quiz_rescale_grade($rawgrade, $quiz, $round = true) {
323 if ($quiz->sumgrades) {
324 $grade = $rawgrade * $quiz->grade / $quiz->sumgrades;
325 if ($round) {
326 $grade = round($grade, $quiz->decimalpoints);
328 } else {
329 $grade = 0;
331 return $grade;
335 * Get the feedback text that should be show to a student who
336 * got this grade on this quiz. The feedback is processed ready for diplay.
338 * @param float $grade a grade on this quiz.
339 * @param integer $quizid the id of the quiz object.
340 * @return string the comment that corresponds to this grade (empty string if there is not one.
342 function quiz_feedback_for_grade($grade, $quizid) {
343 $feedback = get_field_select('quiz_feedback', 'feedbacktext',
344 "quizid = $quizid AND mingrade <= $grade AND $grade < maxgrade");
346 if (empty($feedback)) {
347 $feedback = '';
350 // Clean the text, ready for display.
351 $formatoptions = new stdClass;
352 $formatoptions->noclean = true;
353 $feedback = format_text($feedback, FORMAT_MOODLE, $formatoptions);
355 return $feedback;
359 * @param integer $quizid the id of the quiz object.
360 * @return boolean Whether this quiz has any non-blank feedback text.
362 function quiz_has_feedback($quizid) {
363 static $cache = array();
364 if (!array_key_exists($quizid, $cache)) {
365 $cache[$quizid] = record_exists_select('quiz_feedback',
366 "quizid = $quizid AND " . sql_isnotempty('quiz_feedback', 'feedbacktext', false, true));
368 return $cache[$quizid];
372 * The quiz grade is the score that student's results are marked out of. When it
373 * changes, the corresponding data in quiz_grades and quiz_feedback needs to be
374 * rescaled.
376 * @param float $newgrade the new maximum grade for the quiz.
377 * @param object $quiz the quiz we are updating. Passed by reference so its grade field can be updated too.
378 * @return boolean indicating success or failure.
380 function quiz_set_grade($newgrade, &$quiz) {
381 // This is potentially expensive, so only do it if necessary.
382 if (abs($quiz->grade - $newgrade) < 1e-7) {
383 // Nothing to do.
384 return true;
387 // Use a transaction, so that on those databases that support it, this is safer.
388 begin_sql();
390 // Update the quiz table.
391 $success = set_field('quiz', 'grade', $newgrade, 'id', $quiz->instance);
393 // Rescaling the other data is only possible if the old grade was non-zero.
394 if ($quiz->grade > 1e-7) {
395 global $CFG;
397 $factor = $newgrade/$quiz->grade;
398 $quiz->grade = $newgrade;
400 // Update the quiz_grades table.
401 $timemodified = time();
402 $success = $success && execute_sql("
403 UPDATE {$CFG->prefix}quiz_grades
404 SET grade = $factor * grade, timemodified = $timemodified
405 WHERE quiz = $quiz->id
406 ", false);
408 // Update the quiz_grades table.
409 $success = $success && execute_sql("
410 UPDATE {$CFG->prefix}quiz_feedback
411 SET mingrade = $factor * mingrade, maxgrade = $factor * maxgrade
412 WHERE quizid = $quiz->id
413 ", false);
416 // update grade item and send all grades to gradebook
417 quiz_grade_item_update($quiz);
418 quiz_update_grades($quiz);
420 if ($success) {
421 return commit_sql();
422 } else {
423 rollback_sql();
424 return false;
429 * Save the overall grade for a user at a quiz in the quiz_grades table
431 * @param object $quiz The quiz for which the best grade is to be calculated and then saved.
432 * @param integer $userid The userid to calculate the grade for. Defaults to the current user.
433 * @return boolean Indicates success or failure.
435 function quiz_save_best_grade($quiz, $userid = null) {
436 global $USER;
438 if (empty($userid)) {
439 $userid = $USER->id;
442 // Get all the attempts made by the user
443 if (!$attempts = quiz_get_user_attempts($quiz->id, $userid)) {
444 notify('Could not find any user attempts');
445 return false;
448 // Calculate the best grade
449 $bestgrade = quiz_calculate_best_grade($quiz, $attempts);
450 $bestgrade = quiz_rescale_grade($bestgrade, $quiz);
452 // Save the best grade in the database
453 if ($grade = get_record('quiz_grades', 'quiz', $quiz->id, 'userid', $userid)) {
454 $grade->grade = $bestgrade;
455 $grade->timemodified = time();
456 if (!update_record('quiz_grades', $grade)) {
457 notify('Could not update best grade');
458 return false;
460 } else {
461 $grade->quiz = $quiz->id;
462 $grade->userid = $userid;
463 $grade->grade = $bestgrade;
464 $grade->timemodified = time();
465 if (!insert_record('quiz_grades', $grade)) {
466 notify('Could not insert new best grade');
467 return false;
471 quiz_update_grades($quiz, $userid);
472 return true;
476 * Calculate the overall grade for a quiz given a number of attempts by a particular user.
478 * @return float The overall grade
479 * @param object $quiz The quiz for which the best grade is to be calculated
480 * @param array $attempts An array of all the attempts of the user at the quiz
482 function quiz_calculate_best_grade($quiz, $attempts) {
484 switch ($quiz->grademethod) {
486 case QUIZ_ATTEMPTFIRST:
487 foreach ($attempts as $attempt) {
488 return $attempt->sumgrades;
490 break;
492 case QUIZ_ATTEMPTLAST:
493 foreach ($attempts as $attempt) {
494 $final = $attempt->sumgrades;
496 return $final;
498 case QUIZ_GRADEAVERAGE:
499 $sum = 0;
500 $count = 0;
501 foreach ($attempts as $attempt) {
502 $sum += $attempt->sumgrades;
503 $count++;
505 return (float)$sum/$count;
507 default:
508 case QUIZ_GRADEHIGHEST:
509 $max = 0;
510 foreach ($attempts as $attempt) {
511 if ($attempt->sumgrades > $max) {
512 $max = $attempt->sumgrades;
515 return $max;
520 * Return the attempt with the best grade for a quiz
522 * Which attempt is the best depends on $quiz->grademethod. If the grade
523 * method is GRADEAVERAGE then this function simply returns the last attempt.
524 * @return object The attempt with the best grade
525 * @param object $quiz The quiz for which the best grade is to be calculated
526 * @param array $attempts An array of all the attempts of the user at the quiz
528 function quiz_calculate_best_attempt($quiz, $attempts) {
530 switch ($quiz->grademethod) {
532 case QUIZ_ATTEMPTFIRST:
533 foreach ($attempts as $attempt) {
534 return $attempt;
536 break;
538 case QUIZ_GRADEAVERAGE: // need to do something with it :-)
539 case QUIZ_ATTEMPTLAST:
540 foreach ($attempts as $attempt) {
541 $final = $attempt;
543 return $final;
545 default:
546 case QUIZ_GRADEHIGHEST:
547 $max = -1;
548 foreach ($attempts as $attempt) {
549 if ($attempt->sumgrades > $max) {
550 $max = $attempt->sumgrades;
551 $maxattempt = $attempt;
554 return $maxattempt;
559 * @return the options for calculating the quiz grade from the individual attempt grades.
561 function quiz_get_grading_options() {
562 return array (
563 QUIZ_GRADEHIGHEST => get_string('gradehighest', 'quiz'),
564 QUIZ_GRADEAVERAGE => get_string('gradeaverage', 'quiz'),
565 QUIZ_ATTEMPTFIRST => get_string('attemptfirst', 'quiz'),
566 QUIZ_ATTEMPTLAST => get_string('attemptlast', 'quiz'));
570 * @param int $option one of the values QUIZ_GRADEHIGHEST, QUIZ_GRADEAVERAGE, QUIZ_ATTEMPTFIRST or QUIZ_ATTEMPTLAST.
571 * @return the lang string for that option.
573 function quiz_get_grading_option_name($option) {
574 $strings = quiz_get_grading_options();
575 return $strings[$option];
578 /// Other quiz functions ////////////////////////////////////////////////////
581 * Parse field names used for the replace options on question edit forms
583 function quiz_parse_fieldname($name, $nameprefix='question') {
584 $reg = array();
585 if (preg_match("/$nameprefix(\\d+)(\w+)/", $name, $reg)) {
586 return array('mode' => $reg[2], 'id' => (int)$reg[1]);
587 } else {
588 return false;
593 * Upgrade states for an attempt to Moodle 1.5 model
595 * Any state that does not yet have its timestamp set to nonzero has not yet been upgraded from Moodle 1.4
596 * The reason these are still around is that for large sites it would have taken too long to
597 * upgrade all states at once. This function sets the timestamp field and creates an entry in the
598 * question_sessions table.
599 * @param object $attempt The attempt whose states need upgrading
601 function quiz_upgrade_states($attempt) {
602 global $CFG;
603 // The old quiz model only allowed a single response per quiz attempt so that there will be
604 // only one state record per question for this attempt.
606 // We set the timestamp of all states to the timemodified field of the attempt.
607 execute_sql("UPDATE {$CFG->prefix}question_states SET timestamp = '$attempt->timemodified' WHERE attempt = '$attempt->uniqueid'", false);
609 // For each state we create an entry in the question_sessions table, with both newest and
610 // newgraded pointing to this state.
611 // Actually we only do this for states whose question is actually listed in $attempt->layout.
612 // We do not do it for states associated to wrapped questions like for example the questions
613 // used by a RANDOM question
614 $session = new stdClass;
615 $session->attemptid = $attempt->uniqueid;
616 $questionlist = quiz_questions_in_quiz($attempt->layout);
617 if ($questionlist and $states = get_records_select('question_states', "attempt = '$attempt->uniqueid' AND question IN ($questionlist)")) {
618 foreach ($states as $state) {
619 $session->newgraded = $state->id;
620 $session->newest = $state->id;
621 $session->questionid = $state->question;
622 insert_record('question_sessions', $session, false);
628 * @param object $quiz the quiz
629 * @param object $question the question
630 * @return the HTML for a preview question icon.
632 function quiz_question_preview_button($quiz, $question) {
633 global $CFG, $COURSE;
634 if (!question_has_capability_on($question, 'use', $question->category)){
635 return '';
637 $strpreview = get_string('previewquestion', 'quiz');
638 $quizorcourseid = $quiz->id?('&amp;quizid=' . $quiz->id):('&amp;courseid=' .$COURSE->id);
639 return link_to_popup_window('/question/preview.php?id=' . $question->id . $quizorcourseid, 'questionpreview',
640 "<img src=\"$CFG->pixpath/t/preview.gif\" class=\"iconsmall\" alt=\"$strpreview\" />",
641 0, 0, $strpreview, QUESTION_PREVIEW_POPUP_OPTIONS, true);
645 * Determine render options
647 * @param int $reviewoptions
648 * @param object $state
650 function quiz_get_renderoptions($reviewoptions, $state) {
651 $options = new stdClass;
653 // Show the question in readonly (review) mode if the question is in
654 // the closed state
655 $options->readonly = question_state_is_closed($state);
657 // Show feedback once the question has been graded (if allowed by the quiz)
658 $options->feedback = question_state_is_graded($state) && ($reviewoptions & QUIZ_REVIEW_FEEDBACK & QUIZ_REVIEW_IMMEDIATELY);
660 // Show validation only after a validation event
661 $options->validation = QUESTION_EVENTVALIDATE === $state->event;
663 // Show correct responses in readonly mode if the quiz allows it
664 $options->correct_responses = $options->readonly && ($reviewoptions & QUIZ_REVIEW_ANSWERS & QUIZ_REVIEW_IMMEDIATELY);
666 // Show general feedback if the question has been graded and the quiz allows it.
667 $options->generalfeedback = question_state_is_graded($state) && ($reviewoptions & QUIZ_REVIEW_GENERALFEEDBACK & QUIZ_REVIEW_IMMEDIATELY);
669 // Show overallfeedback once the attempt is over.
670 $options->overallfeedback = false;
672 // Always show responses and scores
673 $options->responses = true;
674 $options->scores = true;
675 $options->quizstate = QUIZ_STATE_DURING;
677 return $options;
681 * Determine review options
683 * @param object $quiz the quiz instance.
684 * @param object $attempt the attempt in question.
685 * @param $context the roles and permissions context,
686 * normally the context for the quiz module instance.
688 * @return object an object with boolean fields responses, scores, feedback,
689 * correct_responses, solutions and general feedback
691 function quiz_get_reviewoptions($quiz, $attempt, $context=null) {
692 $options = new stdClass;
693 $options->readonly = true;
695 // Provide the links to the question review and comment script
696 $options->questionreviewlink = '/mod/quiz/reviewquestion.php';
698 // Show a link to the comment box only for closed attempts
699 if ($attempt->timefinish && !is_null($context) && has_capability('mod/quiz:grade', $context)) {
700 $options->questioncommentlink = '/mod/quiz/comment.php';
703 if (!is_null($context) && has_capability('mod/quiz:viewreports', $context) &&
704 has_capability('moodle/grade:viewhidden', $context) && !$attempt->preview) {
705 // People who can see reports and hidden grades should be shown everything,
706 // except during preview when teachers want to see what students see.
707 $options->responses = true;
708 $options->scores = true;
709 $options->feedback = true;
710 $options->correct_responses = true;
711 $options->solutions = false;
712 $options->generalfeedback = true;
713 $options->overallfeedback = true;
714 $options->quizstate = QUIZ_STATE_TEACHERACCESS;
715 } else {
716 // Work out the state of the attempt ...
717 if (((time() - $attempt->timefinish) < 120) || $attempt->timefinish==0) {
718 $quiz_state_mask = QUIZ_REVIEW_IMMEDIATELY;
719 $options->quizstate = QUIZ_STATE_IMMEDIATELY;
720 } else if (!$quiz->timeclose or time() < $quiz->timeclose) {
721 $quiz_state_mask = QUIZ_REVIEW_OPEN;
722 $options->quizstate = QUIZ_STATE_OPEN;
723 } else {
724 $quiz_state_mask = QUIZ_REVIEW_CLOSED;
725 $options->quizstate = QUIZ_STATE_CLOSED;
728 // ... and hence extract the appropriate review options.
729 $options->responses = ($quiz->review & $quiz_state_mask & QUIZ_REVIEW_RESPONSES) ? 1 : 0;
730 $options->scores = ($quiz->review & $quiz_state_mask & QUIZ_REVIEW_SCORES) ? 1 : 0;
731 $options->feedback = ($quiz->review & $quiz_state_mask & QUIZ_REVIEW_FEEDBACK) ? 1 : 0;
732 $options->correct_responses = ($quiz->review & $quiz_state_mask & QUIZ_REVIEW_ANSWERS) ? 1 : 0;
733 $options->solutions = ($quiz->review & $quiz_state_mask & QUIZ_REVIEW_SOLUTIONS) ? 1 : 0;
734 $options->generalfeedback = ($quiz->review & $quiz_state_mask & QUIZ_REVIEW_GENERALFEEDBACK) ? 1 : 0;
735 $options->overallfeedback = $attempt->timefinish && ($quiz->review & $quiz_state_mask & QUIZ_REVIEW_OVERALLFEEDBACK);
738 return $options;
742 * Combines the review options from a number of different quiz attempts.
743 * Returns an array of two ojects, so he suggested way of calling this
744 * funciton is:
745 * list($someoptions, $alloptions) = quiz_get_combined_reviewoptions(...)
747 * @param object $quiz the quiz instance.
748 * @param array $attempts an array of attempt objects.
749 * @param $context the roles and permissions context,
750 * normally the context for the quiz module instance.
752 * @return array of two options objects, one showing which options are true for
753 * at least one of the attempts, the other showing which options are true
754 * for all attempts.
756 function quiz_get_combined_reviewoptions($quiz, $attempts, $context=null) {
757 $fields = array('readonly', 'scores', 'feedback', 'correct_responses', 'solutions', 'generalfeedback', 'overallfeedback');
758 $someoptions = new stdClass;
759 $alloptions = new stdClass;
760 foreach ($fields as $field) {
761 $someoptions->$field = false;
762 $alloptions->$field = true;
764 foreach ($attempts as $attempt) {
765 $attemptoptions = quiz_get_reviewoptions($quiz, $attempt, $context);
766 foreach ($fields as $field) {
767 $someoptions->$field = $someoptions->$field || $attemptoptions->$field;
768 $alloptions->$field = $alloptions->$field && $attemptoptions->$field;
771 return array($someoptions, $alloptions);
774 /// FUNCTIONS FOR SENDING NOTIFICATION EMAILS ///////////////////////////////
777 * Sends confirmation email to the student taking the course
779 * @param stdClass $a associative array of replaceable fields for the templates
781 * @return bool|string result of email_to_user()
783 function quiz_send_confirmation($a) {
785 global $USER;
787 // recipient is self
788 $a->useridnumber = $USER->idnumber;
789 $a->username = fullname($USER);
790 $a->userusername = $USER->username;
792 // fetch the subject and body from strings
793 $subject = get_string('emailconfirmsubject', 'quiz', $a);
794 $body = get_string('emailconfirmbody', 'quiz', $a);
796 // send email and analyse result
797 return email_to_user($USER, get_admin(), $subject, $body);
801 * Sends notification email to the interested parties that assign the role capability
803 * @param object $recipient user object of the intended recipient
804 * @param stdClass $a associative array of replaceable fields for the templates
806 * @return bool|string result of email_to_user()
808 function quiz_send_notification($recipient, $a) {
810 global $USER;
812 // recipient info for template
813 $a->username = fullname($recipient);
814 $a->userusername = $recipient->username;
815 $a->userusername = $recipient->username;
817 // fetch the subject and body from strings
818 $subject = get_string('emailnotifysubject', 'quiz', $a);
819 $body = get_string('emailnotifybody', 'quiz', $a);
821 // send email and analyse result
822 return email_to_user($recipient, $USER, $subject, $body);
826 * Takes a bunch of information to format into an email and send
827 * to the specified recipient.
829 * @param object $course the course
830 * @param object $quiz the quiz
831 * @param object $attempt this attempt just finished
832 * @param object $context the quiz context
833 * @param object $cm the coursemodule for this quiz
835 * @return int number of emails sent
837 function quiz_send_notification_emails($course, $quiz, $attempt, $context, $cm) {
838 global $CFG, $USER;
839 // we will count goods and bads for error logging
840 $emailresult = array('good' => 0, 'block' => 0, 'fail' => 0);
842 // do nothing if required objects not present
843 if (empty($course) or empty($quiz) or empty($attempt) or empty($context)) {
844 debugging('quiz_send_notification_emails: Email(s) not sent due to program error.',
845 DEBUG_DEVELOPER);
846 return $emailresult['fail'];
849 // check for confirmation required
850 $sendconfirm = false;
851 $notifyexcludeusers = '';
852 if (has_capability('mod/quiz:emailconfirmsubmission', $context, NULL, false)) {
853 // exclude from notify emails later
854 $notifyexcludeusers = $USER->id;
855 // send the email
856 $sendconfirm = true;
859 // check for notifications required
860 $notifyfields = 'u.id, u.username, u.firstname, u.lastname, u.email, u.emailstop, u.lang, u.timezone, u.mailformat, u.maildisplay';
861 $groups = groups_get_all_groups($course->id, $USER->id);
862 if (is_array($groups) && count($groups) > 0) {
863 $groups = array_keys($groups);
864 } else if (groups_get_activity_groupmode($cm, $course) != NOGROUPS) {
865 // If the user is not in a group, and the quiz is set to group mode,
866 // then set $gropus to a non-existant id so that only users with
867 // 'moodle/site:accessallgroups' get notified.
868 $groups = -1;
869 } else {
870 $groups = '';
872 $userstonotify = get_users_by_capability($context, 'mod/quiz:emailnotifysubmission',
873 $notifyfields, '', '', '', $groups, $notifyexcludeusers, false, false, true);
875 // if something to send, then build $a
876 if (! empty($userstonotify) or $sendconfirm) {
877 $a = new stdClass;
878 // course info
879 $a->coursename = $course->fullname;
880 $a->courseshortname = $course->shortname;
881 // quiz info
882 $a->quizname = $quiz->name;
883 $a->quizreporturl = $CFG->wwwroot . '/mod/quiz/report.php?q=' . $quiz->id;
884 $a->quizreportlink = '<a href="' . $a->quizreporturl . '">' . format_string($quiz->name) . ' report</a>';
885 $a->quizreviewurl = $CFG->wwwroot . '/mod/quiz/review.php?attempt=' . $attempt->id;
886 $a->quizreviewlink = '<a href="' . $a->quizreviewurl . '">' . format_string($quiz->name) . ' review</a>';
887 $a->quizurl = $CFG->wwwroot . '/mod/quiz/view.php?q=' . $quiz->id;
888 $a->quizlink = '<a href="' . $a->quizurl . '">' . format_string($quiz->name) . '</a>';
889 // attempt info
890 $a->submissiontime = userdate($attempt->timefinish);
891 $a->timetaken = format_time($attempt->timefinish - $attempt->timestart);
892 // student who sat the quiz info
893 $a->studentidnumber = $USER->idnumber;
894 $a->studentname = fullname($USER);
895 $a->studentusername = $USER->username;
898 // send confirmation if required
899 if ($sendconfirm) {
900 // send the email and update stats
901 switch (quiz_send_confirmation($a)) {
902 case true:
903 $emailresult['good']++;
904 break;
905 case false:
906 $emailresult['fail']++;
907 break;
908 case 'emailstop':
909 $emailresult['block']++;
910 break;
914 // send notifications if required
915 if (!empty($userstonotify)) {
916 // loop through recipients and send an email to each and update stats
917 foreach ($userstonotify as $recipient) {
918 switch (quiz_send_notification($recipient, $a)) {
919 case true:
920 $emailresult['good']++;
921 break;
922 case false:
923 $emailresult['fail']++;
924 break;
925 case 'emailstop':
926 $emailresult['block']++;
927 break;
932 // log errors sending emails if any
933 if (! empty($emailresult['fail'])) {
934 debugging('quiz_send_notification_emails:: '.$emailresult['fail'].' email(s) failed to be sent.', DEBUG_DEVELOPER);
936 if (! empty($emailresult['block'])) {
937 debugging('quiz_send_notification_emails:: '.$emailresult['block'].' email(s) were blocked by the user.', DEBUG_DEVELOPER);
940 // return the number of successfully sent emails
941 return $emailresult['good'];