MDL-20636 Fix question flag javascript.
[moodle.git] / mod / quiz / locallib.php
1 <?php
3 // This file is part of Moodle - http://moodle.org/
4 //
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 // GNU General Public License for more details.
14 //
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
18 /**
19  * Library of functions used by the quiz module.
20  *
21  * This contains functions that are called from within the quiz module only
22  * Functions that are also called by core Moodle are in {@link lib.php}
23  * This script also loads the code in {@link questionlib.php} which holds
24  * the module-indpendent code for handling questions and which in turn
25  * initialises all the questiontype classes.
26  *
27  * @package    mod
28  * @subpackage quiz
29  * @copyright  1999 onwards Martin Dougiamas and others {@link http://moodle.com}
30  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
31  */
34 defined('MOODLE_INTERNAL') || die();
36 require_once($CFG->dirroot . '/mod/quiz/lib.php');
37 require_once($CFG->dirroot . '/mod/quiz/accessrules.php');
38 require_once($CFG->dirroot . '/mod/quiz/attemptlib.php');
39 require_once($CFG->dirroot . '/question/editlib.php');
40 require_once($CFG->libdir  . '/eventslib.php');
41 require_once($CFG->libdir . '/filelib.php');
44 /**#@+
45  * Options determining how the grades from individual attempts are combined to give
46  * the overall grade for a user
47  */
48 define('QUIZ_GRADEHIGHEST', '1');
49 define('QUIZ_GRADEAVERAGE', '2');
50 define('QUIZ_ATTEMPTFIRST', '3');
51 define('QUIZ_ATTEMPTLAST',  '4');
52 /**#@-*/
54 /**
55  * We show the countdown timer if there is less than this amount of time left before the
56  * the quiz close date. (1 hour)
57  */
58 define('QUIZ_SHOW_TIME_BEFORE_DEADLINE', '3600');
60 /// Functions related to attempts /////////////////////////////////////////
62 /**
63  * Creates an object to represent a new attempt at a quiz
64  *
65  * Creates an attempt object to represent an attempt at the quiz by the current
66  * user starting at the current time. The ->id field is not set. The object is
67  * NOT written to the database.
68  *
69  * @param object $quiz the quiz to create an attempt for.
70  * @param int $attemptnumber the sequence number for the attempt.
71  * @param object $lastattempt the previous attempt by this user, if any. Only needed
72  *         if $attemptnumber > 1 and $quiz->attemptonlast is true.
73  * @param int $timenow the time the attempt was started at.
74  * @param bool $ispreview whether this new attempt is a preview.
75  *
76  * @return object the newly created attempt object.
77  */
78 function quiz_create_attempt($quiz, $attemptnumber, $lastattempt, $timenow, $ispreview = false) {
79     global $USER;
81     if ($attemptnumber == 1 || !$quiz->attemptonlast) {
82     /// We are not building on last attempt so create a new attempt.
83         $attempt = new stdClass();
84         $attempt->quiz = $quiz->id;
85         $attempt->userid = $USER->id;
86         $attempt->preview = 0;
87         if ($quiz->shufflequestions) {
88             $attempt->layout = quiz_clean_layout(quiz_repaginate($quiz->questions, $quiz->questionsperpage, true),true);
89         } else {
90             $attempt->layout = quiz_clean_layout($quiz->questions,true);
91         }
92     } else {
93     /// Build on last attempt.
94         if (empty($lastattempt)) {
95             print_error('cannotfindprevattempt', 'quiz');
96         }
97         $attempt = $lastattempt;
98     }
100     $attempt->attempt = $attemptnumber;
101     $attempt->timestart = $timenow;
102     $attempt->timefinish = 0;
103     $attempt->timemodified = $timenow;
105 /// If this is a preview, mark it as such.
106     if ($ispreview) {
107         $attempt->preview = 1;
108     }
110     return $attempt;
113 /**
114  * Returns an unfinished attempt (if there is one) for the given
115  * user on the given quiz. This function does not return preview attempts.
116  *
117  * @param int $quizid the id of the quiz.
118  * @param int $userid the id of the user.
119  *
120  * @return mixed the unfinished attempt if there is one, false if not.
121  */
122 function quiz_get_user_attempt_unfinished($quizid, $userid) {
123     $attempts = quiz_get_user_attempts($quizid, $userid, 'unfinished', true);
124     if ($attempts) {
125         return array_shift($attempts);
126     } else {
127         return false;
128     }
131 /**
132  * Returns the most recent attempt by a given user on a given quiz.
133  * May be finished, or may not.
134  *
135  * @param int $quizid the id of the quiz.
136  * @param int $userid the id of the user.
137  *
138  * @return mixed the attempt if there is one, false if not.
139  */
140 function quiz_get_latest_attempt_by_user($quizid, $userid) {
141     global $CFG, $DB;
142     $attempt = $DB->get_records_sql('
143             SELECT qa.*
144             FROM {quiz_attempts} qa
145             WHERE qa.quiz = ? AND qa.userid = ?
146             ORDER BY qa.timestart DESC, qa.id DESC',
147             array($quizid, $userid), 0, 1);
148     if ($attempt) {
149         return array_shift($attempt);
150     } else {
151         return false;
152     }
155 /**
156  * Delete a quiz attempt.
157  * @param mixed $attempt an integer attempt id or an attempt object (row of the quiz_attempts table).
158  * @param object $quiz the quiz object.
159  */
160 function quiz_delete_attempt($attempt, $quiz) {
161     global $DB;
162     if (is_numeric($attempt)) {
163         if (!$attempt = $DB->get_record('quiz_attempts', array('id' => $attempt))) {
164             return;
165         }
166     }
168     if ($attempt->quiz != $quiz->id) {
169         debugging("Trying to delete attempt $attempt->id which belongs to quiz $attempt->quiz " .
170                 "but was passed quiz $quiz->id.");
171         return;
172     }
174     $DB->delete_records('quiz_attempts', array('id' => $attempt->id));
175     question_engine::delete_questions_usage_by_activity($attempt->uniqueid);
177     // Search quiz_attempts for other instances by this user.
178     // If none, then delete record for this quiz, this user from quiz_grades
179     // else recalculate best grade
181     $userid = $attempt->userid;
182     if (!$DB->record_exists('quiz_attempts', array('userid' => $userid, 'quiz' => $quiz->id))) {
183         $DB->delete_records('quiz_grades', array('userid' => $userid,'quiz' => $quiz->id));
184     } else {
185         quiz_save_best_grade($quiz, $userid);
186     }
188     quiz_update_grades($quiz, $userid);
191 /**
192  * Delete all the preview attempts at a quiz, or possibly all the attempts belonging
193  * to one user.
194  * @param object $quiz the quiz object.
195  * @param int $userid (optional) if given, only delete the previews belonging to this user.
196  */
197 function quiz_delete_previews($quiz, $userid = null) {
198     global $DB;
199     $conditions = array('quiz' => $quiz->id, 'preview' => 1);
200     if (!empty($userid)) {
201         $conditions['userid'] = $userid;
202     }
203     $previewattempts = $DB->get_records('quiz_attempts', $conditions);
204     foreach ($previewattempts as $attempt) {
205         quiz_delete_attempt($attempt, $quiz);
206     }
209 /**
210  * @param int $quizid The quiz id.
211  * @return bool whether this quiz has any (non-preview) attempts.
212  */
213 function quiz_has_attempts($quizid) {
214     global $DB;
215     return $DB->record_exists('quiz_attempts', array('quiz' => $quizid, 'preview' => 0));
218 /// Functions to do with quiz layout and pages ////////////////////////////////
220 /**
221  * Returns a comma separated list of question ids for the quiz
222  *
223  * @param string $layout The string representing the quiz layout. Each page is
224  *      represented as a comma separated list of question ids and 0 indicating
225  *      page breaks. So 5,2,0,3,0 means questions 5 and 2 on page 1 and question
226  *      3 on page 2
227  * @return string comma separated list of question ids, without page breaks.
228  */
229 function quiz_questions_in_quiz($layout) {
230     $questions = str_replace(',0', '', quiz_clean_layout($layout, true));
231     if ($questions === '0') {
232         return '';
233     } else {
234         return $questions;
235     }
238 /**
239  * Returns the number of pages in a quiz layout
240  *
241  * @param string $layout The string representing the quiz layout. Always ends in ,0
242  * @return int The number of pages in the quiz.
243  */
244 function quiz_number_of_pages($layout) {
245     return substr_count(',' . $layout, ',0');
248 /**
249  * Returns the number of questions in the quiz layout
250  *
251  * @param string $layout the string representing the quiz layout.
252  * @return int The number of questions in the quiz.
253  */
254 function quiz_number_of_questions_in_quiz($layout) {
255     $layout = quiz_questions_in_quiz(quiz_clean_layout($layout));
256     $count = substr_count($layout, ',');
257     if ($layout !== '') {
258         $count++;
259     }
260     return $count;
263 /**
264  * Re-paginates the quiz layout
265  *
266  * @param string $layout  The string representing the quiz layout.
267  * @param int $perpage The number of questions per page
268  * @param bool $shuffle Should the questions be reordered randomly?
269  * @return string the new layout string
270  */
271 function quiz_repaginate($layout, $perpage, $shuffle = false) {
272     $layout = str_replace(',0', '', $layout); // remove existing page breaks
273     $questions = explode(',', $layout);
274     if ($shuffle) {
275         shuffle($questions);
276     }
277     $i = 1;
278     $layout = '';
279     foreach ($questions as $question) {
280         if ($perpage and $i > $perpage) {
281             $layout .= '0,';
282             $i = 1;
283         }
284         $layout .= $question.',';
285         $i++;
286     }
287     return $layout.'0';
290 /// Functions to do with quiz grades //////////////////////////////////////////
292 /**
293  * Creates an array of maximum grades for a quiz
294  *
295  * The grades are extracted from the quiz_question_instances table.
296  * @param object $quiz The quiz settings.
297  * @return array of grades indexed by question id. These are the maximum
298  *      possible grades that students can achieve for each of the questions.
299  */
300 function quiz_get_all_question_grades($quiz) {
301     global $CFG, $DB;
303     $questionlist = quiz_questions_in_quiz($quiz->questions);
304     if (empty($questionlist)) {
305         return array();
306     }
308     $params = array($quiz->id);
309     $wheresql = '';
310     if (!is_null($questionlist)) {
311         list($usql, $question_params) = $DB->get_in_or_equal(explode(',', $questionlist));
312         $wheresql = " AND question $usql ";
313         $params = array_merge($params, $question_params);
314     }
316     $instances = $DB->get_records_sql("SELECT question,grade,id
317                                     FROM {quiz_question_instances}
318                                     WHERE quiz = ? $wheresql", $params);
320     $list = explode(",", $questionlist);
321     $grades = array();
323     foreach ($list as $qid) {
324         if (isset($instances[$qid])) {
325             $grades[$qid] = $instances[$qid]->grade;
326         } else {
327             $grades[$qid] = 1;
328         }
329     }
330     return $grades;
333 /**
334  * Convert the raw grade stored in $attempt into a grade out of the maximum
335  * grade for this quiz.
336  *
337  * @param float $rawgrade the unadjusted grade, fof example $attempt->sumgrades
338  * @param object $quiz the quiz object. Only the fields grade, sumgrades and decimalpoints are used.
339  * @param bool|string $format whether to format the results for display
340  *      or 'question' to format a question grade (different number of decimal places.
341  * @return float|string the rescaled grade, or null/the lang string 'notyetgraded' if the $grade is null.
342  */
343 function quiz_rescale_grade($rawgrade, $quiz, $format = true) {
344     if (is_null($rawgrade)) {
345         $grade = null;
346     } else if ($quiz->sumgrades >= 0.000005) {
347         $grade = $rawgrade * $quiz->grade / $quiz->sumgrades;
348     } else {
349         $grade = 0;
350     }
351     if ($format === 'question') {
352         $grade = quiz_format_question_grade($quiz, $grade);
353     } else if ($format) {
354         $grade = quiz_format_grade($quiz, $grade);
355     }
356     return $grade;
359 /**
360  * Get the feedback text that should be show to a student who
361  * got this grade on this quiz. The feedback is processed ready for diplay.
362  *
363  * @param float $grade a grade on this quiz.
364  * @param object $quiz the quiz settings.
365  * @param object $context the quiz context.
366  * @return string the comment that corresponds to this grade (empty string if there is not one.
367  */
368 function quiz_feedback_for_grade($grade, $quiz, $context) {
369     global $DB;
371     if (is_null($grade)) {
372         return '';
373     }
375     $feedback = $DB->get_record_select('quiz_feedback',
376             'quizid = ? AND mingrade <= ? AND ? < maxgrade', array($quiz->id, $grade, $grade));
378     if (empty($feedback->feedbacktext)) {
379         return '';
380     }
382     // Clean the text, ready for display.
383     $formatoptions = new stdClass();
384     $formatoptions->noclean = true;
385     $feedbacktext = file_rewrite_pluginfile_urls($feedback->feedbacktext, 'pluginfile.php', $context->id, 'mod_quiz', 'feedback', $feedback->id);
386     $feedbacktext = format_text($feedbacktext, $feedback->feedbacktextformat, $formatoptions);
388     return $feedbacktext;
391 /**
392  * @param object $quiz the quiz database row.
393  * @return bool Whether this quiz has any non-blank feedback text.
394  */
395 function quiz_has_feedback($quiz) {
396     global $DB;
397     static $cache = array();
398     if (!array_key_exists($quiz->id, $cache)) {
399         $cache[$quiz->id] = quiz_has_grades($quiz) &&
400                 $DB->record_exists_select('quiz_feedback', "quizid = ? AND " .
401                     $DB->sql_isnotempty('quiz_feedback', 'feedbacktext', false, true),
402                 array($quiz->id));
403     }
404     return $cache[$quiz->id];
407 function quiz_no_questions_message($quiz, $cm, $context) {
408     global $OUTPUT;
410     $output = '';
411     $output .= $OUTPUT->notification(get_string('noquestions', 'quiz'));
412     if (has_capability('mod/quiz:manage', $context)) {
413         $output .= $OUTPUT->single_button(new moodle_url('/mod/quiz/edit.php',
414                 array('cmid' => $cm->id)), get_string('editquiz', 'quiz'), 'get');
415     }
417     return $output;
420 /**
421  * Update the sumgrades field of the quiz. This needs to be called whenever
422  * the grading structure of the quiz is changed. For example if a question is
423  * added or removed, or a question weight is changed.
424  *
425  * @param object $quiz a quiz.
426  */
427 function quiz_update_sumgrades($quiz) {
428     global $DB;
429     $sql = 'UPDATE {quiz}
430             SET sumgrades = COALESCE((
431                 SELECT SUM(grade)
432                 FROM {quiz_question_instances}
433                 WHERE quiz = {quiz}.id
434             ), 0)
435             WHERE id = ?';
436     $DB->execute($sql, array($quiz->id));
437     $quiz->sumgrades = $DB->get_field('quiz', 'sumgrades', array('id' => $quiz->id));
438     if ($quiz->sumgrades < 0.000005) {
439         quiz_set_grade(0, $quiz);
440     }
443 function quiz_update_all_attempt_sumgrades($quiz) {
444     global $DB;
445     $dm = new question_engine_data_mapper();
446     $timenow = time();
448     $sql = "UPDATE {quiz_attempts}
449             SET
450                 timemodified = :timenow,
451                 sumgrades = (
452                     {$dm->sum_usage_marks_subquery('uniqueid')}
453                 )
454             WHERE quiz = :quizid AND timefinish <> 0";
455     $DB->execute($sql, array('timenow' => $timenow, 'quizid' => $quiz->id));
458 /**
459  * The quiz grade is the maximum that student's results are marked out of. When it
460  * changes, the corresponding data in quiz_grades and quiz_feedback needs to be
461  * rescaled. After calling this function, you probably need to call
462  * quiz_update_all_attempt_sumgrades, quiz_update_all_final_grades and
463  * quiz_update_grades.
464  *
465  * @param float $newgrade the new maximum grade for the quiz.
466  * @param object $quiz the quiz we are updating. Passed by reference so its grade field can be updated too.
467  * @return bool indicating success or failure.
468  */
469 function quiz_set_grade($newgrade, $quiz) {
470     global $DB;
471     // This is potentially expensive, so only do it if necessary.
472     if (abs($quiz->grade - $newgrade) < 1e-7) {
473         // Nothing to do.
474         return true;
475     }
477     // Use a transaction, so that on those databases that support it, this is safer.
478     $transaction = $DB->start_delegated_transaction();
480     try {
481         // Update the quiz table.
482         $DB->set_field('quiz', 'grade', $newgrade, array('id' => $quiz->instance));
484         // Rescaling the other data is only possible if the old grade was non-zero.
485         if ($quiz->grade > 1e-7) {
486             global $CFG;
488             $factor = $newgrade/$quiz->grade;
489             $quiz->grade = $newgrade;
491             // Update the quiz_grades table.
492             $timemodified = time();
493             $DB->execute("
494                     UPDATE {quiz_grades}
495                     SET grade = ? * grade, timemodified = ?
496                     WHERE quiz = ?
497             ", array($factor, $timemodified, $quiz->id));
499             // Update the quiz_feedback table.
500             $DB->execute("
501                     UPDATE {quiz_feedback}
502                     SET mingrade = ? * mingrade, maxgrade = ? * maxgrade
503                     WHERE quizid = ?
504             ", array($factor, $factor, $quiz->id));
505         }
507         // update grade item and send all grades to gradebook
508         quiz_grade_item_update($quiz);
509         quiz_update_grades($quiz);
511         $transaction->allow_commit();
512         return true;
514     } catch (Exception $e) {
515         $transaction->rollback($e);
516     }
519 /**
520  * Save the overall grade for a user at a quiz in the quiz_grades table
521  *
522  * @param object $quiz The quiz for which the best grade is to be calculated and then saved.
523  * @param int $userid The userid to calculate the grade for. Defaults to the current user.
524  * @param array $attempts The attempts of this user. Useful if you are
525  * looping through many users. Attempts can be fetched in one master query to
526  * avoid repeated querying.
527  * @return bool Indicates success or failure.
528  */
529 function quiz_save_best_grade($quiz, $userid = null, $attempts = array()) {
530     global $DB;
531     global $USER, $OUTPUT;
533     if (empty($userid)) {
534         $userid = $USER->id;
535     }
537     if (!$attempts){
538         // Get all the attempts made by the user
539         $attempts = quiz_get_user_attempts($quiz->id, $userid);
540     }
542     // Calculate the best grade
543     $bestgrade = quiz_calculate_best_grade($quiz, $attempts);
544     $bestgrade = quiz_rescale_grade($bestgrade, $quiz, false);
546     // Save the best grade in the database
547     if (is_null($bestgrade)) {
548         $DB->delete_records('quiz_grades', array('quiz' => $quiz->id, 'userid' => $userid));
550     } else if ($grade = $DB->get_record('quiz_grades', array('quiz' => $quiz->id, 'userid' => $userid))) {
551         $grade->grade = $bestgrade;
552         $grade->timemodified = time();
553         $DB->update_record('quiz_grades', $grade);
555     } else {
556         $grade->quiz = $quiz->id;
557         $grade->userid = $userid;
558         $grade->grade = $bestgrade;
559         $grade->timemodified = time();
560         $DB->insert_record('quiz_grades', $grade);
561     }
563     quiz_update_grades($quiz, $userid);
566 /**
567  * Calculate the overall grade for a quiz given a number of attempts by a particular user.
568  *
569  * @return float          The overall grade
570  * @param object $quiz    The quiz for which the best grade is to be calculated
571  * @param array $attempts An array of all the attempts of the user at the quiz
572  */
573 function quiz_calculate_best_grade($quiz, $attempts) {
575     switch ($quiz->grademethod) {
577         case QUIZ_ATTEMPTFIRST:
578             foreach ($attempts as $attempt) {
579                 return $attempt->sumgrades;
580             }
581             return $final;
583         case QUIZ_ATTEMPTLAST:
584             foreach ($attempts as $attempt) {
585                 $final = $attempt->sumgrades;
586             }
587             return $final;
589         case QUIZ_GRADEAVERAGE:
590             $sum = 0;
591             $count = 0;
592             foreach ($attempts as $attempt) {
593                 if (!is_null($attempt->sumgrades)) {
594                     $sum += $attempt->sumgrades;
595                     $count++;
596                 }
597             }
598             if ($count == 0) {
599                 return null;
600             }
601             return $sum / $count;
603         default:
604         case QUIZ_GRADEHIGHEST:
605             $max = null;
606             foreach ($attempts as $attempt) {
607                 if ($attempt->sumgrades > $max) {
608                     $max = $attempt->sumgrades;
609                 }
610             }
611             return $max;
612     }
615 /**
616  * Update the final grade at this quiz for all students.
617  *
618  * This function is equivalent to calling quiz_save_best_grade for all
619  * users, but much more efficient.
620  *
621  * @param object $quiz the quiz settings.
622  */
623 function quiz_update_all_final_grades($quiz) {
624     global $DB;
626     if (!$quiz->sumgrades) {
627         return;
628     }
630     $param = array('iquizid' => $quiz->id);
631     $firstlastattemptjoin = "JOIN (
632             SELECT
633                 iquiza.userid,
634                 MIN(attempt) AS firstattempt,
635                 MAX(attempt) AS lastattempt
637             FROM {quiz_attempts iquiza}
639             WHERE
640                 iquiza.timefinish <> 0 AND
641                 iquiza.preview = 0 AND
642                 iquiza.quiz = :iquizid
644             GROUP BY iquiza.userid
645         ) first_last_attempts ON first_last_attempts.userid = quiza.userid";
647     switch ($quiz->grademethod) {
648         case QUIZ_ATTEMPTFIRST:
649             // Becuase of the where clause, there will only be one row, but we
650             // must still use an aggregate function.
651             $select = 'MAX(quiza.sumgrades)';
652             $join = $firstlastattemptjoin;
653             $where = 'quiza.attempt = first_last_attempts.firstattempt AND';
654             break;
656         case QUIZ_ATTEMPTLAST:
657             // Becuase of the where clause, there will only be one row, but we
658             // must still use an aggregate function.
659             $select = 'MAX(quiza.sumgrades)';
660             $join = $firstlastattemptjoin;
661             $where = 'quiza.attempt = first_last_attempts.lastattempt AND';
662             break;
664         case QUIZ_GRADEAVERAGE:
665             $select = 'AVG(quiza.sumgrades)';
666             $join = '';
667             $where = '';
668             break;
670         default:
671         case QUIZ_GRADEHIGHEST:
672             $select = 'MAX(quiza.sumgrades)';
673             $join = '';
674             $where = '';
675             break;
676     }
678     if ($quiz->sumgrades >= 0.000005) {
679         $finalgrade = $select . ' * ' . ($quiz->grade / $quiz->sumgrades);
680     } else {
681         $finalgrade = '0';
682     }
683     $param['quizid'] = $quiz->id;
684     $param['quizid2'] = $quiz->id;
685     $param['quizid3'] = $quiz->id;
686     $param['quizid4'] = $quiz->id;
687     $finalgradesubquery = "
688             SELECT quiza.userid, $finalgrade AS newgrade
689             FROM {quiz_attempts} quiza
690             $join
691             WHERE
692                 $where
693                 quiza.timefinish <> 0 AND
694                 quiza.preview = 0 AND
695                 quiza.quiz = :quizid3
696             GROUP BY quiza.userid";
698     $changedgrades = $DB->get_records_sql("
699             SELECT users.userid, qg.id, qg.grade, newgrades.newgrade
701             FROM (
702                 SELECT userid
703                 FROM {quiz_grades} qg
704                 WHERE quiz = :quizid
705             UNION
706                 SELECT DISTINCT userid
707                 FROM {quiz_attempts} quiza2
708                 WHERE
709                     quiza2.timefinish <> 0 AND
710                     quiza2.preview = 0 AND
711                     quiza2.quiz = :quizid2
712             ) users
714             LEFT JOIN {quiz_grades} qg ON qg.userid = users.userid AND qg.quiz = :quizid4
716             LEFT JOIN (
717                 $finalgradesubquery
718             ) newgrades ON newgrades.userid = users.userid
720             WHERE
721                 ABS(newgrades.newgrade - qg.grade) > 0.000005 OR
722                 (newgrades.newgrade IS NULL) <> (qg.grade IS NULL)",
723             $param);
725     $timenow = time();
726     $todelete = array();
727     foreach ($changedgrades as $changedgrade) {
729         if (is_null($changedgrade->newgrade)) {
730             $todelete[] = $changedgrade->userid;
732         } else if (is_null($changedgrade->grade)) {
733             $toinsert = new stdClass();
734             $toinsert->quiz = $quiz->id;
735             $toinsert->userid = $changedgrade->userid;
736             $toinsert->timemodified = $timenow;
737             $toinsert->grade = $changedgrade->newgrade;
738             $DB->insert_record('quiz_grades', $toinsert);
740         } else {
741             $toupdate = new stdClass();
742             $toupdate->id = $changedgrade->id;
743             $toupdate->grade = $changedgrade->newgrade;
744             $toupdate->timemodified = $timenow;
745             $DB->update_record('quiz_grades', $toupdate);
746         }
747     }
749     if (!empty($todelete)) {
750         list($test, $params) = $DB->get_in_or_equals($todelete);
751         $DB->delete_records_select('quiz_grades',
752                 'quiz = ? AND userid ', array($quiz->id) + $params);
753     }
756 /**
757  * Return the attempt with the best grade for a quiz
758  *
759  * Which attempt is the best depends on $quiz->grademethod. If the grade
760  * method is GRADEAVERAGE then this function simply returns the last attempt.
761  * @return object         The attempt with the best grade
762  * @param object $quiz    The quiz for which the best grade is to be calculated
763  * @param array $attempts An array of all the attempts of the user at the quiz
764  */
765 function quiz_calculate_best_attempt($quiz, $attempts) {
767     switch ($quiz->grademethod) {
769         case QUIZ_ATTEMPTFIRST:
770             foreach ($attempts as $attempt) {
771                 return $attempt;
772             }
773             break;
775         case QUIZ_GRADEAVERAGE: // need to do something with it :-)
776         case QUIZ_ATTEMPTLAST:
777             foreach ($attempts as $attempt) {
778                 $final = $attempt;
779             }
780             return $final;
782         default:
783         case QUIZ_GRADEHIGHEST:
784             $max = -1;
785             foreach ($attempts as $attempt) {
786                 if ($attempt->sumgrades > $max) {
787                     $max = $attempt->sumgrades;
788                     $maxattempt = $attempt;
789                 }
790             }
791             return $maxattempt;
792     }
795 /**
796  * @return the options for calculating the quiz grade from the individual attempt grades.
797  */
798 function quiz_get_grading_options() {
799     return array(
800         QUIZ_GRADEHIGHEST => get_string('gradehighest', 'quiz'),
801         QUIZ_GRADEAVERAGE => get_string('gradeaverage', 'quiz'),
802         QUIZ_ATTEMPTFIRST => get_string('attemptfirst', 'quiz'),
803         QUIZ_ATTEMPTLAST  => get_string('attemptlast', 'quiz')
804     );
807 /**
808  * @param int $option one of the values QUIZ_GRADEHIGHEST, QUIZ_GRADEAVERAGE, QUIZ_ATTEMPTFIRST or QUIZ_ATTEMPTLAST.
809  * @return the lang string for that option.
810  */
811 function quiz_get_grading_option_name($option) {
812     $strings = quiz_get_grading_options();
813     return $strings[$option];
816 /// Other quiz functions ////////////////////////////////////////////////////
818 /**
819  * Upgrade states for an attempt to Moodle 1.5 model
820  *
821  * Any state that does not yet have its timestamp set to nonzero has not yet been upgraded from Moodle 1.4
822  * The reason these are still around is that for large sites it would have taken too long to
823  * upgrade all states at once. This function sets the timestamp field and creates an entry in the
824  * question_sessions table.
825  * @param object $attempt  The attempt whose states need upgrading
826  */
827 function quiz_upgrade_states($attempt) {
828     global $DB;
829     // The old quiz model only allowed a single response per quiz attempt so that there will be
830     // only one state record per question for this attempt.
832     // We set the timestamp of all states to the timemodified field of the attempt.
833     $DB->execute("UPDATE {question_states} SET timestamp = ? WHERE attempt = ?", array($attempt->timemodified, $attempt->uniqueid));
835     // For each state we create an entry in the question_sessions table, with both newest and
836     // newgraded pointing to this state.
837     // Actually we only do this for states whose question is actually listed in $attempt->layout.
838     // We do not do it for states associated to wrapped questions like for example the questions
839     // used by a RANDOM question
840     $session = new stdClass();
841     $session->attemptid = $attempt->uniqueid;
842     $questionlist = quiz_questions_in_quiz($attempt->layout);
843     $params = array($attempt->uniqueid);
844     list($usql, $question_params) = $DB->get_in_or_equal(explode(',',$questionlist));
845     $params = array_merge($params, $question_params);
847     if ($questionlist and $states = $DB->get_records_select('question_states', "attempt = ? AND question $usql", $params)) {
848         foreach ($states as $state) {
849             $session->newgraded = $state->id;
850             $session->newest = $state->id;
851             $session->questionid = $state->question;
852             $DB->insert_record('question_sessions', $session, false);
853         }
854     }
857 /**
858  * @param object $quiz the quiz.
859  * @param int $cmid the course_module object for this quiz.
860  * @param object $question the question.
861  * @param string $returnurl url to return to after action is done.
862  * @return string html for a number of icons linked to action pages for a
863  * question - preview and edit / view icons depending on user capabilities.
864  */
865 function quiz_question_action_icons($quiz, $cmid, $question, $returnurl) {
866     $html = quiz_question_preview_button($quiz, $question) . ' ' .
867             quiz_question_edit_button($cmid, $question, $returnurl);
868     return $html;
871 /**
872  * @param int $cmid the course_module.id for this quiz.
873  * @param object $question the question.
874  * @param string $returnurl url to return to after action is done.
875  * @param string $contentbeforeicon some HTML content to be added inside the link, before the icon.
876  * @return the HTML for an edit icon, view icon, or nothing for a question (depending on permissions).
877  */
878 function quiz_question_edit_button($cmid, $question, $returnurl, $contentaftericon = '') {
879     global $CFG, $OUTPUT;
881     // Minor efficiency saving. Only get strings once, even if there are a lot of icons on one page.
882     static $stredit = null;
883     static $strview = null;
884     if ($stredit === null){
885         $stredit = get_string('edit');
886         $strview = get_string('view');
887     }
889     // What sort of icon should we show?
890     $action = '';
891     if (!empty($question->id) && (question_has_capability_on($question, 'edit', $question->category) ||
892             question_has_capability_on($question, 'move', $question->category))) {
893         $action = $stredit;
894         $icon = '/t/edit';
895     } else if (!empty($question->id) && question_has_capability_on($question, 'view', $question->category)) {
896         $action = $strview;
897         $icon = '/i/info';
898     }
900     // Build the icon.
901     if ($action) {
902         $questionparams = array('returnurl' => $returnurl, 'cmid' => $cmid, 'id' => $question->id);
903         $questionurl = new moodle_url("$CFG->wwwroot/question/question.php", $questionparams);
904         return '<a title="' . $action . '" href="' . $questionurl->out() . '"><img src="' .
905                 $OUTPUT->pix_url($icon) . '" alt="' . $action . '" />' . $contentaftericon .
906                 '</a>';
907     } else {
908         return $contentaftericon;
909     }
912 /**
913  * @param object $quiz the quiz
914  * @param object $question the question
915  * @param bool $label if true, show the preview question label after the icon
916  * @return the HTML for a preview question icon.
917  */
918 function quiz_question_preview_button($quiz, $question, $label = false) {
919     global $CFG, $COURSE, $OUTPUT;
920     if (!question_has_capability_on($question, 'use', $question->category)) {
921         return '';
922     }
924     // Get the appropriate display options.
925     $displayoptions = mod_quiz_display_options::make_from_quiz($quiz,
926             mod_quiz_display_options::DURING);
928     // Work out the correcte preview URL.
929     $url = question_preview_url($question->id, $quiz->preferredbehaviour,
930             $question->maxmark, $displayoptions);
932     // Do we want a label?
933     $strpreviewlabel = '';
934     if ($label) {
935         $strpreviewlabel = get_string('preview', 'quiz');
936     }
938     // Build the icon.
939     $strpreviewquestion = get_string('previewquestion', 'quiz');
940     $image = $OUTPUT->pix_icon('t/preview', $strpreviewquestion);
942     parse_str(QUESTION_PREVIEW_POPUP_OPTIONS, $options);
943     $action = new popup_action('click', $url, 'questionpreview', $options);
945     return $OUTPUT->action_link($url, $image, $action, array('title' => $strpreviewquestion));
948 /**
949  * @param object $attempt the attempt.
950  * @param object $context the quiz context.
951  * @return int whether flags should be shown/editable to the current user for this attempt.
952  */
953 function quiz_get_flag_option($attempt, $context) {
954     global $USER;
955     if (!has_capability('moodle/question:flag', $context)) {
956         return question_display_options::HIDDEN;
957     } else if ($attempt->userid == $USER->id) {
958         return question_display_options::EDITABLE;
959     } else {
960         return question_display_options::VISIBLE;
961     }
964 /**
965  * Work out what state this quiz attempt is in.
966  * @param object $quiz the quiz settings
967  * @param object $attempt the quiz_attempt database row.
968  * @return int one of the mod_quiz_display_options::DURING,
969  *      IMMEDIATELY_AFTER, LATER_WHILE_OPEN or AFTER_CLOSE constants.
970  */
971 function quiz_attempt_state($quiz, $attempt) {
972     if ($attempt->timefinish == 0) {
973         return mod_quiz_display_options::DURING;
974     } else if (time() < $attempt->timefinish + 120) {
975         return mod_quiz_display_options::IMMEDIATELY_AFTER;
976     } else if (!$quiz->timeclose || time() < $quiz->timeclose) {
977         return mod_quiz_display_options::LATER_WHILE_OPEN;
978     } else {
979         return mod_quiz_display_options::AFTER_CLOSE;
980     }
983 /**
984  * The the appropraite mod_quiz_display_options object for this attempt at this
985  * quiz right now.
986  *
987  * @param object $quiz the quiz instance.
988  * @param object $attempt the attempt in question.
989  * @param $context the quiz context.
990  *
991  * @return mod_quiz_display_options
992  */
993 function quiz_get_review_options($quiz, $attempt, $context) {
994     $options = mod_quiz_display_options::make_from_quiz($quiz, quiz_attempt_state($quiz, $attempt));
996     $options->readonly = true;
997     $options->flags = quiz_get_flag_option($attempt, $context);
998     if (!empty($attempt->id)) {
999         $options->questionreviewlink = new moodle_url('/mod/quiz/reviewquestion.php',
1000                 array('attempt' => $attempt->id));
1001     }
1003     // Show a link to the comment box only for closed attempts
1004     if (!empty($attempt->id) && $attempt->timefinish && !$attempt->preview &&
1005             !is_null($context) && has_capability('mod/quiz:grade', $context)) {
1006         $options->manualcomment = question_display_options::VISIBLE;
1007         $options->manualcommentlink = new moodle_url('/mod/quiz/comment.php',
1008                 array('attempt' => $attempt->id));
1009     }
1011     if (!is_null($context) && !$attempt->preview && has_capability('mod/quiz:viewreports', $context) &&
1012             has_capability('moodle/grade:viewhidden', $context)) {
1013         // People who can see reports and hidden grades should be shown everything,
1014         // except during preview when teachers want to see what students see.
1015         $options->attempt = question_display_options::VISIBLE;
1016         $options->correctness = question_display_options::VISIBLE;
1017         $options->marks = question_display_options::MARK_AND_MAX;
1018         $options->feedback = question_display_options::VISIBLE;
1019         $options->numpartscorrect = question_display_options::VISIBLE;
1020         $options->generalfeedback = question_display_options::VISIBLE;
1021         $options->rightanswer = question_display_options::VISIBLE;
1022         $options->overallfeedback = question_display_options::VISIBLE;
1023         $options->history = question_display_options::VISIBLE;
1025     }
1027     return $options;
1030 /**
1031  * Combines the review options from a number of different quiz attempts.
1032  * Returns an array of two ojects, so the suggested way of calling this
1033  * funciton is:
1034  * list($someoptions, $alloptions) = quiz_get_combined_reviewoptions(...)
1035  *
1036  * @param object $quiz the quiz instance.
1037  * @param array $attempts an array of attempt objects.
1038  * @param $context the roles and permissions context,
1039  *          normally the context for the quiz module instance.
1040  *
1041  * @return array of two options objects, one showing which options are true for
1042  *          at least one of the attempts, the other showing which options are true
1043  *          for all attempts.
1044  */
1045 function quiz_get_combined_reviewoptions($quiz, $attempts) {
1046     $fields = array('marks', 'feedback', 'generalfeedback', 'rightanswer', 'overallfeedback');
1047     $someoptions = new stdClass();
1048     $alloptions = new stdClass();
1049     foreach ($fields as $field) {
1050         $someoptions->$field = false;
1051         $alloptions->$field = true;
1052     }
1053     foreach ($attempts as $attempt) {
1054         $attemptoptions = mod_quiz_display_options::make_from_quiz($quiz,
1055                 quiz_attempt_state($quiz, $attempt));
1056         foreach ($fields as $field) {
1057             $someoptions->$field = $someoptions->$field || $attemptoptions->$field;
1058             $alloptions->$field = $alloptions->$field && $attemptoptions->$field;
1059         }
1060     }
1061     return array($someoptions, $alloptions);
1064 /**
1065  * Clean the question layout from various possible anomalies:
1066  * - Remove consecutive ","'s
1067  * - Remove duplicate question id's
1068  * - Remove extra "," from beginning and end
1069  * - Finally, add a ",0" in the end if there is none
1070  *
1071  * @param $string $layout the quiz layout to clean up, usually from $quiz->questions.
1072  * @param bool $removeemptypages If true, remove empty pages from the quiz. False by default.
1073  * @return $string the cleaned-up layout
1074  */
1075 function quiz_clean_layout($layout, $removeemptypages = false) {
1076     // Remove repeated ','s. This can happen when a restore fails to find the right
1077     // id to relink to.
1078     $layout = preg_replace('/,{2,}/', ',', trim($layout, ','));
1080     // Remove duplicate question ids
1081     $layout = explode(',', $layout);
1082     $cleanerlayout = array();
1083     $seen = array();
1084     foreach ($layout as $item) {
1085         if ($item == 0) {
1086             $cleanerlayout[] = '0';
1087         } else if (!in_array($item, $seen)) {
1088             $cleanerlayout[] = $item;
1089             $seen[] = $item;
1090         }
1091     }
1093     if ($removeemptypages) {
1094         // Avoid duplicate page breaks
1095         $layout = $cleanerlayout;
1096         $cleanerlayout = array();
1097         $stripfollowingbreaks = true; // Ensure breaks are stripped from the start.
1098         foreach ($layout as $item) {
1099             if ($stripfollowingbreaks && $item == 0) {
1100                 continue;
1101             }
1102             $cleanerlayout[] = $item;
1103             $stripfollowingbreaks = $item == 0;
1104         }
1105     }
1107     // Add a page break at the end if there is none
1108     if (end($cleanerlayout) !== '0') {
1109         $cleanerlayout[] = '0';
1110     }
1112     return implode(',', $cleanerlayout);
1115 /**
1116  * Get the slot for a question with a particular id.
1117  * @param object $quiz the quiz settings.
1118  * @param int $questionid the of a question in the quiz.
1119  * @return int the corresponding slot. Null if the question is not in the quiz.
1120  */
1121 function quiz_get_slot_for_question($quiz, $questionid) {
1122     $questionids = quiz_questions_in_quiz($quiz->questions);
1123     foreach (explode(',', $questionids) as $key => $id) {
1124         if ($id == $questionid) {
1125             return $key + 1;
1126         }
1127     }
1128     return null;
1131 /// FUNCTIONS FOR SENDING NOTIFICATION EMAILS ///////////////////////////////
1133 /**
1134  * Sends confirmation email to the student taking the course
1135  *
1136  * @param object $a associative array of replaceable fields for the templates
1137  *
1138  * @return bool|string result of events_triger
1139  */
1140 function quiz_send_confirmation($a) {
1142     global $USER;
1144     // recipient is self
1145     $a->useridnumber = $USER->idnumber;
1146     $a->username = fullname($USER);
1147     $a->userusername = $USER->username;
1149     // fetch the subject and body from strings
1150     $subject = get_string('emailconfirmsubject', 'quiz', $a);
1151     $body = get_string('emailconfirmbody', 'quiz', $a);
1153     // send email and analyse result
1154     $eventdata = new stdClass();
1155     $eventdata->component        = 'mod_quiz';
1156     $eventdata->name             = 'confirmation';
1157     $eventdata->notification      = 1;
1159     $eventdata->userfrom          = get_admin();
1160     $eventdata->userto            = $USER;
1161     $eventdata->subject           = $subject;
1162     $eventdata->fullmessage       = $body;
1163     $eventdata->fullmessageformat = FORMAT_PLAIN;
1164     $eventdata->fullmessagehtml   = '';
1166     $eventdata->smallmessage      = get_string('emailconfirmsmall', 'quiz', $a);
1167     $eventdata->contexturl        = $a->quizurl;
1168     $eventdata->contexturlname    = $a->quizname;
1170     return message_send($eventdata);
1173 /**
1174  * Sends notification messages to the interested parties that assign the role capability
1175  *
1176  * @param object $recipient user object of the intended recipient
1177  * @param object $a associative array of replaceable fields for the templates
1178  *
1179  * @return bool|string result of events_triger()
1180  */
1181 function quiz_send_notification($recipient, $a) {
1183     global $USER;
1185     // recipient info for template
1186     $a->username = fullname($recipient);
1187     $a->userusername = $recipient->username;
1189     // fetch the subject and body from strings
1190     $subject = get_string('emailnotifysubject', 'quiz', $a);
1191     $body = get_string('emailnotifybody', 'quiz', $a);
1193     // send email and analyse result
1194     $eventdata = new stdClass();
1195     $eventdata->component        = 'mod_quiz';
1196     $eventdata->name             = 'submission';
1197     $eventdata->notification      = 1;
1199     $eventdata->userfrom          = $USER;
1200     $eventdata->userto            = $recipient;
1201     $eventdata->subject           = $subject;
1202     $eventdata->fullmessage       = $body;
1203     $eventdata->fullmessageformat = FORMAT_PLAIN;
1204     $eventdata->fullmessagehtml   = '';
1206     $eventdata->smallmessage      = get_string('emailnotifysmall', 'quiz', $a);
1207     $eventdata->contexturl        = $a->quizreviewurl;
1208     $eventdata->contexturlname    = $a->quizname;
1210     return message_send($eventdata);
1213 /**
1214  * Takes a bunch of information to format into an email and send
1215  * to the specified recipient.
1216  *
1217  * @param object $course the course
1218  * @param object $quiz the quiz
1219  * @param object $attempt this attempt just finished
1220  * @param object $context the quiz context
1221  * @param object $cm the coursemodule for this quiz
1222  *
1223  * @return int number of emails sent
1224  */
1225 function quiz_send_notification_emails($course, $quiz, $attempt, $context, $cm) {
1226     global $CFG, $USER;
1227     // we will count goods and bads for error logging
1228     $emailresult = array('good' => 0, 'fail' => 0);
1230     // do nothing if required objects not present
1231     if (empty($course) or empty($quiz) or empty($attempt) or empty($context)) {
1232         debugging('quiz_send_notification_emails: Email(s) not sent due to program error.',
1233                 DEBUG_DEVELOPER);
1234         return $emailresult['fail'];
1235     }
1237     // check for confirmation required
1238     $sendconfirm = false;
1239     $notifyexcludeusers = '';
1240     if (has_capability('mod/quiz:emailconfirmsubmission', $context, NULL, false)) {
1241         // exclude from notify emails later
1242         $notifyexcludeusers = $USER->id;
1243         // send the email
1244         $sendconfirm = true;
1245     }
1247     // check for notifications required
1248     $notifyfields = 'u.id, u.username, u.firstname, u.lastname, u.email, u.lang, u.timezone, u.mailformat, u.maildisplay';
1249     $groups = groups_get_all_groups($course->id, $USER->id);
1250     if (is_array($groups) && count($groups) > 0) {
1251         $groups = array_keys($groups);
1252     } else if (groups_get_activity_groupmode($cm, $course) != NOGROUPS) {
1253         // If the user is not in a group, and the quiz is set to group mode,
1254         // then set $gropus to a non-existant id so that only users with
1255         // 'moodle/site:accessallgroups' get notified.
1256         $groups = -1;
1257     } else {
1258         $groups = '';
1259     }
1260     $userstonotify = get_users_by_capability($context, 'mod/quiz:emailnotifysubmission',
1261             $notifyfields, '', '', '', $groups, $notifyexcludeusers, false, false, true);
1263     // if something to send, then build $a
1264     if (! empty($userstonotify) or $sendconfirm) {
1265         $a = new stdClass();
1266         // course info
1267         $a->coursename = $course->fullname;
1268         $a->courseshortname = $course->shortname;
1269         // quiz info
1270         $a->quizname = $quiz->name;
1271         $a->quizreporturl = $CFG->wwwroot . '/mod/quiz/report.php?id=' . $cm->id;
1272         $a->quizreportlink = '<a href="' . $a->quizreporturl . '">' . format_string($quiz->name) . ' report</a>';
1273         $a->quizreviewurl = $CFG->wwwroot . '/mod/quiz/review.php?attempt=' . $attempt->id;
1274         $a->quizreviewlink = '<a href="' . $a->quizreviewurl . '">' . format_string($quiz->name) . ' review</a>';
1275         $a->quizurl = $CFG->wwwroot . '/mod/quiz/view.php?id=' . $cm->id;
1276         $a->quizlink = '<a href="' . $a->quizurl . '">' . format_string($quiz->name) . '</a>';
1277         // attempt info
1278         $a->submissiontime = userdate($attempt->timefinish);
1279         $a->timetaken = format_time($attempt->timefinish - $attempt->timestart);
1280         // student who sat the quiz info
1281         $a->studentidnumber = $USER->idnumber;
1282         $a->studentname = fullname($USER);
1283         $a->studentusername = $USER->username;
1284     }
1286     // send confirmation if required
1287     if ($sendconfirm) {
1288         // send the email and update stats
1289         switch (quiz_send_confirmation($a)) {
1290             case true:
1291                 $emailresult['good']++;
1292                 break;
1293             case false:
1294                 $emailresult['fail']++;
1295                 break;
1296         }
1297     }
1299     // send notifications if required
1300     if (!empty($userstonotify)) {
1301         // loop through recipients and send an email to each and update stats
1302         foreach ($userstonotify as $recipient) {
1303             switch (quiz_send_notification($recipient, $a)) {
1304                 case true:
1305                     $emailresult['good']++;
1306                     break;
1307                 case false:
1308                     $emailresult['fail']++;
1309                     break;
1310             }
1311         }
1312     }
1314     // log errors sending emails if any
1315     if (! empty($emailresult['fail'])) {
1316         debugging('quiz_send_notification_emails:: '.$emailresult['fail'].' email(s) failed to be sent.', DEBUG_DEVELOPER);
1317     }
1319     // return the number of successfully sent emails
1320     return $emailresult['good'];
1323 /**
1324  * Checks if browser is safe browser
1325  *
1326  * @return true, if browser is safe browser else false
1327  */
1328 function quiz_check_safe_browser() {
1329     return strpos($_SERVER['HTTP_USER_AGENT'], "SEB") !== false;
1332 function quiz_get_js_module() {
1333     global $PAGE;
1334     return array(
1335         'name' => 'mod_quiz',
1336         'fullpath' => '/mod/quiz/module.js',
1337         'requires' => array('base', 'dom', 'event-delegate', 'event-key', 'core_question_engine'),
1338         'strings' => array(
1339             array('timesup', 'quiz'),
1340             array('functiondisabledbysecuremode', 'quiz'),
1341             array('flagged', 'question'),
1342         ),
1343     );
1347 /**
1348  * An extension of question_display_options that includes the extra options used
1349  * by the quiz.
1350  *
1351  * @copyright  2010 The Open University
1352  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1353  */
1354 class mod_quiz_display_options extends question_display_options {
1355     /**#@+
1356      * @var integer bits used to indicate various times in relation to a
1357      * quiz attempt.
1358      */
1359     const DURING =            0x10000;
1360     const IMMEDIATELY_AFTER = 0x01000;
1361     const LATER_WHILE_OPEN =  0x00100;
1362     const AFTER_CLOSE =       0x00010;
1363     /**#@-*/
1365     /**
1366      * @var boolean if this is false, then the student is not allowed to review
1367      * anything about the attempt.
1368      */
1369     public $attempt = true;
1371     /**
1372      * @var boolean if this is false, then the student is not allowed to review
1373      * anything about the attempt.
1374      */
1375     public $overallfeedback = self::VISIBLE;
1377     /**
1378      * Set up the various options from the quiz settings, and a time constant.
1379      * @param object $quiz the quiz settings.
1380      * @param int $one of the {@link DURING}, {@link IMMEDIATELY_AFTER},
1381      * {@link LATER_WHILE_OPEN} or {@link AFTER_CLOSE} constants.
1382      * @return mod_quiz_display_options set up appropriately.
1383      */
1384     public static function make_from_quiz($quiz, $when) {
1385         $options = new self();
1387         $options->attempt = self::extract($quiz->reviewattempt, $when, true, false);
1388         $options->correctness = self::extract($quiz->reviewcorrectness, $when);
1389         $options->marks = self::extract($quiz->reviewmarks, $when, self::MARK_AND_MAX);
1390         $options->feedback = self::extract($quiz->reviewspecificfeedback, $when);
1391         $options->generalfeedback = self::extract($quiz->reviewgeneralfeedback, $when);
1392         $options->rightanswer = self::extract($quiz->reviewrightanswer, $when);
1393         $options->overallfeedback = self::extract($quiz->reviewoverallfeedback, $when);
1395         $options->numpartscorrect = $options->feedback;
1397         if ($quiz->questiondecimalpoints != -1) {
1398             $options->markdp = $quiz->questiondecimalpoints;
1399         } else {
1400             $options->markdp = $quiz->decimalpoints;
1401         }
1403         return $options;
1404     }
1406     protected static function extract($bitmask, $bit, $whenset = self::VISIBLE, $whennotset = self::HIDDEN) {
1407         if ($bitmask & $bit) {
1408             return $whenset;
1409         } else {
1410             return $whennotset;
1411         }
1412     }
1416 /**
1417  * A {@link qubaid_condition} for finding all the question usages belonging to
1418  * a particular quiz.
1419  *
1420  * @copyright  2010 The Open University
1421  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1422  */
1423 class quibaid_for_quiz extends qubaid_join {
1424     public function __construct($quizid, $includepreviews = true, $onlyfinished = false) {
1425         global $CFG;
1427         $from = $CFG->prefix . 'quiz_attempts quiza';
1429         $where = 'quiza.quiz = ' . $quizid;
1431         if (!$includepreviews) {
1432             $where .= ' AND preview = 0';
1433         }
1435         if ($onlyfinished) {
1436             $where .= ' AND timefinish <> 0';
1437         }
1439         parent::__construct($from, 'quiza.uniqueid', $where);
1440     }