MDL-10250 - Extend get_users_by_capability to have a $useviewallgroups parameter.
[moodle.git] / mod / quiz / locallib.php
CommitLineData
76cacec8 1<?php // $Id$
ee1fb969 2/**
26d12a0c 3 * Library of functions used by the quiz module.
4 *
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
17 */
76cacec8 18
ee1fb969 19/**
26d12a0c 20 * Include those library functions that are also used by core Moodle or other modules
21 */
22require_once($CFG->dirroot . '/mod/quiz/lib.php');
23require_once($CFG->dirroot . '/question/editlib.php');
76cacec8 24
26d12a0c 25/// Constants ///////////////////////////////////////////////////////////////////
76cacec8 26
ee1fb969 27/**#@+
26d12a0c 28 * Options determining how the grades from individual attempts are combined to give
29 * the overall grade for a user
30 */
ee1fb969 31define("QUIZ_GRADEHIGHEST", "1");
32define("QUIZ_GRADEAVERAGE", "2");
33define("QUIZ_ATTEMPTFIRST", "3");
34define("QUIZ_ATTEMPTLAST", "4");
26d12a0c 35$QUIZ_GRADE_METHOD = array(
36 QUIZ_GRADEHIGHEST => get_string("gradehighest", "quiz"),
37 QUIZ_GRADEAVERAGE => get_string("gradeaverage", "quiz"),
38 QUIZ_ATTEMPTFIRST => get_string("attemptfirst", "quiz"),
39 QUIZ_ATTEMPTLAST => get_string("attemptlast", "quiz")
40);
ee1fb969 41/**#@-*/
42
26d12a0c 43/// Functions related to attempts /////////////////////////////////////////
ee1fb969 44
45/**
26d12a0c 46 * Creates an object to represent a new attempt at a quiz
47 *
48 * Creates an attempt object to represent an attempt at the quiz by the current
49 * user starting at the current time. The ->id field is not set. The object is
50 * NOT written to the database.
51 * @return object The newly created attempt object.
52 * @param object $quiz The quiz to create an attempt for.
53 * @param integer $attemptnumber The sequence number for the attempt.
54 */
ee1fb969 55function quiz_create_attempt($quiz, $attemptnumber) {
d115d8c7 56 global $USER, $CFG;
ee1fb969 57
75e1df6f 58 if (!$attemptnumber > 1 or !$quiz->attemptonlast or !$attempt = get_record('quiz_attempts', 'quiz', $quiz->id, 'userid', $USER->id, 'attempt', $attemptnumber-1)) {
59 // we are not building on last attempt so create a new attempt
60 $attempt->quiz = $quiz->id;
61 $attempt->userid = $USER->id;
62 $attempt->preview = 0;
63 if ($quiz->shufflequestions) {
64 $attempt->layout = quiz_repaginate($quiz->questions, $quiz->questionsperpage, true);
65 } else {
66 $attempt->layout = $quiz->questions;
67 }
68 }
69
ee1fb969 70 $timenow = time();
ee1fb969 71 $attempt->attempt = $attemptnumber;
72 $attempt->sumgrades = 0.0;
ee1fb969 73 $attempt->timestart = $timenow;
74 $attempt->timefinish = 0;
75 $attempt->timemodified = $timenow;
4f48fb42 76 $attempt->uniqueid = question_new_attempt_uniqueid();
75e1df6f 77
ee1fb969 78 return $attempt;
79}
80
cd06115f 81/**
82 * Returns an unfinished attempt (if there is one) for the given
83 * user on the given quiz. This function does not return preview attempts.
77ed3ba5 84 *
cd06115f 85 * @param integer $quizid the id of the quiz.
86 * @param integer $userid the id of the user.
77ed3ba5 87 *
88 * @return mixed the unfinished attempt if there is one, false if not.
cd06115f 89 */
0d156caa 90function quiz_get_user_attempt_unfinished($quizid, $userid) {
77ed3ba5 91 $attempts = quiz_get_user_attempts($quizid, $userid, 'unfinished');
92 if ($attempts) {
93 return array_shift($attempts);
94 } else {
95 return false;
96 }
920fb237 97}
98
212b7b8c 99/**
100 * @param integer $quizid the quiz id.
101 * @param integer $userid the userid.
77ed3ba5 102 * @param string $status 'all', 'finished' or 'unfinished' to control
103 * @return an array of all the user's attempts at this quiz. Returns an empty array if there are none.
212b7b8c 104 */
77ed3ba5 105function quiz_get_user_attempts($quizid, $userid, $status = 'finished') {
106 $status_condition = array(
107 'all' => '',
108 'finished' => ' AND timefinish > 0',
109 'unfinished' => ' AND timefinish = 0'
110 );
111 if ($attempts = get_records_select('quiz_attempts',
112 "quiz = '$quizid' AND userid = '$userid' AND preview = 0" . $status_condition[$status],
113 'attempt ASC')) {
114 return $attempts;
212b7b8c 115 } else {
116 return array();
117 }
76cacec8 118}
119
ff51d646 120/**
121 * Delete a quiz attempt.
122 */
123function quiz_delete_attempt($attempt, $quiz) {
124 if (is_numeric($attempt)) {
125 if (!$attempt = get_record('quiz_attempts', 'id', $attempt)) {
126 return;
127 }
128 }
129
130 if ($attempt->quiz != $quiz->id) {
131 debugging("Trying to delete attempt $attempt->id which belongs to quiz $attempt->quiz " .
132 "but was passed quiz $quiz->id.");
133 return;
134 }
135
136 delete_records('quiz_attempts', 'id', $attempt->id);
137 delete_attempt($attempt->uniqueid);
138
139 // Search quiz_attempts for other instances by this user.
140 // If none, then delete record for this quiz, this user from quiz_grades
141 // else recalculate best grade
142
143 $userid = $attempt->userid;
144 if (!record_exists('quiz_attempts', 'userid', $userid, 'quiz', $quiz->id)) {
145 delete_records('quiz_grades', 'userid', $userid,'quiz', $quiz->id);
146 } else {
147 quiz_save_best_grade($quiz, $userid);
148 }
149}
150
26d12a0c 151/// Functions to do with quiz layout and pages ////////////////////////////////
0d156caa 152
ee1fb969 153/**
26d12a0c 154 * Returns a comma separated list of question ids for the current page
155 *
156 * @return string Comma separated list of question ids
157 * @param string $layout The string representing the quiz layout. Each page is represented as a
158 * comma separated list of question ids and 0 indicating page breaks.
159 * So 5,2,0,3,0 means questions 5 and 2 on page 1 and question 3 on page 2
160 * @param integer $page The number of the current page.
161 */
ee1fb969 162function quiz_questions_on_page($layout, $page) {
163 $pages = explode(',0', $layout);
164 return trim($pages[$page], ',');
76cacec8 165}
166
ee1fb969 167/**
26d12a0c 168 * Returns a comma separated list of question ids for the quiz
169 *
170 * @return string Comma separated list of question ids
171 * @param string $layout The string representing the quiz layout. Each page is represented as a
172 * comma separated list of question ids and 0 indicating page breaks.
173 * So 5,2,0,3,0 means questions 5 and 2 on page 1 and question 3 on page 2
174 */
ee1fb969 175function quiz_questions_in_quiz($layout) {
176 return str_replace(',0', '', $layout);
177}
76cacec8 178
ee1fb969 179/**
26d12a0c 180 * Returns the number of pages in the quiz layout
181 *
182 * @return integer Comma separated list of question ids
183 * @param string $layout The string representing the quiz layout.
184 */
ee1fb969 185function quiz_number_of_pages($layout) {
186 return substr_count($layout, ',0');
187}
76cacec8 188
ee1fb969 189/**
26d12a0c 190 * Returns the first question number for the current quiz page
191 *
192 * @return integer The number of the first question
193 * @param string $quizlayout The string representing the layout for the whole quiz
194 * @param string $pagelayout The string representing the layout for the current page
195 */
ee1fb969 196function quiz_first_questionnumber($quizlayout, $pagelayout) {
197 // this works by finding all the questions from the quizlayout that
198 // come before the current page and then adding up their lengths.
199 global $CFG;
49347eac 200 $start = strpos($quizlayout, ','.$pagelayout.',')-2;
ee1fb969 201 if ($start > 0) {
202 $prevlist = substr($quizlayout, 0, $start);
4f48fb42 203 return get_field_sql("SELECT sum(length)+1 FROM {$CFG->prefix}question
ee1fb969 204 WHERE id IN ($prevlist)");
76cacec8 205 } else {
ee1fb969 206 return 1;
76cacec8 207 }
ee1fb969 208}
76cacec8 209
ee1fb969 210/**
26d12a0c 211 * Re-paginates the quiz layout
212 *
213 * @return string The new layout string
214 * @param string $layout The string representing the quiz layout.
215 * @param integer $perpage The number of questions per page
216 * @param boolean $shuffle Should the questions be reordered randomly?
217 */
ee1fb969 218function quiz_repaginate($layout, $perpage, $shuffle=false) {
219 $layout = str_replace(',0', '', $layout); // remove existing page breaks
220 $questions = explode(',', $layout);
221 if ($shuffle) {
222 srand((float)microtime() * 1000000); // for php < 4.2
223 shuffle($questions);
224 }
225 $i = 1;
226 $layout = '';
227 foreach ($questions as $question) {
228 if ($perpage and $i > $perpage) {
229 $layout .= '0,';
230 $i = 1;
76cacec8 231 }
ee1fb969 232 $layout .= $question.',';
233 $i++;
76cacec8 234 }
ee1fb969 235 return $layout.'0';
236}
76cacec8 237
ee1fb969 238/**
26d12a0c 239 * Print navigation panel for quiz attempt and review pages
240 *
241 * @param integer $page The number of the current page (counting from 0).
242 * @param integer $pages The total number of pages.
243 */
ee1fb969 244function quiz_print_navigation_panel($page, $pages) {
245 //$page++;
2f945146 246 echo '<div class="pagingbar">';
ee1fb969 247 echo '<span class="title">' . get_string('page') . ':</span>';
248 if ($page > 0) {
249 // Print previous link
2f945146 250 $strprev = get_string('previous');
ee1fb969 251 echo '<a href="javascript:navigate(' . ($page - 1) . ');" title="'
252 . $strprev . '">(' . $strprev . ')</a>';
2f945146 253 }
ee1fb969 254 for ($i = 0; $i < $pages; $i++) {
255 if ($i == $page) {
256 echo '<span class="thispage">'.($i+1).'</span>';
2f945146 257 } else {
ee1fb969 258 echo '<a href="javascript:navigate(' . ($i) . ');">'.($i+1).'</a>';
76cacec8 259 }
76cacec8 260 }
ee1fb969 261
262 if ($page < $pages - 1) {
263 // Print next link
2f945146 264 $strnext = get_string('next');
ee1fb969 265 echo '<a href="javascript:navigate(' . ($page + 1) . ');" title="'
266 . $strnext . '">(' . $strnext . ')</a>';
76cacec8 267 }
2f945146 268 echo '</div>';
76cacec8 269}
270
26d12a0c 271/// Functions to do with quiz grades //////////////////////////////////////////
76cacec8 272
ee1fb969 273/**
26d12a0c 274 * Creates an array of maximum grades for a quiz
275 *
276 * The grades are extracted from the quiz_question_instances table.
277 * @return array Array of grades indexed by question id
278 * These are the maximum possible grades that
279 * students can achieve for each of the questions
280 * @param integer $quiz The quiz object
281 */
ee1fb969 282function quiz_get_all_question_grades($quiz) {
283 global $CFG;
76cacec8 284
ee1fb969 285 $questionlist = quiz_questions_in_quiz($quiz->questions);
76cacec8 286 if (empty($questionlist)) {
287 return array();
288 }
289
ee1fb969 290 $instances = get_records_sql("SELECT question,grade,id
291 FROM {$CFG->prefix}quiz_question_instances
292 WHERE quiz = '$quiz->id'" .
293 (is_null($questionlist) ? '' :
294 "AND question IN ($questionlist)"));
76cacec8 295
296 $list = explode(",", $questionlist);
297 $grades = array();
298
299 foreach ($list as $qid) {
ee1fb969 300 if (isset($instances[$qid])) {
301 $grades[$qid] = $instances[$qid]->grade;
76cacec8 302 } else {
303 $grades[$qid] = 1;
304 }
305 }
306 return $grades;
307}
308
212b7b8c 309/**
310 * Get the best current grade for a particular user in a quiz.
77ed3ba5 311 *
212b7b8c 312 * @param object $quiz the quiz object.
313 * @param integer $userid the id of the user.
314 * @return float the user's current grade for this quiz.
315 */
e37da3a5 316function quiz_get_best_grade($quiz, $userid) {
212b7b8c 317 $grade = get_field('quiz_grades', 'grade', 'quiz', $quiz->id, 'userid', $userid);
318
319 // Need to detect errors/no result, without catching 0 scores.
320 if (is_numeric($grade)) {
321 return round($grade,$quiz->decimalpoints);
322 } else {
cb8057de 323 return NULL;
76cacec8 324 }
212b7b8c 325}
76cacec8 326
212b7b8c 327/**
328 * Convert the raw grade stored in $attempt into a grade out of the maximum
329 * grade for this quiz.
77ed3ba5 330 *
212b7b8c 331 * @param float $rawgrade the unadjusted grade, fof example $attempt->sumgrades
332 * @param object $quiz the quiz object. Only the fields grade, sumgrades and decimalpoints are used.
333 * @return float the rescaled grade.
334 */
335function quiz_rescale_grade($rawgrade, $quiz) {
336 if ($quiz->sumgrades) {
337 return round($rawgrade*$quiz->grade/$quiz->sumgrades, $quiz->decimalpoints);
338 } else {
339 return 0;
340 }
76cacec8 341}
342
ee1fb969 343/**
212b7b8c 344 * Get the feedback text that should be show to a student who
345 * got this grade on this quiz.
77ed3ba5 346 *
212b7b8c 347 * @param float $grade a grade on this quiz.
348 * @param integer $quizid the id of the quiz object.
349 * @return string the comment that corresponds to this grade (empty string if there is not one.
350 */
351function quiz_feedback_for_grade($grade, $quizid) {
352 $feedback = get_field_select('quiz_feedback', 'feedbacktext',
353 "quizid = $quizid AND mingrade <= $grade AND $grade < maxgrade");
354
355 if (empty($feedback)) {
356 $feedback = '';
357 }
77ed3ba5 358
212b7b8c 359 return $feedback;
360}
361
362/**
363 * @param integer $quizid the id of the quiz object.
364 * @return boolean Whether this quiz has any non-blank feedback text.
365 */
366function quiz_has_feedback($quizid) {
367 static $cache = array();
368 if (!array_key_exists($quizid, $cache)) {
369 $cache[$quizid] = record_exists_select('quiz_feedback',
370 "quizid = $quizid AND feedbacktext <> ''");
371 }
372 return $cache[$quizid];
373}
374
375/**
77ed3ba5 376 * The quiz grade is the score that student's results are marked out of. When it
212b7b8c 377 * changes, the corresponding data in quiz_grades and quiz_feedback needs to be
378 * rescaled.
77ed3ba5 379 *
212b7b8c 380 * @param float $newgrade the new maximum grade for the quiz.
381 * @param object $quiz the quiz we are updating. Passed by reference so its grade field can be updated too.
382 * @return boolean indicating success or failure.
383 */
384function quiz_set_grade($newgrade, &$quiz) {
385 // This is potentially expensive, so only do it if necessary.
386 if (abs($quiz->grade - $newgrade) < 1e-7) {
387 // Nothing to do.
388 return true;
389 }
b22fc901 390
212b7b8c 391 // Use a transaction, so that on those databases that support it, this is safer.
392 begin_sql();
77ed3ba5 393
212b7b8c 394 // Update the quiz table.
395 $success = set_field('quiz', 'grade', $newgrade, 'id', $quiz->instance);
77ed3ba5 396
212b7b8c 397 // Rescaling the other data is only possible if the old grade was non-zero.
398 if ($quiz->grade > 1e-7) {
399 global $CFG;
77ed3ba5 400
212b7b8c 401 $factor = $newgrade/$quiz->grade;
402 $quiz->grade = $newgrade;
403
404 // Update the quiz_grades table.
405 $timemodified = time();
b22fc901 406 $success = $success && execute_sql("
407 UPDATE {$CFG->prefix}quiz_grades
408 SET grade = $factor * grade, timemodified = $timemodified
409 WHERE quiz = $quiz->id
410 ", false);
411
212b7b8c 412 // Update the quiz_grades table.
b22fc901 413 $success = $success && execute_sql("
414 UPDATE {$CFG->prefix}quiz_feedback
415 SET mingrade = $factor * mingrade, maxgrade = $factor * maxgrade
416 WHERE quizid = $quiz->id
417 ", false);
212b7b8c 418 }
77ed3ba5 419
d6dd2108 420 // update grade item and send all grades to gradebook
421 quiz_grade_item_update($quiz);
422 quiz_update_grades($quiz);
423
212b7b8c 424 if ($success) {
425 return commit_sql();
426 } else {
427 rollback_sql();
428 return false;
429 }
430}
431
432/**
433 * Save the overall grade for a user at a quiz in the quiz_grades table
434 *
435 * @param object $quiz The quiz for which the best grade is to be calculated and then saved.
436 * @param integer $userid The userid to calculate the grade for. Defaults to the current user.
437 * @return boolean Indicates success or failure.
438 */
439function quiz_save_best_grade($quiz, $userid = null) {
ee1fb969 440 global $USER;
441
212b7b8c 442 if (empty($userid)) {
ee1fb969 443 $userid = $USER->id;
444 }
76cacec8 445
ee1fb969 446 // Get all the attempts made by the user
76cacec8 447 if (!$attempts = quiz_get_user_attempts($quiz->id, $userid)) {
35f45a0d 448 notify('Could not find any user attempts');
76cacec8 449 return false;
450 }
451
ee1fb969 452 // Calculate the best grade
76cacec8 453 $bestgrade = quiz_calculate_best_grade($quiz, $attempts);
212b7b8c 454 $bestgrade = quiz_rescale_grade($bestgrade, $quiz);
77ed3ba5 455
ee1fb969 456 // Save the best grade in the database
212b7b8c 457 if ($grade = get_record('quiz_grades', 'quiz', $quiz->id, 'userid', $userid)) {
fc44ee0d 458 $grade->grade = $bestgrade;
76cacec8 459 $grade->timemodified = time();
35f45a0d 460 if (!update_record('quiz_grades', $grade)) {
461 notify('Could not update best grade');
76cacec8 462 return false;
463 }
464 } else {
465 $grade->quiz = $quiz->id;
466 $grade->userid = $userid;
fc44ee0d 467 $grade->grade = $bestgrade;
76cacec8 468 $grade->timemodified = time();
35f45a0d 469 if (!insert_record('quiz_grades', $grade)) {
470 notify('Could not insert new best grade');
76cacec8 471 return false;
472 }
473 }
d6dd2108 474
475 quiz_update_grades($quiz, $userid);
76cacec8 476 return true;
477}
478
fc44ee0d 479/**
26d12a0c 480 * Calculate the overall grade for a quiz given a number of attempts by a particular user.
481 *
482 * @return float The overall grade
483 * @param object $quiz The quiz for which the best grade is to be calculated
484 * @param array $attempts An array of all the attempts of the user at the quiz
485 */
76cacec8 486function quiz_calculate_best_grade($quiz, $attempts) {
76cacec8 487
488 switch ($quiz->grademethod) {
489
ee1fb969 490 case QUIZ_ATTEMPTFIRST:
76cacec8 491 foreach ($attempts as $attempt) {
492 return $attempt->sumgrades;
493 }
494 break;
495
ee1fb969 496 case QUIZ_ATTEMPTLAST:
76cacec8 497 foreach ($attempts as $attempt) {
498 $final = $attempt->sumgrades;
499 }
500 return $final;
501
ee1fb969 502 case QUIZ_GRADEAVERAGE:
76cacec8 503 $sum = 0;
504 $count = 0;
505 foreach ($attempts as $attempt) {
506 $sum += $attempt->sumgrades;
507 $count++;
508 }
509 return (float)$sum/$count;
510
511 default:
ee1fb969 512 case QUIZ_GRADEHIGHEST:
76cacec8 513 $max = 0;
514 foreach ($attempts as $attempt) {
515 if ($attempt->sumgrades > $max) {
516 $max = $attempt->sumgrades;
517 }
518 }
519 return $max;
520 }
521}
522
fc44ee0d 523/**
26d12a0c 524 * Return the attempt with the best grade for a quiz
525 *
526 * Which attempt is the best depends on $quiz->grademethod. If the grade
527 * method is GRADEAVERAGE then this function simply returns the last attempt.
528 * @return object The attempt with the best grade
529 * @param object $quiz The quiz for which the best grade is to be calculated
530 * @param array $attempts An array of all the attempts of the user at the quiz
531 */
76cacec8 532function quiz_calculate_best_attempt($quiz, $attempts) {
76cacec8 533
534 switch ($quiz->grademethod) {
535
ee1fb969 536 case QUIZ_ATTEMPTFIRST:
76cacec8 537 foreach ($attempts as $attempt) {
538 return $attempt;
539 }
540 break;
541
ee1fb969 542 case QUIZ_GRADEAVERAGE: // need to do something with it :-)
543 case QUIZ_ATTEMPTLAST:
76cacec8 544 foreach ($attempts as $attempt) {
545 $final = $attempt;
546 }
547 return $final;
548
549 default:
ee1fb969 550 case QUIZ_GRADEHIGHEST:
76cacec8 551 $max = -1;
552 foreach ($attempts as $attempt) {
553 if ($attempt->sumgrades > $max) {
554 $max = $attempt->sumgrades;
555 $maxattempt = $attempt;
556 }
557 }
558 return $maxattempt;
559 }
560}
561
26d12a0c 562/// Other quiz functions ////////////////////////////////////////////////////
ee1fb969 563
bb52d44c 564/**
26d12a0c 565 * Print a box with quiz start and due dates
566 *
567 * @param object $quiz
568 */
bb52d44c 569function quiz_view_dates($quiz) {
570 if (!$quiz->timeopen && !$quiz->timeclose) {
571 return;
572 }
573
574 print_simple_box_start('center', '', '', '', 'generalbox', 'dates');
575 echo '<table>';
576 if ($quiz->timeopen) {
721b5b28 577 echo '<tr><td class="c0">'.get_string("quizopen", "quiz").':</td>';
bb52d44c 578 echo ' <td class="c1">'.userdate($quiz->timeopen).'</td></tr>';
579 }
580 if ($quiz->timeclose) {
721b5b28 581 echo '<tr><td class="c0">'.get_string("quizclose", "quiz").':</td>';
bb52d44c 582 echo ' <td class="c1">'.userdate($quiz->timeclose).'</td></tr>';
583 }
584 echo '</table>';
585 print_simple_box_end();
586}
587
ee1fb969 588/**
26d12a0c 589 * Parse field names used for the replace options on question edit forms
590 */
c6bfdec3 591function quiz_parse_fieldname($name, $nameprefix='question') {
592 $reg = array();
9274189f 593 if (preg_match("/$nameprefix(\\d+)(\w+)/", $name, $reg)) {
c6bfdec3 594 return array('mode' => $reg[2], 'id' => (int)$reg[1]);
595 } else {
596 return false;
597 }
598}
0ae98223 599
ee1fb969 600/**
26d12a0c 601 * Upgrade states for an attempt to Moodle 1.5 model
602 *
603 * Any state that does not yet have its timestamp set to nonzero has not yet been upgraded from Moodle 1.4
604 * The reason these are still around is that for large sites it would have taken too long to
605 * upgrade all states at once. This function sets the timestamp field and creates an entry in the
606 * question_sessions table.
607 * @param object $attempt The attempt whose states need upgrading
608 */
ee1fb969 609function quiz_upgrade_states($attempt) {
610 global $CFG;
1f48479e 611 // The old quiz model only allowed a single response per quiz attempt so that there will be
612 // only one state record per question for this attempt.
613
614 // We set the timestamp of all states to the timemodified field of the attempt.
4f48fb42 615 execute_sql("UPDATE {$CFG->prefix}question_states SET timestamp = '$attempt->timemodified' WHERE attempt = '$attempt->uniqueid'", false);
1f48479e 616
03d1753c 617 // For each state we create an entry in the question_sessions table, with both newest and
1f48479e 618 // newgraded pointing to this state.
619 // Actually we only do this for states whose question is actually listed in $attempt->layout.
620 // We do not do it for states associated to wrapped questions like for example the questions
621 // used by a RANDOM question
7c4b621a 622 $session = new stdClass;
623 $session->attemptid = $attempt->uniqueid;
1f48479e 624 $questionlist = quiz_questions_in_quiz($attempt->layout);
4f48fb42 625 if ($questionlist and $states = get_records_select('question_states', "attempt = '$attempt->uniqueid' AND question IN ($questionlist)")) {
ee1fb969 626 foreach ($states as $state) {
03d1753c 627 $session->newgraded = $state->id;
628 $session->newest = $state->id;
629 $session->questionid = $state->question;
630 insert_record('question_sessions', $session, false);
ee1fb969 631 }
632 }
633}
634
7d87171b 635/**
636 * @param object $quiz the quiz
637 * @param object $question the question
638 * @return the HTML for a preview question icon.
639 */
640function quiz_question_preview_button($quiz, $question) {
641 global $CFG;
ad7e7ba8 642 $strpreview = get_string('previewquestion', 'quiz');
7d87171b 643 return link_to_popup_window('/question/preview.php?id=' . $question->id . '&amp;quizid=' . $quiz->id, 'questionpreview',
644 "<img src=\"$CFG->pixpath/t/preview.gif\" class=\"iconsmall\" alt=\"$strpreview\" />",
645 0, 0, $strpreview, QUESTION_PREVIEW_POPUP_OPTIONS, true);
ad7e7ba8 646}
0d156caa 647
4f48fb42 648/**
4dca7e51 649 * Determine render options
650 *
651 * @param int $reviewoptions
652 * @param object $state
653 */
654function quiz_get_renderoptions($reviewoptions, $state) {
1b8a7434 655 $options = new stdClass;
77ed3ba5 656
4f48fb42 657 // Show the question in readonly (review) mode if the question is in
658 // the closed state
4dca7e51 659 $options->readonly = question_state_is_closed($state);
4f48fb42 660
661 // Show feedback once the question has been graded (if allowed by the quiz)
4dca7e51 662 $options->feedback = question_state_is_graded($state) && ($reviewoptions & QUIZ_REVIEW_FEEDBACK & QUIZ_REVIEW_IMMEDIATELY);
4f48fb42 663
664 // Show validation only after a validation event
665 $options->validation = QUESTION_EVENTVALIDATE === $state->event;
666
667 // Show correct responses in readonly mode if the quiz allows it
4dca7e51 668 $options->correct_responses = $options->readonly && ($reviewoptions & QUIZ_REVIEW_ANSWERS & QUIZ_REVIEW_IMMEDIATELY);
4f48fb42 669
a4514d91 670 // Show general feedback if the question has been graded and the quiz allows it.
671 $options->generalfeedback = question_state_is_graded($state) && ($reviewoptions & QUIZ_REVIEW_GENERALFEEDBACK & QUIZ_REVIEW_IMMEDIATELY);
1b8a7434 672
77ed3ba5 673 // Show overallfeedback once the attempt is over.
674 $options->overallfeedback = false;
675
4f48fb42 676 // Always show responses and scores
677 $options->responses = true;
678 $options->scores = true;
679
680 return $options;
681}
682
4f48fb42 683/**
cd06115f 684 * Determine review options
77ed3ba5 685 *
cd06115f 686 * @param object $quiz the quiz instance.
687 * @param object $attempt the attempt in question.
688 * @param $context the roles and permissions context,
689 * normally the context for the quiz module instance.
77ed3ba5 690 *
cd06115f 691 * @return object an object with boolean fields responses, scores, feedback,
a4514d91 692 * correct_responses, solutions and general feedback
cd06115f 693 */
694function quiz_get_reviewoptions($quiz, $attempt, $context=null) {
1b8a7434 695
696 $options = new stdClass;
4f48fb42 697 $options->readonly = true;
b6e907a2 698 // Provide the links to the question review and comment script
699 $options->questionreviewlink = '/mod/quiz/reviewquestion.php';
700
8ff134db 701 if ($context && has_capability('mod/quiz:viewreports', $context) and !$attempt->preview) {
4f48fb42 702 // The teacher should be shown everything except during preview when the teachers
703 // wants to see just what the students see
704 $options->responses = true;
705 $options->scores = true;
706 $options->feedback = true;
707 $options->correct_responses = true;
708 $options->solutions = false;
a4514d91 709 $options->generalfeedback = true;
77ed3ba5 710 $options->overallfeedback = true;
711
b6e907a2 712 // Show a link to the comment box only for closed attempts
713 if ($attempt->timefinish) {
714 $options->questioncommentlink = '/mod/quiz/comment.php';
715 }
4f48fb42 716 } else {
1b8a7434 717 if (((time() - $attempt->timefinish) < 120) || $attempt->timefinish==0) {
718 $quiz_state_mask = QUIZ_REVIEW_IMMEDIATELY;
719 } else if (!$quiz->timeclose or time() < $quiz->timeclose) {
720 $quiz_state_mask = QUIZ_REVIEW_OPEN;
721 } else {
722 $quiz_state_mask = QUIZ_REVIEW_CLOSED;
723 }
724 $options->responses = ($quiz->review & $quiz_state_mask & QUIZ_REVIEW_RESPONSES) ? 1 : 0;
725 $options->scores = ($quiz->review & $quiz_state_mask & QUIZ_REVIEW_SCORES) ? 1 : 0;
726 $options->feedback = ($quiz->review & $quiz_state_mask & QUIZ_REVIEW_FEEDBACK) ? 1 : 0;
727 $options->correct_responses = ($quiz->review & $quiz_state_mask & QUIZ_REVIEW_ANSWERS) ? 1 : 0;
728 $options->solutions = ($quiz->review & $quiz_state_mask & QUIZ_REVIEW_SOLUTIONS) ? 1 : 0;
a4514d91 729 $options->generalfeedback = ($quiz->review & $quiz_state_mask & QUIZ_REVIEW_GENERALFEEDBACK) ? 1 : 0;
77ed3ba5 730 $options->overallfeedback = $attempt->timefinish && ($quiz->review & $quiz_state_mask & QUIZ_REVIEW_FEEDBACK);
4f48fb42 731 }
77ed3ba5 732
4f48fb42 733 return $options;
734}
77ed3ba5 735
40377b18 736/**
77ed3ba5 737 * Combines the review options from a number of different quiz attempts.
738 * Returns an array of two ojects, so he suggested way of calling this
739 * funciton is:
740 * list($someoptions, $alloptions) = quiz_get_combined_reviewoptions(...)
741 *
742 * @param object $quiz the quiz instance.
743 * @param array $attempts an array of attempt objects.
744 * @param $context the roles and permissions context,
745 * normally the context for the quiz module instance.
746 *
747 * @return array of two options objects, one showing which options are true for
748 * at least one of the attempts, the other showing which options are true
749 * for all attempts.
750 */
751function quiz_get_combined_reviewoptions($quiz, $attempts, $context=null) {
752 $fields = array('readonly', 'scores', 'feedback', 'correct_responses', 'solutions', 'generalfeedback', 'overallfeedback');
753 $someoptions = new stdClass;
754 $alloptions = new stdClass;
755 foreach ($fields as $field) {
756 $someoptions->$field = false;
757 $alloptions->$field = true;
40377b18 758 }
77ed3ba5 759 foreach ($attempts as $attempt) {
760 $attemptoptions = quiz_get_reviewoptions($quiz, $attempt, $context);
7d014bf5 761 foreach ($fields as $field) {
762 $someoptions->$field = $someoptions->$field || $attemptoptions->$field;
763 $alloptions->$field = $alloptions->$field && $attemptoptions->$field;
764 }
77ed3ba5 765 }
766 return array($someoptions, $alloptions);
40377b18 767}
76cacec8 768?>