MDL-20636 Try to allow Opaque questions to work in deferred feedback mode, if they...
[moodle.git] / mod / quiz / locallib.php
CommitLineData
83192608 1<?php
84e628a0 2
3///////////////////////////////////////////////////////////////////////////
4// //
5// NOTICE OF COPYRIGHT //
6// //
7// Moodle - Modular Object-Oriented Dynamic Learning Environment //
8// http://moodle.org //
9// //
10// Copyright (C) 1999 onwards Martin Dougiamas http://dougiamas.com //
11// //
12// This program is free software; you can redistribute it and/or modify //
13// it under the terms of the GNU General Public License as published by //
14// the Free Software Foundation; either version 2 of the License, or //
15// (at your option) any later version. //
16// //
17// This program is distributed in the hope that it will be useful, //
18// but WITHOUT ANY WARRANTY; without even the implied warranty of //
19// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
20// GNU General Public License for more details: //
21// //
22// http://www.gnu.org/copyleft/gpl.html //
23// //
24///////////////////////////////////////////////////////////////////////////
25
ee1fb969 26/**
26d12a0c 27 * Library of functions used by the quiz module.
28 *
29 * This contains functions that are called from within the quiz module only
30 * Functions that are also called by core Moodle are in {@link lib.php}
31 * This script also loads the code in {@link questionlib.php} which holds
32 * the module-indpendent code for handling questions and which in turn
33 * initialises all the questiontype classes.
f63a4ff2 34 *
26d12a0c 35 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
36 * @package quiz
37 */
76cacec8 38
66d07f81 39if (!defined('MOODLE_INTERNAL')) {
40 die('Direct access to this script is forbidden.'); /// It must be included from a Moodle page.
41}
42
ee1fb969 43/**
26d12a0c 44 * Include those library functions that are also used by core Moodle or other modules
45 */
46require_once($CFG->dirroot . '/mod/quiz/lib.php');
c52c62d1 47require_once($CFG->dirroot . '/mod/quiz/accessrules.php');
78e7a3dd 48require_once($CFG->dirroot . '/mod/quiz/attemptlib.php');
84e628a0 49require_once($CFG->dirroot . '/question/editlib.php');
3b120e46 50require_once($CFG->libdir . '/eventslib.php');
99d19c13 51require_once($CFG->libdir . '/filelib.php');
76cacec8 52
26d12a0c 53/// Constants ///////////////////////////////////////////////////////////////////
76cacec8 54
a4faed69 55/**#@+
56 * Constants to describe the various states a quiz attempt can be in.
57 */
c52c62d1 58define('QUIZ_STATE_DURING', 'during');
59define('QUIZ_STATE_IMMEDIATELY', 'immedately');
60define('QUIZ_STATE_OPEN', 'open');
61define('QUIZ_STATE_CLOSED', 'closed');
a4faed69 62define('QUIZ_STATE_TEACHERACCESS', 'teacheraccess'); // State only relevant if you are in a studenty role.
63/**#@-*/
64
c52c62d1 65/**
66 * We show the countdown timer if there is less than this amount of time left before the
67 * the quiz close date. (1 hour)
68 */
69define('QUIZ_SHOW_TIME_BEFORE_DEADLINE', '3600');
70
26d12a0c 71/// Functions related to attempts /////////////////////////////////////////
ee1fb969 72
73/**
26d12a0c 74 * Creates an object to represent a new attempt at a quiz
75 *
76 * Creates an attempt object to represent an attempt at the quiz by the current
77 * user starting at the current time. The ->id field is not set. The object is
78 * NOT written to the database.
c52c62d1 79 *
80 * @param object $quiz the quiz to create an attempt for.
81 * @param integer $attemptnumber the sequence number for the attempt.
82 * @param object $lastattempt the previous attempt by this user, if any. Only needed
83 * if $attemptnumber > 1 and $quiz->attemptonlast is true.
84 * @param integer $timenow the time the attempt was started at.
85 * @param boolean $ispreview whether this new attempt is a preview.
86 *
87 * @return object the newly created attempt object.
26d12a0c 88 */
c52c62d1 89function quiz_create_attempt($quiz, $attemptnumber, $lastattempt, $timenow, $ispreview = false) {
90 global $USER;
ee1fb969 91
e89e09d2 92 if ($attemptnumber == 1 || !$quiz->attemptonlast) {
c52c62d1 93 /// We are not building on last attempt so create a new attempt.
94 $attempt = new stdClass;
75e1df6f 95 $attempt->quiz = $quiz->id;
96 $attempt->userid = $USER->id;
97 $attempt->preview = 0;
98 if ($quiz->shufflequestions) {
fa583f5f 99 $attempt->layout = quiz_clean_layout(quiz_repaginate($quiz->questions, $quiz->questionsperpage, true),true);
75e1df6f 100 } else {
fa583f5f 101 $attempt->layout = quiz_clean_layout($quiz->questions,true);
75e1df6f 102 }
c52c62d1 103 } else {
104 /// Build on last attempt.
105 if (empty($lastattempt)) {
5a2a5331 106 print_error('cannotfindprevattempt', 'quiz');
c52c62d1 107 }
108 $attempt = $lastattempt;
75e1df6f 109 }
110
ee1fb969 111 $attempt->attempt = $attemptnumber;
112 $attempt->sumgrades = 0.0;
ee1fb969 113 $attempt->timestart = $timenow;
114 $attempt->timefinish = 0;
115 $attempt->timemodified = $timenow;
4f48fb42 116 $attempt->uniqueid = question_new_attempt_uniqueid();
75e1df6f 117
c52c62d1 118/// If this is a preview, mark it as such.
119 if ($ispreview) {
120 $attempt->preview = 1;
121 }
122
ee1fb969 123 return $attempt;
124}
125
cd06115f 126/**
c52c62d1 127 * Returns the unfinished attempt for the given
128 * user on the given quiz, if there is one.
77ed3ba5 129 *
cd06115f 130 * @param integer $quizid the id of the quiz.
131 * @param integer $userid the id of the user.
77ed3ba5 132 *
133 * @return mixed the unfinished attempt if there is one, false if not.
cd06115f 134 */
0d156caa 135function quiz_get_user_attempt_unfinished($quizid, $userid) {
ed8bebce 136 $attempts = quiz_get_user_attempts($quizid, $userid, 'unfinished', true);
77ed3ba5 137 if ($attempts) {
138 return array_shift($attempts);
139 } else {
140 return false;
141 }
920fb237 142}
143
c52c62d1 144/**
145 * Returns the most recent attempt by a given user on a given quiz.
146 * May be finished, or may not.
147 *
148 * @param integer $quizid the id of the quiz.
149 * @param integer $userid the id of the user.
150 *
58ffdbb8 151 * @return mixed the attempt if there is one, false if not.
c52c62d1 152 */
153function quiz_get_latest_attempt_by_user($quizid, $userid) {
9cf4a18b 154 global $CFG, $DB;
155 $attempt = $DB->get_records_sql('SELECT qa.* FROM {quiz_attempts} qa
156 WHERE qa.quiz=? AND qa.userid= ? ORDER BY qa.timestart DESC, qa.id DESC', array($quizid, $userid), 0, 1);
58ffdbb8 157 if ($attempt) {
158 return array_shift($attempt);
159 } else {
160 return false;
161 }
c52c62d1 162}
163
164/**
dfee5a9a 165 * Load an attempt by id. You need to use this method instead of $DB->get_record, because
c52c62d1 166 * of some ancient history to do with the upgrade from Moodle 1.4 to 1.5, See the comment
167 * after CREATE TABLE `prefix_quiz_newest_states` in mod/quiz/db/mysql.php.
168 *
169 * @param integer $attemptid the id of the attempt to load.
170 */
171function quiz_load_attempt($attemptid) {
9cf4a18b 172 global $DB;
173 $attempt = $DB->get_record('quiz_attempts', array('id' => $attemptid));
45da388f 174 if (!$attempt) {
175 return false;
176 }
c52c62d1 177
7a719748 178 // TODO kill this.
9cf4a18b 179 if (!$DB->record_exists('question_sessions', array('attemptid' => $attempt->uniqueid))) {
c52c62d1 180 /// this attempt has not yet been upgraded to the new model
181 quiz_upgrade_states($attempt);
182 }
183
184 return $attempt;
185}
186
ff51d646 187/**
188 * Delete a quiz attempt.
97344ae3 189 * @param mixed $attempt an integer attempt id or an attempt object (row of the quiz_attempts table).
190 * @param object $quiz the quiz object.
ff51d646 191 */
192function quiz_delete_attempt($attempt, $quiz) {
9cf4a18b 193 global $DB;
ff51d646 194 if (is_numeric($attempt)) {
9cf4a18b 195 if (!$attempt = $DB->get_record('quiz_attempts', array('id' => $attempt))) {
ff51d646 196 return;
197 }
198 }
271e6dec 199
ff51d646 200 if ($attempt->quiz != $quiz->id) {
201 debugging("Trying to delete attempt $attempt->id which belongs to quiz $attempt->quiz " .
202 "but was passed quiz $quiz->id.");
203 return;
204 }
271e6dec 205
9cf4a18b 206 $DB->delete_records('quiz_attempts', array('id' => $attempt->id));
ff51d646 207 delete_attempt($attempt->uniqueid);
208
209 // Search quiz_attempts for other instances by this user.
210 // If none, then delete record for this quiz, this user from quiz_grades
211 // else recalculate best grade
212
213 $userid = $attempt->userid;
9cf4a18b 214 if (!$DB->record_exists('quiz_attempts', array('userid' => $userid, 'quiz' => $quiz->id))) {
215 $DB->delete_records('quiz_grades', array('userid' => $userid,'quiz' => $quiz->id));
ff51d646 216 } else {
217 quiz_save_best_grade($quiz, $userid);
218 }
07a7d859 219
220 quiz_update_grades($quiz, $userid);
ff51d646 221}
222
4299df1d 223/**
224 * Delete all the preview attempts at a quiz, or possibly all the attempts belonging
225 * to one user.
226 * @param object $quiz the quiz object.
227 * @param integer $userid (optional) if given, only delete the previews belonging to this user.
228 */
229function quiz_delete_previews($quiz, $userid = null) {
230 global $DB;
231 $conditions = array('quiz' => $quiz->id, 'preview' => 1);
232 if (!empty($userid)) {
233 $conditions['userid'] = $userid;
234 }
235 $previewattempts = $DB->get_records('quiz_attempts', $conditions);
236 foreach ($previewattempts as $attempt) {
237 quiz_delete_attempt($attempt, $quiz);
238 }
239}
240
84e628a0 241/**
242 * @param integer $quizid The quiz id.
243 * @return boolean whether this quiz has any (non-preview) attempts.
244 */
245function quiz_has_attempts($quizid) {
246 global $DB;
247 return $DB->record_exists('quiz_attempts', array('quiz' => $quizid, 'preview' => 0));
248}
249
26d12a0c 250/// Functions to do with quiz layout and pages ////////////////////////////////
0d156caa 251
ee1fb969 252/**
26d12a0c 253 * Returns a comma separated list of question ids for the current page
254 *
1332ce53 255 * @param string $layout the string representing the quiz layout. Each page is represented as a
256 * comma separated list of question ids and 0 indicating page breaks.
257 * So 5,2,0,3,0 means questions 5 and 2 on page 1 and question 3 on page 2
258 * @param integer $page the number of the current page.
259 * @return string comma separated list of question ids
26d12a0c 260 */
ee1fb969 261function quiz_questions_on_page($layout, $page) {
262 $pages = explode(',0', $layout);
263 return trim($pages[$page], ',');
76cacec8 264}
265
ee1fb969 266/**
26d12a0c 267 * Returns a comma separated list of question ids for the quiz
268 *
1332ce53 269 * @param string $layout The string representing the quiz layout. Each page is
270 * represented as a comma separated list of question ids and 0 indicating
271 * page breaks. So 5,2,0,3,0 means questions 5 and 2 on page 1 and question
272 * 3 on page 2
273 * @return string comma separated list of question ids, without page breaks.
26d12a0c 274 */
ee1fb969 275function quiz_questions_in_quiz($layout) {
1332ce53 276 $layout = preg_replace('/,(0+,)+/', ',', $layout); // Remove page breaks from the middle.
277 $layout = preg_replace('/^0+,/', '', $layout); // And from the start.
278 $layout = preg_replace('/(^|,)0+$/', '', $layout); // And from the end.
fa583f5f 279 return $layout;
ee1fb969 280}
76cacec8 281
ee1fb969 282/**
1332ce53 283 * Returns the number of pages in a quiz layout
26d12a0c 284 *
1332ce53 285 * @param string $layout The string representing the quiz layout. Always ends in ,0
286 * @return integer The number of pages in the quiz.
26d12a0c 287 */
ee1fb969 288function quiz_number_of_pages($layout) {
1332ce53 289 $count = 0;
290 if ($layout !== '') {
fa583f5f 291 //if the first page is empty, include it, too
1332ce53 292 if (strcmp($layout[0], '0') === 0) {
fa583f5f 293 $count++;
294 }
1332ce53 295 $count += substr_count($layout, ',0');
fa583f5f 296 }
297 return $count;
298}
299/**
300 * Returns the number of questions in the quiz layout
301 *
1332ce53 302 * @param string $layout the string representing the quiz layout.
303 * @return integer The number of questions in the quiz.
fa583f5f 304 */
305function quiz_number_of_questions_in_quiz($layout) {
78634f1e 306 $layout = quiz_questions_in_quiz(quiz_clean_layout($layout));
1332ce53 307 $count = substr_count($layout, ',');
308 if ($layout !== '') {
fa583f5f 309 $count++;
310 }
311 return $count;
ee1fb969 312}
76cacec8 313
ee1fb969 314/**
26d12a0c 315 * Returns the first question number for the current quiz page
316 *
26d12a0c 317 * @param string $quizlayout The string representing the layout for the whole quiz
318 * @param string $pagelayout The string representing the layout for the current page
1332ce53 319 * @return integer the number of the first question
26d12a0c 320 */
ee1fb969 321function quiz_first_questionnumber($quizlayout, $pagelayout) {
322 // this works by finding all the questions from the quizlayout that
323 // come before the current page and then adding up their lengths.
9cf4a18b 324 global $CFG, $DB;
49347eac 325 $start = strpos($quizlayout, ','.$pagelayout.',')-2;
ee1fb969 326 if ($start > 0) {
327 $prevlist = substr($quizlayout, 0, $start);
9cf4a18b 328 list($usql, $params) = $DB->get_in_or_equal(explode(',', $prevlist));
329 return $DB->get_field_sql("SELECT sum(length)+1 FROM {question}
330 WHERE id $usql", $params);
76cacec8 331 } else {
ee1fb969 332 return 1;
76cacec8 333 }
ee1fb969 334}
76cacec8 335
ee1fb969 336/**
26d12a0c 337 * Re-paginates the quiz layout
338 *
26d12a0c 339 * @param string $layout The string representing the quiz layout.
340 * @param integer $perpage The number of questions per page
341 * @param boolean $shuffle Should the questions be reordered randomly?
1332ce53 342 * @return string the new layout string
26d12a0c 343 */
4299df1d 344function quiz_repaginate($layout, $perpage, $shuffle = false) {
ee1fb969 345 $layout = str_replace(',0', '', $layout); // remove existing page breaks
346 $questions = explode(',', $layout);
fa583f5f 347 //remove empty pages from beginning
348 while (reset($questions) == '0') {
349 array_shift($questions);
350 }
ee1fb969 351 if ($shuffle) {
ee1fb969 352 shuffle($questions);
353 }
354 $i = 1;
355 $layout = '';
356 foreach ($questions as $question) {
357 if ($perpage and $i > $perpage) {
358 $layout .= '0,';
359 $i = 1;
76cacec8 360 }
ee1fb969 361 $layout .= $question.',';
362 $i++;
76cacec8 363 }
ee1fb969 364 return $layout.'0';
365}
76cacec8 366
26d12a0c 367/// Functions to do with quiz grades //////////////////////////////////////////
76cacec8 368
ee1fb969 369/**
26d12a0c 370 * Creates an array of maximum grades for a quiz
26d12a0c 371 * The grades are extracted from the quiz_question_instances table.
83192608 372 *
26d12a0c 373 * @param integer $quiz The quiz object
4e6f553d 374 * @return array Array of grades indexed by question id. These are the maximum
375 * possible grades that students can achieve for each of the questions.
26d12a0c 376 */
ee1fb969 377function quiz_get_all_question_grades($quiz) {
9cf4a18b 378 global $CFG, $DB;
76cacec8 379
ee1fb969 380 $questionlist = quiz_questions_in_quiz($quiz->questions);
76cacec8 381 if (empty($questionlist)) {
382 return array();
383 }
384
9cf4a18b 385 $params = array($quiz->id);
386 $wheresql = '';
387 if (!is_null($questionlist)) {
388 list($usql, $question_params) = $DB->get_in_or_equal(explode(',', $questionlist));
389 $wheresql = " AND question $usql ";
390 $params = array_merge($params, $question_params);
391 }
392
393 $instances = $DB->get_records_sql("SELECT question,grade,id
394 FROM {quiz_question_instances}
395 WHERE quiz = ? $wheresql", $params);
76cacec8 396
397 $list = explode(",", $questionlist);
398 $grades = array();
399
400 foreach ($list as $qid) {
ee1fb969 401 if (isset($instances[$qid])) {
402 $grades[$qid] = $instances[$qid]->grade;
76cacec8 403 } else {
404 $grades[$qid] = 1;
405 }
406 }
407 return $grades;
408}
409
4e6f553d 410/**
411 * Update the sumgrades field of the quiz. This needs to be called whenever
412 * the grading structure of the quiz is changed. For example if a question is
413 * added or removed, or a question weight is changed.
414 *
415 * @param object $quiz a quiz.
416 */
417function quiz_update_sumgrades($quiz) {
418 global $DB;
419 $grades = quiz_get_all_question_grades($quiz);
420 $sumgrades = 0;
421 foreach ($grades as $grade) {
422 $sumgrades += $grade;
423 }
424 if (!isset($quiz->sumgrades) || $quiz->sumgrades != $sumgrades) {
425 $DB->set_field('quiz', 'sumgrades', $sumgrades, array('id' => $quiz->id));
426 $quiz->sumgrades = $sumgrades;
427 }
428}
76cacec8 429
212b7b8c 430/**
431 * Convert the raw grade stored in $attempt into a grade out of the maximum
432 * grade for this quiz.
77ed3ba5 433 *
212b7b8c 434 * @param float $rawgrade the unadjusted grade, fof example $attempt->sumgrades
84e628a0 435 * @param object $quiz the quiz object. Only the fields grade, sumgrades, decimalpoints and questiondecimalpoints are used.
436 * @param mixed $round false = don't round, true = round using quiz_format_grade, 'question' = round using quiz_format_question_grade.
212b7b8c 437 * @return float the rescaled grade.
438 */
b7b4f8bd 439function quiz_rescale_grade($rawgrade, $quiz, $round = true) {
c8b2ab1b 440 if ($quiz->sumgrades != 0) {
b7b4f8bd 441 $grade = $rawgrade * $quiz->grade / $quiz->sumgrades;
84e628a0 442 if ($round === 'question') { // === really necessary here true == 'question' is true in PHP!
443 $grade = quiz_format_question_grade($quiz, $grade);
444 } else if ($round) {
f88fb62c 445 $grade = quiz_format_grade($quiz, $grade);
b7b4f8bd 446 }
212b7b8c 447 } else {
b7b4f8bd 448 $grade = 0;
212b7b8c 449 }
b7b4f8bd 450 return $grade;
76cacec8 451}
452
ee1fb969 453/**
212b7b8c 454 * Get the feedback text that should be show to a student who
de266537 455 * got this grade on this quiz. The feedback is processed ready for diplay.
77ed3ba5 456 *
212b7b8c 457 * @param float $grade a grade on this quiz.
458 * @param integer $quizid the id of the quiz object.
459 * @return string the comment that corresponds to this grade (empty string if there is not one.
460 */
fe6ce234 461function quiz_feedback_for_grade($grade, $quiz, $context, $cm=null) {
9cf4a18b 462 global $DB;
212b7b8c 463
fe6ce234
DC
464 $feedback = $DB->get_record_select('quiz_feedback', "quizid = ? AND mingrade <= ? AND $grade < maxgrade", array($quiz->id, $grade));
465
466 if (empty($feedback->feedbacktext)) {
467 $feedback->feedbacktext = '';
212b7b8c 468 }
77ed3ba5 469
de266537 470 // Clean the text, ready for display.
471 $formatoptions = new stdClass;
472 $formatoptions->noclean = true;
fe6ce234
DC
473 $feedbacktext = file_rewrite_pluginfile_urls($feedback->feedbacktext, 'pluginfile.php', $context->id, 'mod_quiz', 'feedback', $feedback->id);
474 $feedbacktext = format_text($feedbacktext, $feedback->feedbacktextformat, $formatoptions);
de266537 475
fe6ce234 476 return $feedbacktext;
212b7b8c 477}
478
479/**
8ce3e17f 480 * @param object $quiz the quiz database row.
212b7b8c 481 * @return boolean Whether this quiz has any non-blank feedback text.
482 */
739b0711 483function quiz_has_feedback($quiz) {
9cf4a18b 484 global $DB;
212b7b8c 485 static $cache = array();
739b0711 486 if (!array_key_exists($quiz->id, $cache)) {
487 $cache[$quiz->id] = quiz_has_grades($quiz) &&
488 $DB->record_exists_select('quiz_feedback', "quizid = ? AND " .
489 $DB->sql_isnotempty('quiz_feedback', 'feedbacktext', false, true),
490 array($quiz->id));
212b7b8c 491 }
739b0711 492 return $cache[$quiz->id];
212b7b8c 493}
494
495/**
77ed3ba5 496 * The quiz grade is the score that student's results are marked out of. When it
212b7b8c 497 * changes, the corresponding data in quiz_grades and quiz_feedback needs to be
498 * rescaled.
77ed3ba5 499 *
212b7b8c 500 * @param float $newgrade the new maximum grade for the quiz.
501 * @param object $quiz the quiz we are updating. Passed by reference so its grade field can be updated too.
d5a8d9aa 502 * @return boolean indicating success or failure. TODO: MDL-20625
212b7b8c 503 */
504function quiz_set_grade($newgrade, &$quiz) {
9cf4a18b 505 global $DB;
212b7b8c 506 // This is potentially expensive, so only do it if necessary.
507 if (abs($quiz->grade - $newgrade) < 1e-7) {
508 // Nothing to do.
509 return true;
510 }
b22fc901 511
212b7b8c 512 // Use a transaction, so that on those databases that support it, this is safer.
d5a8d9aa
PS
513 $transaction = $DB->start_delegated_transaction();
514
515 try {
516 // Update the quiz table.
517 $DB->set_field('quiz', 'grade', $newgrade, array('id' => $quiz->instance));
518
519 // Rescaling the other data is only possible if the old grade was non-zero.
520 if ($quiz->grade > 1e-7) {
521 global $CFG;
522
523 $factor = $newgrade/$quiz->grade;
524 $quiz->grade = $newgrade;
525
526 // Update the quiz_grades table.
527 $timemodified = time();
528 $DB->execute("
529 UPDATE {quiz_grades}
530 SET grade = ? * grade, timemodified = ?
531 WHERE quiz = ?
532 ", array($factor, $timemodified, $quiz->id));
533
534 // Update the quiz_feedback table.
535 $DB->execute("
536 UPDATE {quiz_feedback}
537 SET mingrade = ? * mingrade, maxgrade = ? * maxgrade
538 WHERE quizid = ?
539 ", array($factor, $factor, $quiz->id));
540 }
77ed3ba5 541
d5a8d9aa
PS
542 // update grade item and send all grades to gradebook
543 quiz_grade_item_update($quiz);
544 quiz_update_grades($quiz);
d6dd2108 545
d5a8d9aa
PS
546 $transaction->allow_commit();
547 return true;
548
549 } catch (Exception $e) {
550 //TODO: MDL-20625 this part was returning false, but now throws exception
551 $transaction->rollback($e);
212b7b8c 552 }
553}
554
555/**
556 * Save the overall grade for a user at a quiz in the quiz_grades table
557 *
558 * @param object $quiz The quiz for which the best grade is to be calculated and then saved.
559 * @param integer $userid The userid to calculate the grade for. Defaults to the current user.
98f38217 560 * @param array $attempts The attempts of this user. Useful if you are
561 * looping through many users. Attempts can be fetched in one master query to
562 * avoid repeated querying.
212b7b8c 563 * @return boolean Indicates success or failure.
564 */
98f38217 565function quiz_save_best_grade($quiz, $userid = null, $attempts = array()) {
9cf4a18b 566 global $DB;
825116fb 567 global $USER, $OUTPUT;
ee1fb969 568
212b7b8c 569 if (empty($userid)) {
ee1fb969 570 $userid = $USER->id;
571 }
76cacec8 572
98f38217 573 if (!$attempts){
574 // Get all the attempts made by the user
575 if (!$attempts = quiz_get_user_attempts($quiz->id, $userid)) {
825116fb 576 echo $OUTPUT->notification('Could not find any user attempts');
98f38217 577 return false;
578 }
76cacec8 579 }
580
ee1fb969 581 // Calculate the best grade
76cacec8 582 $bestgrade = quiz_calculate_best_grade($quiz, $attempts);
84e628a0 583 $bestgrade = quiz_rescale_grade($bestgrade, $quiz, false);
77ed3ba5 584
ee1fb969 585 // Save the best grade in the database
9cf4a18b 586 if ($grade = $DB->get_record('quiz_grades', array('quiz' => $quiz->id, 'userid' => $userid))) {
fc44ee0d 587 $grade->grade = $bestgrade;
76cacec8 588 $grade->timemodified = time();
a8c31db2 589 $DB->update_record('quiz_grades', $grade);
76cacec8 590 } else {
591 $grade->quiz = $quiz->id;
592 $grade->userid = $userid;
fc44ee0d 593 $grade->grade = $bestgrade;
76cacec8 594 $grade->timemodified = time();
a8c31db2 595 $DB->insert_record('quiz_grades', $grade);
76cacec8 596 }
d6dd2108 597
598 quiz_update_grades($quiz, $userid);
76cacec8 599 return true;
600}
601
fc44ee0d 602/**
26d12a0c 603 * Calculate the overall grade for a quiz given a number of attempts by a particular user.
604 *
605 * @return float The overall grade
606 * @param object $quiz The quiz for which the best grade is to be calculated
607 * @param array $attempts An array of all the attempts of the user at the quiz
608 */
76cacec8 609function quiz_calculate_best_grade($quiz, $attempts) {
76cacec8 610
611 switch ($quiz->grademethod) {
612
ee1fb969 613 case QUIZ_ATTEMPTFIRST:
76cacec8 614 foreach ($attempts as $attempt) {
615 return $attempt->sumgrades;
616 }
617 break;
618
ee1fb969 619 case QUIZ_ATTEMPTLAST:
76cacec8 620 foreach ($attempts as $attempt) {
621 $final = $attempt->sumgrades;
622 }
623 return $final;
624
ee1fb969 625 case QUIZ_GRADEAVERAGE:
76cacec8 626 $sum = 0;
627 $count = 0;
628 foreach ($attempts as $attempt) {
629 $sum += $attempt->sumgrades;
630 $count++;
631 }
632 return (float)$sum/$count;
633
634 default:
ee1fb969 635 case QUIZ_GRADEHIGHEST:
76cacec8 636 $max = 0;
637 foreach ($attempts as $attempt) {
638 if ($attempt->sumgrades > $max) {
639 $max = $attempt->sumgrades;
640 }
641 }
642 return $max;
643 }
644}
645
fc44ee0d 646/**
26d12a0c 647 * Return the attempt with the best grade for a quiz
648 *
649 * Which attempt is the best depends on $quiz->grademethod. If the grade
650 * method is GRADEAVERAGE then this function simply returns the last attempt.
651 * @return object The attempt with the best grade
652 * @param object $quiz The quiz for which the best grade is to be calculated
653 * @param array $attempts An array of all the attempts of the user at the quiz
654 */
76cacec8 655function quiz_calculate_best_attempt($quiz, $attempts) {
76cacec8 656
657 switch ($quiz->grademethod) {
658
ee1fb969 659 case QUIZ_ATTEMPTFIRST:
76cacec8 660 foreach ($attempts as $attempt) {
661 return $attempt;
662 }
663 break;
664
ee1fb969 665 case QUIZ_GRADEAVERAGE: // need to do something with it :-)
666 case QUIZ_ATTEMPTLAST:
76cacec8 667 foreach ($attempts as $attempt) {
668 $final = $attempt;
669 }
670 return $final;
671
672 default:
ee1fb969 673 case QUIZ_GRADEHIGHEST:
76cacec8 674 $max = -1;
675 foreach ($attempts as $attempt) {
676 if ($attempt->sumgrades > $max) {
677 $max = $attempt->sumgrades;
678 $maxattempt = $attempt;
679 }
680 }
681 return $maxattempt;
682 }
683}
684
0675e5eb 685/**
686 * @param int $option one of the values QUIZ_GRADEHIGHEST, QUIZ_GRADEAVERAGE, QUIZ_ATTEMPTFIRST or QUIZ_ATTEMPTLAST.
687 * @return the lang string for that option.
688 */
689function quiz_get_grading_option_name($option) {
690 $strings = quiz_get_grading_options();
691 return $strings[$option];
692}
693
26d12a0c 694/// Other quiz functions ////////////////////////////////////////////////////
ee1fb969 695
ee1fb969 696/**
26d12a0c 697 * Parse field names used for the replace options on question edit forms
698 */
c6bfdec3 699function quiz_parse_fieldname($name, $nameprefix='question') {
700 $reg = array();
9274189f 701 if (preg_match("/$nameprefix(\\d+)(\w+)/", $name, $reg)) {
c6bfdec3 702 return array('mode' => $reg[2], 'id' => (int)$reg[1]);
703 } else {
704 return false;
705 }
706}
0ae98223 707
ee1fb969 708/**
26d12a0c 709 * Upgrade states for an attempt to Moodle 1.5 model
710 *
711 * Any state that does not yet have its timestamp set to nonzero has not yet been upgraded from Moodle 1.4
712 * The reason these are still around is that for large sites it would have taken too long to
713 * upgrade all states at once. This function sets the timestamp field and creates an entry in the
714 * question_sessions table.
715 * @param object $attempt The attempt whose states need upgrading
716 */
ee1fb969 717function quiz_upgrade_states($attempt) {
9cf4a18b 718 global $DB;
ee1fb969 719 global $CFG;
1f48479e 720 // The old quiz model only allowed a single response per quiz attempt so that there will be
721 // only one state record per question for this attempt.
722
723 // We set the timestamp of all states to the timemodified field of the attempt.
9cf4a18b 724 $DB->execute("UPDATE {question_states} SET timestamp = ? WHERE attempt = ?", array($attempt->timemodified, $attempt->uniqueid));
1f48479e 725
03d1753c 726 // For each state we create an entry in the question_sessions table, with both newest and
1f48479e 727 // newgraded pointing to this state.
728 // Actually we only do this for states whose question is actually listed in $attempt->layout.
729 // We do not do it for states associated to wrapped questions like for example the questions
730 // used by a RANDOM question
7c4b621a 731 $session = new stdClass;
732 $session->attemptid = $attempt->uniqueid;
1f48479e 733 $questionlist = quiz_questions_in_quiz($attempt->layout);
9cf4a18b 734 $params = array($attempt->uniqueid);
735 list($usql, $question_params) = $DB->get_in_or_equal(explode(',',$questionlist));
736 $params = array_merge($params, $question_params);
737
738 if ($questionlist and $states = $DB->get_records_select('question_states', "attempt = ? AND question $usql", $params)) {
ee1fb969 739 foreach ($states as $state) {
03d1753c 740 $session->newgraded = $state->id;
741 $session->newest = $state->id;
742 $session->questionid = $state->question;
9cf4a18b 743 $DB->insert_record('question_sessions', $session, false);
ee1fb969 744 }
745 }
746}
2a874d65 747
26a29b1a 748/**
2a874d65 749 * @param object $quiz the quiz.
750 * @param integer $cmid the course_module object for this quiz.
751 * @param object $question the question.
26a29b1a 752 * @param string $returnurl url to return to after action is done.
753 * @return string html for a number of icons linked to action pages for a
754 * question - preview and edit / view icons depending on user capabilities.
755 */
2a874d65 756function quiz_question_action_icons($quiz, $cmid, $question, $returnurl) {
757 $html = quiz_question_preview_button($quiz, $question) . ' ' .
758 quiz_question_edit_button($cmid, $question, $returnurl);
759 return $html;
760}
761
762/**
763 * @param integer $cmid the course_module.id for this quiz.
764 * @param object $question the question.
765 * @param string $returnurl url to return to after action is done.
766 * @param string $contentbeforeicon some HTML content to be added inside the link, before the icon.
767 * @return the HTML for an edit icon, view icon, or nothing for a question (depending on permissions).
768 */
d04f623b 769function quiz_question_edit_button($cmid, $question, $returnurl, $contentaftericon = '') {
d436d197 770 global $CFG, $OUTPUT;
2a874d65 771
772 // Minor efficiency saving. Only get strings once, even if there are a lot of icons on one page.
26a29b1a 773 static $stredit = null;
774 static $strview = null;
775 if ($stredit === null){
776 $stredit = get_string('edit');
777 $strview = get_string('view');
778 }
2a874d65 779
780 // What sort of icon should we show?
781 $action = '';
782 if (question_has_capability_on($question, 'edit', $question->category) ||
783 question_has_capability_on($question, 'move', $question->category)) {
784 $action = $stredit;
785 $icon = '/t/edit';
786 } else if (question_has_capability_on($question, 'view', $question->category)) {
787 $action = $strview;
788 $icon = '/i/info';
789 }
790
791 // Build the icon.
792 if ($action) {
793 $questionparams = array('returnurl' => $returnurl, 'cmid' => $cmid, 'id' => $question->id);
794 $questionurl = new moodle_url("$CFG->wwwroot/question/question.php", $questionparams);
d04f623b
TH
795 return '<a title="' . $action . '" href="' . $questionurl->out() . '"><img src="' .
796 $OUTPUT->pix_url($icon) . '" alt="' . $action . '" />' . $contentaftericon .
797 '</a>';
2a874d65 798 } else {
d04f623b 799 return $contentaftericon;
26a29b1a 800 }
26a29b1a 801}
802
7d87171b 803/**
804 * @param object $quiz the quiz
805 * @param object $question the question
414d5bfb 806 * @param boolean $label if true, show the previewquestion label after the icon
7d87171b 807 * @return the HTML for a preview question icon.
808 */
2a874d65 809function quiz_question_preview_button($quiz, $question, $label = false) {
4096752d 810 global $CFG, $COURSE, $OUTPUT;
2a874d65 811 if (!question_has_capability_on($question, 'use', $question->category)) {
271e6dec 812 return '';
813 }
2a874d65 814
815 // Minor efficiency saving. Only get strings once, even if there are a lot of icons on one page.
816 static $strpreview = null;
817 static $strpreviewquestion = null;
818 if ($strpreview === null){
819 $strpreview = get_string('preview', 'quiz');
820 $strpreviewquestion = get_string('previewquestion', 'quiz');
821 }
822
823 // Do we want a label?
fa583f5f 824 $strpreviewlabel="";
2a874d65 825 if ($label) {
826 $strpreviewlabel = $strpreview;
fa583f5f 827 }
2a874d65 828
829 // Build the icon.
000c278c 830 $image = $OUTPUT->pix_icon('t/preview', $strpreviewquestion);
ca4b4f8f 831
9bf16314 832 $link = new moodle_url($CFG->wwwroot."/question/preview.php?id=$question->id&quizid=$quiz->id");
ca4b4f8f 833 parse_str(QUESTION_PREVIEW_POPUP_OPTIONS, $options);
9bf16314 834 $action = new popup_action('click', $link, 'questionpreview', $options);
4bcc5118 835
9bf16314 836 return $OUTPUT->action_link($link, $image, $action, array('title' => $strpreviewquestion));
ad7e7ba8 837}
0d156caa 838
62e76c67 839/**
840 * @param object $attempt the attempt.
841 * @param object $context the quiz context.
842 * @return integer whether flags should be shown/editable to the current user for this attempt.
843 */
844function quiz_get_flag_option($attempt, $context) {
845 global $USER;
846 static $flagmode = null;
847 if (is_null($flagmode)) {
848 if (!has_capability('moodle/question:flag', $context)) {
849 $flagmode = QUESTION_FLAGSHIDDEN;
850 } else if ($attempt->userid == $USER->id) {
851 $flagmode = QUESTION_FLAGSEDITABLE;
852 } else {
853 $flagmode = QUESTION_FLAGSSHOWN;
854 }
855 }
856 return $flagmode;
857}
858
4f48fb42 859/**
4dca7e51 860 * Determine render options
861 *
862 * @param int $reviewoptions
863 * @param object $state
864 */
62e76c67 865function quiz_get_renderoptions($quiz, $attempt, $context, $state) {
866 $reviewoptions = $quiz->review;
1b8a7434 867 $options = new stdClass;
77ed3ba5 868
62e76c67 869 $options->flags = quiz_get_flag_option($attempt, $context);
870
4f48fb42 871 // Show the question in readonly (review) mode if the question is in
872 // the closed state
4dca7e51 873 $options->readonly = question_state_is_closed($state);
4f48fb42 874
875 // Show feedback once the question has been graded (if allowed by the quiz)
4dca7e51 876 $options->feedback = question_state_is_graded($state) && ($reviewoptions & QUIZ_REVIEW_FEEDBACK & QUIZ_REVIEW_IMMEDIATELY);
4f48fb42 877
4f48fb42 878 // Show correct responses in readonly mode if the quiz allows it
4dca7e51 879 $options->correct_responses = $options->readonly && ($reviewoptions & QUIZ_REVIEW_ANSWERS & QUIZ_REVIEW_IMMEDIATELY);
4f48fb42 880
a4514d91 881 // Show general feedback if the question has been graded and the quiz allows it.
882 $options->generalfeedback = question_state_is_graded($state) && ($reviewoptions & QUIZ_REVIEW_GENERALFEEDBACK & QUIZ_REVIEW_IMMEDIATELY);
1b8a7434 883
77ed3ba5 884 // Show overallfeedback once the attempt is over.
885 $options->overallfeedback = false;
886
4f48fb42 887 // Always show responses and scores
888 $options->responses = true;
889 $options->scores = true;
a4faed69 890 $options->quizstate = QUIZ_STATE_DURING;
1aab3561 891 $options->history = false;
4f48fb42 892
893 return $options;
894}
895
4f48fb42 896/**
cd06115f 897 * Determine review options
77ed3ba5 898 *
cd06115f 899 * @param object $quiz the quiz instance.
900 * @param object $attempt the attempt in question.
62e76c67 901 * @param $context the quiz module context.
77ed3ba5 902 *
cd06115f 903 * @return object an object with boolean fields responses, scores, feedback,
a4514d91 904 * correct_responses, solutions and general feedback
cd06115f 905 */
62e76c67 906function quiz_get_reviewoptions($quiz, $attempt, $context) {
907 global $USER;
908
1b8a7434 909 $options = new stdClass;
4f48fb42 910 $options->readonly = true;
e2833e87 911
62e76c67 912 $options->flags = quiz_get_flag_option($attempt, $context);
913
b6e907a2 914 // Provide the links to the question review and comment script
b55797b8 915 if (!empty($attempt->id)) {
916 $options->questionreviewlink = '/mod/quiz/reviewquestion.php?attempt=' . $attempt->id;
917 }
b6e907a2 918
e2833e87 919 // Show a link to the comment box only for closed attempts
62e76c67 920 if ($attempt->timefinish && has_capability('mod/quiz:grade', $context)) {
3fa5724a 921 $options->questioncommentlink = new moodle_url('/mod/quiz/comment.php', array('attempt' => $attempt->id));
01600b51 922 }
923
1aab3561 924 // Whether to display a response history.
62e76c67 925 $canviewreports = has_capability('mod/quiz:viewreports', $context);
1aab3561 926 $options->history = ($canviewreports && !$attempt->preview) ? 'all' : 'graded';
927
928 if ($canviewreports && has_capability('moodle/grade:viewhidden', $context) && !$attempt->preview) {
e2833e87 929 // People who can see reports and hidden grades should be shown everything,
930 // except during preview when teachers want to see what students see.
4f48fb42 931 $options->responses = true;
c52c62d1 932 $options->scores = true;
4f48fb42 933 $options->feedback = true;
934 $options->correct_responses = true;
935 $options->solutions = false;
a4514d91 936 $options->generalfeedback = true;
77ed3ba5 937 $options->overallfeedback = true;
a4faed69 938 $options->quizstate = QUIZ_STATE_TEACHERACCESS;
4f48fb42 939 } else {
e2833e87 940 // Work out the state of the attempt ...
941 if (((time() - $attempt->timefinish) < 120) || $attempt->timefinish==0) {
942 $quiz_state_mask = QUIZ_REVIEW_IMMEDIATELY;
943 $options->quizstate = QUIZ_STATE_IMMEDIATELY;
944 } else if (!$quiz->timeclose or time() < $quiz->timeclose) {
945 $quiz_state_mask = QUIZ_REVIEW_OPEN;
946 $options->quizstate = QUIZ_STATE_OPEN;
947 } else {
948 $quiz_state_mask = QUIZ_REVIEW_CLOSED;
949 $options->quizstate = QUIZ_STATE_CLOSED;
950 }
951
c52c62d1 952 // ... and hence extract the appropriate review options.
1b8a7434 953 $options->responses = ($quiz->review & $quiz_state_mask & QUIZ_REVIEW_RESPONSES) ? 1 : 0;
954 $options->scores = ($quiz->review & $quiz_state_mask & QUIZ_REVIEW_SCORES) ? 1 : 0;
955 $options->feedback = ($quiz->review & $quiz_state_mask & QUIZ_REVIEW_FEEDBACK) ? 1 : 0;
956 $options->correct_responses = ($quiz->review & $quiz_state_mask & QUIZ_REVIEW_ANSWERS) ? 1 : 0;
957 $options->solutions = ($quiz->review & $quiz_state_mask & QUIZ_REVIEW_SOLUTIONS) ? 1 : 0;
a4514d91 958 $options->generalfeedback = ($quiz->review & $quiz_state_mask & QUIZ_REVIEW_GENERALFEEDBACK) ? 1 : 0;
00719c02 959 $options->overallfeedback = $attempt->timefinish && ($quiz->review & $quiz_state_mask & QUIZ_REVIEW_OVERALLFEEDBACK);
4f48fb42 960 }
77ed3ba5 961
4f48fb42 962 return $options;
963}
77ed3ba5 964
40377b18 965/**
77ed3ba5 966 * Combines the review options from a number of different quiz attempts.
967 * Returns an array of two ojects, so he suggested way of calling this
968 * funciton is:
969 * list($someoptions, $alloptions) = quiz_get_combined_reviewoptions(...)
970 *
971 * @param object $quiz the quiz instance.
972 * @param array $attempts an array of attempt objects.
973 * @param $context the roles and permissions context,
974 * normally the context for the quiz module instance.
975 *
976 * @return array of two options objects, one showing which options are true for
977 * at least one of the attempts, the other showing which options are true
978 * for all attempts.
979 */
62e76c67 980function quiz_get_combined_reviewoptions($quiz, $attempts, $context) {
77ed3ba5 981 $fields = array('readonly', 'scores', 'feedback', 'correct_responses', 'solutions', 'generalfeedback', 'overallfeedback');
982 $someoptions = new stdClass;
983 $alloptions = new stdClass;
984 foreach ($fields as $field) {
985 $someoptions->$field = false;
986 $alloptions->$field = true;
40377b18 987 }
77ed3ba5 988 foreach ($attempts as $attempt) {
989 $attemptoptions = quiz_get_reviewoptions($quiz, $attempt, $context);
7d014bf5 990 foreach ($fields as $field) {
991 $someoptions->$field = $someoptions->$field || $attemptoptions->$field;
992 $alloptions->$field = $alloptions->$field && $attemptoptions->$field;
993 }
77ed3ba5 994 }
995 return array($someoptions, $alloptions);
40377b18 996}
ac48e43a 997
998/// FUNCTIONS FOR SENDING NOTIFICATION EMAILS ///////////////////////////////
999
1000/**
1001 * Sends confirmation email to the student taking the course
1002 *
1003 * @param stdClass $a associative array of replaceable fields for the templates
1004 *
2044a2b2 1005 * @return bool|string result of events_triger
ac48e43a 1006 */
1007function quiz_send_confirmation($a) {
1008
1009 global $USER;
1010
1011 // recipient is self
1012 $a->useridnumber = $USER->idnumber;
1013 $a->username = fullname($USER);
1014 $a->userusername = $USER->username;
1015
1016 // fetch the subject and body from strings
1017 $subject = get_string('emailconfirmsubject', 'quiz', $a);
1018 $body = get_string('emailconfirmbody', 'quiz', $a);
1019
1020 // send email and analyse result
39790bd8 1021 $eventdata = new stdClass();
94263cca
AD
1022 $eventdata->component = 'mod_quiz';
1023 $eventdata->name = 'confirmation';
1024 $eventdata->notification = 1;
1025
3b120e46 1026 $eventdata->userfrom = get_admin();
1027 $eventdata->userto = $USER;
1028 $eventdata->subject = $subject;
1029 $eventdata->fullmessage = $body;
1030 $eventdata->fullmessageformat = FORMAT_PLAIN;
1031 $eventdata->fullmessagehtml = '';
94263cca
AD
1032
1033 $eventdata->smallmessage = get_string('emailconfirmsmall', 'quiz', $a);
1034 $eventdata->contexturl = $a->quizurl;
1035 $eventdata->contexturlname = $a->quizname;
99d19c13 1036
7c7d3afa 1037 return message_send($eventdata);
ac48e43a 1038}
1039
1040/**
bc68fc9a 1041 * Sends notification messages to the interested parties that assign the role capability
ac48e43a 1042 *
1043 * @param object $recipient user object of the intended recipient
1044 * @param stdClass $a associative array of replaceable fields for the templates
1045 *
2044a2b2 1046 * @return bool|string result of events_triger()
ac48e43a 1047 */
1048function quiz_send_notification($recipient, $a) {
1049
1050 global $USER;
1051
1052 // recipient info for template
1053 $a->username = fullname($recipient);
1054 $a->userusername = $recipient->username;
bc68fc9a 1055 //$a->userusername = $recipient->username;
ac48e43a 1056
1057 // fetch the subject and body from strings
1058 $subject = get_string('emailnotifysubject', 'quiz', $a);
1059 $body = get_string('emailnotifybody', 'quiz', $a);
1060
1061 // send email and analyse result
39790bd8 1062 $eventdata = new stdClass();
bc68fc9a
AD
1063 $eventdata->component = 'mod_quiz';
1064 $eventdata->name = 'submission';
1065 $eventdata->notification = 1;
1066
3b120e46 1067 $eventdata->userfrom = $USER;
1068 $eventdata->userto = $recipient;
1069 $eventdata->subject = $subject;
1070 $eventdata->fullmessage = $body;
1071 $eventdata->fullmessageformat = FORMAT_PLAIN;
1072 $eventdata->fullmessagehtml = '';
bc68fc9a
AD
1073
1074 $eventdata->smallmessage = get_string('emailnotifysmall', 'quiz', $a);
1075 $eventdata->contexturl = $a->quizreviewurl;
1076 $eventdata->contexturlname = $a->quizname;
1077
7c7d3afa 1078 return message_send($eventdata);
ac48e43a 1079}
1080
1081/**
1082 * Takes a bunch of information to format into an email and send
1083 * to the specified recipient.
1084 *
1085 * @param object $course the course
1086 * @param object $quiz the quiz
1087 * @param object $attempt this attempt just finished
1088 * @param object $context the quiz context
1089 * @param object $cm the coursemodule for this quiz
1090 *
1091 * @return int number of emails sent
1092 */
1093function quiz_send_notification_emails($course, $quiz, $attempt, $context, $cm) {
1094 global $CFG, $USER;
1095 // we will count goods and bads for error logging
46505ee7 1096 $emailresult = array('good' => 0, 'fail' => 0);
ac48e43a 1097
1098 // do nothing if required objects not present
1099 if (empty($course) or empty($quiz) or empty($attempt) or empty($context)) {
1100 debugging('quiz_send_notification_emails: Email(s) not sent due to program error.',
1101 DEBUG_DEVELOPER);
1102 return $emailresult['fail'];
1103 }
1104
1105 // check for confirmation required
1106 $sendconfirm = false;
1107 $notifyexcludeusers = '';
1108 if (has_capability('mod/quiz:emailconfirmsubmission', $context, NULL, false)) {
1109 // exclude from notify emails later
1110 $notifyexcludeusers = $USER->id;
1111 // send the email
1112 $sendconfirm = true;
1113 }
1114
1115 // check for notifications required
46505ee7 1116 $notifyfields = 'u.id, u.username, u.firstname, u.lastname, u.email, u.lang, u.timezone, u.mailformat, u.maildisplay';
5a128471 1117 $groups = groups_get_all_groups($course->id, $USER->id);
1118 if (is_array($groups) && count($groups) > 0) {
1119 $groups = array_keys($groups);
255985f1 1120 } else if (groups_get_activity_groupmode($cm, $course) != NOGROUPS) {
1121 // If the user is not in a group, and the quiz is set to group mode,
1122 // then set $gropus to a non-existant id so that only users with
1123 // 'moodle/site:accessallgroups' get notified.
1124 $groups = -1;
5a128471 1125 } else {
1126 $groups = '';
1127 }
ac48e43a 1128 $userstonotify = get_users_by_capability($context, 'mod/quiz:emailnotifysubmission',
5a128471 1129 $notifyfields, '', '', '', $groups, $notifyexcludeusers, false, false, true);
ac48e43a 1130
1131 // if something to send, then build $a
1132 if (! empty($userstonotify) or $sendconfirm) {
1133 $a = new stdClass;
1134 // course info
1135 $a->coursename = $course->fullname;
1136 $a->courseshortname = $course->shortname;
1137 // quiz info
1138 $a->quizname = $quiz->name;
a49cb927 1139 $a->quizreporturl = $CFG->wwwroot . '/mod/quiz/report.php?id=' . $cm->id;
4f67bf8c 1140 $a->quizreportlink = '<a href="' . $a->quizreporturl . '">' . format_string($quiz->name) . ' report</a>';
ac48e43a 1141 $a->quizreviewurl = $CFG->wwwroot . '/mod/quiz/review.php?attempt=' . $attempt->id;
4f67bf8c 1142 $a->quizreviewlink = '<a href="' . $a->quizreviewurl . '">' . format_string($quiz->name) . ' review</a>';
a49cb927 1143 $a->quizurl = $CFG->wwwroot . '/mod/quiz/view.php?id=' . $cm->id;
4f67bf8c 1144 $a->quizlink = '<a href="' . $a->quizurl . '">' . format_string($quiz->name) . '</a>';
ac48e43a 1145 // attempt info
b0972c05 1146 $a->submissiontime = userdate($attempt->timefinish);
1147 $a->timetaken = format_time($attempt->timefinish - $attempt->timestart);
ac48e43a 1148 // student who sat the quiz info
1149 $a->studentidnumber = $USER->idnumber;
1150 $a->studentname = fullname($USER);
1151 $a->studentusername = $USER->username;
1152 }
1153
1154 // send confirmation if required
1155 if ($sendconfirm) {
1156 // send the email and update stats
1157 switch (quiz_send_confirmation($a)) {
1158 case true:
1159 $emailresult['good']++;
1160 break;
1161 case false:
1162 $emailresult['fail']++;
1163 break;
ac48e43a 1164 }
1165 }
1166
1167 // send notifications if required
1168 if (!empty($userstonotify)) {
1169 // loop through recipients and send an email to each and update stats
1170 foreach ($userstonotify as $recipient) {
1171 switch (quiz_send_notification($recipient, $a)) {
1172 case true:
1173 $emailresult['good']++;
1174 break;
1175 case false:
1176 $emailresult['fail']++;
1177 break;
ac48e43a 1178 }
1179 }
1180 }
1181
1182 // log errors sending emails if any
1183 if (! empty($emailresult['fail'])) {
1184 debugging('quiz_send_notification_emails:: '.$emailresult['fail'].' email(s) failed to be sent.', DEBUG_DEVELOPER);
1185 }
ac48e43a 1186
1187 // return the number of successfully sent emails
1188 return $emailresult['good'];
1189}
fa583f5f 1190
414d5bfb 1191/**
1192 * Clean the question layout from various possible anomalies:
1193 * - Remove consecutive ","'s
1194 * - Remove duplicate question id's
1195 * - Remove extra "," from beginning and end
1196 * - Finally, add a ",0" in the end if there is none
83192608 1197 *
414d5bfb 1198 * @param $string $layout the quiz layout to clean up, usually from $quiz->questions.
1199 * @param boolean $removeemptypages If true, remove empty pages from the quiz. False by default.
1200 * @return $string the cleaned-up layout
1201 */
edd5246b 1202function quiz_clean_layout($layout, $removeemptypages = false){
1203 // Remove duplicate "," (or triple, or...)
1204 $layout = preg_replace('/,{2,}/', ',', trim($layout, ','));
1205
1206 // Remove duplicate question ids
1207 $layout = explode(',', $layout);
1208 $cleanerlayout = array();
1209 $seen = array();
1210 foreach ($layout as $item) {
1211 if ($item == 0) {
1212 $cleanerlayout[] = '0';
1213 } else if (!in_array($item, $seen)) {
1214 $cleanerlayout[] = $item;
1215 $seen[] = $item;
1216 }
fa583f5f 1217 }
edd5246b 1218
1219 if ($removeemptypages) {
fa583f5f 1220 // Avoid duplicate page breaks
edd5246b 1221 $layout = $cleanerlayout;
1222 $cleanerlayout = array();
1223 $stripfollowingbreaks = true; // Ensure breaks are stripped from the start.
1224 foreach ($layout as $item) {
1225 if ($stripfollowingbreaks && $item == 0) {
1226 continue;
1227 }
1228 $cleanerlayout[] = $item;
1229 $stripfollowingbreaks = $item == 0;
fa583f5f 1230 }
1231 }
edd5246b 1232
1233 // Add a page break at the end if there is none
1234 if (end($cleanerlayout) !== '0') {
1235 $cleanerlayout[] = '0';
fa583f5f 1236 }
1237
edd5246b 1238 return implode(',', $cleanerlayout);
fa583f5f 1239}
c52c62d1 1240/**
1241 * Print a quiz error message. This is a thin wrapper around print_error, for convinience.
1242 *
1243 * @param mixed $quiz either the quiz object, or the interger quiz id.
1244 * @param string $errorcode the name of the string from quiz.php to print.
1245 * @param object $a any extra data required by the error string.
1246 */
1247function quiz_error($quiz, $errorcode, $a = null) {
1248 global $CFG;
1249 if (is_object($quiz)) {
1250 $quiz = $quiz->id;
1251 }
1252 print_error($errorcode, 'quiz', $CFG->wwwroot . '/mod/quiz/view.php?q=' . $quiz, $a);
1253}
7d4dfc48 1254
1255/**
1256 * Checks if browser is safe browser
83192608 1257 *
7d4dfc48 1258 * @return true, if browser is safe browser else false
1259*/
1260function quiz_check_safe_browser() {
1261 return strpos($_SERVER['HTTP_USER_AGENT'], "SEB") !== false;
1262}
ff065f96
TH
1263
1264function quiz_get_js_module() {
157434a5 1265 global $PAGE;
ff065f96
TH
1266 return array(
1267 'name' => 'mod_quiz',
1268 'fullpath' => '/mod/quiz/module.js',
157434a5 1269 'requires' => array('base', 'dom', 'event-delegate', 'event-key', 'core_question_engine'),
ff065f96
TH
1270 'strings' => array(
1271 array('timesup', 'quiz'),
1272 array('functiondisabledbysecuremode', 'quiz'),
a26246ea 1273 array('flagged', 'question'),
ff065f96
TH
1274 ),
1275 );
fe6ce234 1276}