MDL-20636 Reveiw all throw statements.
[moodle.git] / mod / quiz / attemptlib.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  * Back-end code for handling data about quizzes and the current user's attempt.
20  *
21  * There are classes for loading all the information about a quiz and attempts,
22  * and for displaying the navigation panel.
23  *
24  * @package    mod
25  * @subpackage quiz
26  * @copyright  2008 onwards Tim Hunt
27  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
28  */
31 defined('MOODLE_INTERNAL') || die();
34 /**
35  * Class for quiz exceptions. Just saves a couple of arguments on the
36  * constructor for a moodle_exception.
37  *
38  * @copyright  2008 Tim Hunt
39  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
40  * @since      Moodle 2.0
41  */
42 class moodle_quiz_exception extends moodle_exception {
43     function __construct($quizobj, $errorcode, $a = NULL, $link = '', $debuginfo = null) {
44         if (!$link) {
45             $link = $quizobj->view_url();
46         }
47         parent::__construct($errorcode, 'quiz', $link, $a, $debuginfo);
48     }
49 }
52 /**
53  * A class encapsulating a quiz and the questions it contains, and making the
54  * information available to scripts like view.php.
55  *
56  * Initially, it only loads a minimal amout of information about each question - loading
57  * extra information only when necessary or when asked. The class tracks which questions
58  * are loaded.
59  *
60  * @copyright  2008 Tim Hunt
61  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
62  * @since      Moodle 2.0
63  */
64 class quiz {
65     // Fields initialised in the constructor.
66     protected $course;
67     protected $cm;
68     protected $quiz;
69     protected $context;
70     protected $questionids;
72     // Fields set later if that data is needed.
73     protected $questions = null;
74     protected $accessmanager = null;
75     protected $ispreviewuser = null;
77     // Constructor =========================================================================
78     /**
79      * Constructor, assuming we already have the necessary data loaded.
80      *
81      * @param object $quiz the row from the quiz table.
82      * @param object $cm the course_module object for this quiz.
83      * @param object $course the row from the course table for the course we belong to.
84      * @param bool $getcontext intended for testing - stops the constructor getting the context.
85      */
86     function __construct($quiz, $cm, $course, $getcontext = true) {
87         $this->quiz = $quiz;
88         $this->cm = $cm;
89         $this->quiz->cmid = $this->cm->id;
90         $this->course = $course;
91         if ($getcontext && !empty($cm->id)) {
92             $this->context = get_context_instance(CONTEXT_MODULE, $cm->id);
93         }
94         $this->questionids = explode(',', quiz_questions_in_quiz($this->quiz->questions));
95     }
97     /**
98      * Static function to create a new quiz object for a specific user.
99      *
100      * @param int $quizid the the quiz id.
101      * @param int $userid the the userid.
102      * @return quiz the new quiz object
103      */
104     static public function create($quizid, $userid) {
105         global $DB;
107         $quiz = $DB->get_record('quiz', array('id' => $quizid), '*', MUST_EXIST);
108         $course = $DB->get_record('course', array('id' => $quiz->course), '*', MUST_EXIST);
109         $cm = get_coursemodule_from_instance('quiz', $quiz->id, $course->id, false, MUST_EXIST);
111         // Update quiz with override information
112         $quiz = quiz_update_effective_access($quiz, $userid);
114         return new quiz($quiz, $cm, $course);
115     }
117     // Functions for loading more data =====================================================
119     /**
120      * Load just basic information about all the questions in this quiz.
121      */
122     public function preload_questions() {
123         if (empty($this->questionids)) {
124             throw new moodle_quiz_exception($this, 'noquestions', $this->edit_url());
125         }
126         $this->questions = question_preload_questions($this->questionids,
127                 'qqi.grade AS maxmark, qqi.id AS instance',
128                 '{quiz_question_instances} qqi ON qqi.quiz = :quizid AND q.id = qqi.question',
129                 array('quizid' => $this->quiz->id));
130     }
132    /**
133      * Fully load some or all of the questions for this quiz. You must call {@link preload_questions()} first.
134      *
135      * @param array $questionids question ids of the questions to load. null for all.
136      */
137     public function load_questions($questionids = null) {
138         if (is_null($questionids)) {
139             $questionids = $this->questionids;
140         }
141         $questionstoprocess = array();
142         foreach ($questionids as $id) {
143             if (array_key_exists($id, $this->questions)) {
144                 $questionstoprocess[$id] = $this->questions[$id];
145             }
146         }
147         get_question_options($questionstoprocess);
148     }
150     // Simple getters ======================================================================
151     /** @return int the course id. */
152     public function get_courseid() {
153         return $this->course->id;
154     }
156     /** @return object the row of the course table. */
157     public function get_course() {
158         return $this->course;
159     }
161     /** @return int the quiz id. */
162     public function get_quizid() {
163         return $this->quiz->id;
164     }
166     /** @return object the row of the quiz table. */
167     public function get_quiz() {
168         return $this->quiz;
169     }
171     /** @return string the name of this quiz. */
172     public function get_quiz_name() {
173         return $this->quiz->name;
174     }
176     /** @return int the number of attempts allowed at this quiz (0 = infinite). */
177     public function get_num_attempts_allowed() {
178         return $this->quiz->attempts;
179     }
181     /** @return int the course_module id. */
182     public function get_cmid() {
183         return $this->cm->id;
184     }
186     /** @return object the course_module object. */
187     public function get_cm() {
188         return $this->cm;
189     }
191     /** @return object the module context for this quiz. */
192     public function get_context() {
193         return $this->context;
194     }
196     /**
197      * @return bool wether the current user is someone who previews the quiz,
198      * rather than attempting it.
199      */
200     public function is_preview_user() {
201         if (is_null($this->ispreviewuser)) {
202             $this->ispreviewuser = has_capability('mod/quiz:preview', $this->context);
203         }
204         return $this->ispreviewuser;
205     }
207     /**
208      * @return whether any questions have been added to this quiz.
209      */
210     public function has_questions() {
211         return !empty($this->questionids);
212     }
214     /**
215      * @param int $id the question id.
216      * @return object the question object with that id.
217      */
218     public function get_question($id) {
219         return $this->questions[$id];
220     }
222     /**
223      * @param array $questionids question ids of the questions to load. null for all.
224      */
225     public function get_questions($questionids = null) {
226         if (is_null($questionids)) {
227             $questionids = $this->questionids;
228         }
229         $questions = array();
230         foreach ($questionids as $id) {
231             if (!array_key_exists($id, $this->questions)) {
232                 throw new moodle_exception('cannotstartmissingquestion', 'quiz', $this->view_url());
233             }
234             $questions[$id] = $this->questions[$id];
235             $this->ensure_question_loaded($id);
236         }
237         return $questions;
238     }
240     /**
241      * @param int $timenow the current time as a unix timestamp.
242      * @return quiz_access_manager and instance of the quiz_access_manager class for this quiz at this time.
243      */
244     public function get_access_manager($timenow) {
245         if (is_null($this->accessmanager)) {
246             $this->accessmanager = new quiz_access_manager($this, $timenow,
247                     has_capability('mod/quiz:ignoretimelimits', $this->context, NULL, false));
248         }
249         return $this->accessmanager;
250     }
252     /**
253      * Wrapper round the has_capability funciton that automatically passes in the quiz context.
254      */
255     public function has_capability($capability, $userid = NULL, $doanything = true) {
256         return has_capability($capability, $this->context, $userid, $doanything);
257     }
259     /**
260      * Wrapper round the require_capability funciton that automatically passes in the quiz context.
261      */
262     public function require_capability($capability, $userid = NULL, $doanything = true) {
263         return require_capability($capability, $this->context, $userid, $doanything);
264     }
266     // URLs related to this attempt ========================================================
267     /**
268      * @return string the URL of this quiz's view page.
269      */
270     public function view_url() {
271         global $CFG;
272         return $CFG->wwwroot . '/mod/quiz/view.php?id=' . $this->cm->id;
273     }
275     /**
276      * @return string the URL of this quiz's edit page.
277      */
278     public function edit_url() {
279         global $CFG;
280         return $CFG->wwwroot . '/mod/quiz/edit.php?cmid=' . $this->cm->id;
281     }
283     /**
284      * @param int $attemptid the id of an attempt.
285      * @return string the URL of that attempt.
286      */
287     public function attempt_url($attemptid) {
288         global $CFG;
289         return $CFG->wwwroot . '/mod/quiz/attempt.php?attempt=' . $attemptid;
290     }
292     /**
293      * @return string the URL of this quiz's edit page. Needs to be POSTed to with a cmid parameter.
294      */
295     public function start_attempt_url() {
296         return new moodle_url('/mod/quiz/startattempt.php',
297                 array('cmid' => $this->cm->id, 'sesskey' => sesskey()));
298     }
300     /**
301      * @param int $attemptid the id of an attempt.
302      * @return string the URL of the review of that attempt.
303      */
304     public function review_url($attemptid) {
305         return new moodle_url('/mod/quiz/review.php', array('attempt' => $attemptid));
306     }
308     // Bits of content =====================================================================
310     /**
311      * @param string $title the name of this particular quiz page.
312      * @return array the data that needs to be sent to print_header_simple as the $navigation
313      * parameter.
314      */
315     public function navigation($title) {
316         global $PAGE;
317         $PAGE->navbar->add($title);
318         return '';
319     }
321     // Private methods =====================================================================
322     /**
323      *  Check that the definition of a particular question is loaded, and if not throw an exception.
324      *  @param $id a questionid.
325      */
326     protected function ensure_question_loaded($id) {
327         if (isset($this->questions[$id]->_partiallyloaded)) {
328             throw new moodle_quiz_exception($this, 'questionnotloaded', $id);
329         }
330     }
334 /**
335  * This class extends the quiz class to hold data about the state of a particular attempt,
336  * in addition to the data about the quiz.
337  *
338  * @copyright  2008 Tim Hunt
339  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
340  * @since      Moodle 2.0
341  */
342 class quiz_attempt {
343     // Fields initialised in the constructor.
344     protected $quizobj;
345     protected $attempt;
346     protected $quba;
348     // Fields set later if that data is needed.
349     protected $pagelayout; // array page no => array of numbers on the page in order.
350     protected $reviewoptions = null;
352     // Constructor =========================================================================
353     /**
354      * Constructor assuming we already have the necessary data loaded.
355      *
356      * @param object $attempt the row of the quiz_attempts table.
357      * @param object $quiz the quiz object for this attempt and user.
358      * @param object $cm the course_module object for this quiz.
359      * @param object $course the row from the course table for the course we belong to.
360      */
361     function __construct($attempt, $quiz, $cm, $course) {
362         $this->attempt = $attempt;
363         $this->quizobj = new quiz($quiz, $cm, $course);
364         $this->quba = question_engine::load_questions_usage_by_activity($this->attempt->uniqueid);
365         $this->determine_layout();
366         $this->number_questions();
367     }
369     /**
370      * Used by {create()} and {create_from_usage_id()}.
371      * @param array $conditions passed to $DB->get_record('quiz_attempts', $conditions).
372      */
373     static protected function create_helper($conditions) {
374         global $DB;
376 // TODO deal with the issue that makes this necessary.
377 //    if (!$DB->record_exists('question_sessions', array('attemptid' => $attempt->uniqueid))) {
378 //        // this attempt has not yet been upgraded to the new model
379 //        quiz_upgrade_states($attempt);
380 //    }
382         $attempt = $DB->get_record('quiz_attempts', $conditions, '*', MUST_EXIST);
383         $quiz = $DB->get_record('quiz', array('id' => $attempt->quiz), '*', MUST_EXIST);
384         $course = $DB->get_record('course', array('id' => $quiz->course), '*', MUST_EXIST);
385         $cm = get_coursemodule_from_instance('quiz', $quiz->id, $course->id, false, MUST_EXIST);
387         // Update quiz with override information
388         $quiz = quiz_update_effective_access($quiz, $attempt->userid);
390         return new quiz_attempt($attempt, $quiz, $cm, $course);
391     }
393     /**
394      * Static function to create a new quiz_attempt object given an attemptid.
395      *
396      * @param int $attemptid the attempt id.
397      * @return quiz_attempt the new quiz_attempt object
398      */
399     static public function create($attemptid) {
400         return self::create_helper(array('id' => $attemptid));
401     }
403     /**
404      * Static function to create a new quiz_attempt object given a usage id.
405      *
406      * @param int $usageid the attempt usage id.
407      * @return quiz_attempt the new quiz_attempt object
408      */
409     static public function create_from_usage_id($usageid) {
410         return self::create_helper(array('uniqueid' => $usageid));
411     }
413     private function determine_layout() {
414         $this->pagelayout = array();
416         // Break up the layout string into pages.
417         $pagelayouts = explode(',0', quiz_clean_layout($this->attempt->layout, true));
419         // Strip off any empty last page (normally there is one).
420         if (end($pagelayouts) == '') {
421             array_pop($pagelayouts);
422         }
424         // File the ids into the arrays.
425         $this->pagelayout = array();
426         foreach ($pagelayouts as $page => $pagelayout) {
427             $pagelayout = trim($pagelayout, ',');
428             if ($pagelayout == '') {
429                 continue;
430             }
431             $this->pagelayout[$page] = explode(',', $pagelayout);
432         }
433     }
435     // Number the questions.
436     private function number_questions() {
437         $number = 1;
438         foreach ($this->pagelayout as $page => $slots) {
439             foreach ($slots as $slot) {
440                 $question = $this->quba->get_question($slot);
441                 if ($question->length > 0) {
442                     $question->_number = $number;
443                     $number += $question->length;
444                 } else {
445                     $question->_number = get_string('infoshort', 'quiz');
446                 }
447                 $question->_page = $page;
448             }
449         }
450     }
452     // Simple getters ======================================================================
453     public function get_quiz() {
454         return $this->quizobj->get_quiz();
455     }
457     public function get_quizobj() {
458         return $this->quizobj;
459     }
461     /** @return int the course id. */
462     public function get_courseid() {
463         return $this->quizobj->get_courseid();
464     }
466     /** @return int the course id. */
467     public function get_course() {
468         return $this->quizobj->get_course();
469     }
471     /** @return int the quiz id. */
472     public function get_quizid() {
473         return $this->quizobj->get_quizid();
474     }
476     /** @return string the name of this quiz. */
477     public function get_quiz_name() {
478         return $this->quizobj->get_quiz_name();
479     }
481     /** @return object the course_module object. */
482     public function get_cm() {
483         return $this->quizobj->get_cm();
484     }
486     /** @return object the course_module object. */
487     public function get_cmid() {
488         return $this->quizobj->get_cmid();
489     }
491     /**
492      * @return bool wether the current user is someone who previews the quiz,
493      * rather than attempting it.
494      */
495     public function is_preview_user() {
496         return $this->quizobj->is_preview_user();
497     }
499     /** @return int the number of attempts allowed at this quiz (0 = infinite). */
500     public function get_num_attempts_allowed() {
501         return $this->quizobj->get_num_attempts_allowed();
502     }
504     /** @return int number fo pages in this quiz. */
505     public function get_num_pages() {
506         return count($this->pagelayout);
507     }
509     /**
510      * @param int $timenow the current time as a unix timestamp.
511      * @return quiz_access_manager and instance of the quiz_access_manager class for this quiz at this time.
512      */
513     public function get_access_manager($timenow) {
514         return $this->quizobj->get_access_manager($timenow);
515     }
517     /** @return int the attempt id. */
518     public function get_attemptid() {
519         return $this->attempt->id;
520     }
522     /** @return int the attempt unique id. */
523     public function get_uniqueid() {
524         return $this->attempt->uniqueid;
525     }
527     /** @return object the row from the quiz_attempts table. */
528     public function get_attempt() {
529         return $this->attempt;
530     }
532     /** @return int the number of this attemp (is it this user's first, second, ... attempt). */
533     public function get_attempt_number() {
534         return $this->attempt->attempt;
535     }
537     /** @return int the id of the user this attempt belongs to. */
538     public function get_userid() {
539         return $this->attempt->userid;
540     }
542     /** @return bool whether this attempt has been finished (true) or is still in progress (false). */
543     public function is_finished() {
544         return $this->attempt->timefinish != 0;
545     }
547     /** @return bool whether this attempt is a preview attempt. */
548     public function is_preview() {
549         return $this->attempt->preview;
550     }
552     /**
553      * Is this a student dealing with their own attempt/teacher previewing,
554      * or someone with 'mod/quiz:viewreports' reviewing someone elses attempt.
555      *
556      * @return bool whether this situation should be treated as someone looking at their own
557      * attempt. The distinction normally only matters when an attempt is being reviewed.
558      */
559     public function is_own_attempt() {
560         global $USER;
561         return $this->attempt->userid == $USER->id &&
562                 (!$this->is_preview_user() || $this->attempt->preview);
563     }
565     /**
566      * Is the current user allowed to review this attempt. This applies when
567      * {@link is_own_attempt()} returns false.
568      * @return bool whether the review should be allowed.
569      */
570     public function is_review_allowed() {
571         if (!$this->has_capability('mod/quiz:viewreports')) {
572             return false;
573         }
575         $cm = $this->get_cm();
576         if ($this->has_capability('moodle/site:accessallgroups') ||
577                 groups_get_activity_groupmode($cm) != SEPARATEGROUPS) {
578             return true;
579         }
581         // Check the users have at least one group in common.
582         $teachersgroups = groups_get_activity_allowed_groups($cm);
583         $studentsgroups = groups_get_all_groups($cm->course, $this->attempt->userid, $cm->groupingid);
584         return $teachersgroups && $studentsgroups &&
585                 array_intersect(array_keys($teachersgroups), array_keys($studentsgroups));
586     }
588     /**
589      * Get the overall feedback corresponding to a particular mark.
590      * @param $grade a particular grade.
591      */
592     public function get_overall_feedback($grade) {
593         return quiz_feedback_for_grade($grade, $this->get_quiz(),
594                 $this->quizobj->get_context());
595     }
597     /**
598      * Wrapper round the has_capability funciton that automatically passes in the quiz context.
599      */
600     public function has_capability($capability, $userid = NULL, $doanything = true) {
601         return $this->quizobj->has_capability($capability, $userid, $doanything);
602     }
604     /**
605      * Wrapper round the require_capability funciton that automatically passes in the quiz context.
606      */
607     public function require_capability($capability, $userid = NULL, $doanything = true) {
608         return $this->quizobj->require_capability($capability, $userid, $doanything);
609     }
611     /**
612      * Check the appropriate capability to see whether this user may review their own attempt.
613      * If not, prints an error.
614      */
615     public function check_review_capability() {
616         if (!$this->has_capability('mod/quiz:viewreports')) {
617             if ($this->get_attempt_state() == mod_quiz_display_options::IMMEDIATELY_AFTER) {
618                 $this->require_capability('mod/quiz:attempt');
619             } else {
620                 $this->require_capability('mod/quiz:reviewmyattempts');
621             }
622         }
623     }
625     /**
626      * @return int one of the mod_quiz_display_options::DURING,
627      *      IMMEDIATELY_AFTER, LATER_WHILE_OPEN or AFTER_CLOSE constants.
628      */
629     public function get_attempt_state() {
630         return quiz_attempt_state($this->get_quiz(), $this->attempt);
631     }
633     /**
634      * Wrapper that the correct mod_quiz_display_options for this quiz at the
635      * moment.
636      *
637      * @return question_display_options the render options for this user on this attempt.
638      */
639     public function get_display_options($reviewing) {
640         if ($reviewing) {
641             if (is_null($this->reviewoptions)) {
642                 $this->reviewoptions = quiz_get_review_options($this->get_quiz(),
643                         $this->attempt, $this->quizobj->get_context());
644             }
645             return $this->reviewoptions;
647         } else {
648             $options = mod_quiz_display_options::make_from_quiz($this->get_quiz(),
649                     mod_quiz_display_options::DURING);
650             $options->flags = quiz_get_flag_option($this->attempt, $this->quizobj->get_context());
651             return $options;
652         }
653     }
655     /**
656      * @param int $page page number
657      * @return bool true if this is the last page of the quiz.
658      */
659     public function is_last_page($page) {
660         return $page == count($this->pagelayout) - 1;
661     }
663     /**
664      * Return the list of question ids for either a given page of the quiz, or for the
665      * whole quiz.
666      *
667      * @param mixed $page string 'all' or integer page number.
668      * @return array the reqested list of question ids.
669      */
670     public function get_slots($page = 'all') {
671         if ($page === 'all') {
672             $numbers = array();
673             foreach ($this->pagelayout as $numbersonpage) {
674                 $numbers = array_merge($numbers, $numbersonpage);
675             }
676             return $numbers;
677         } else {
678             return $this->pagelayout[$page];
679         }
680     }
682     /**
683      * Get the question_attempt object for a particular question in this attempt.
684      * @param int $slot the number used to identify this question within this attempt.
685      * @return question_attempt
686      */
687     public function get_question_attempt($slot) {
688         return $this->quba->get_question_attempt($slot);
689     }
691     /**
692      * Is a particular question in this attempt a real question, or something like a description.
693      * @param int $slot the number used to identify this question within this attempt.
694      * @return bool whether that question is a real question.
695      */
696     public function is_real_question($slot) {
697         return $this->quba->get_question($slot)->length != 0;
698     }
700     /**
701      * Is a particular question in this attempt a real question, or something like a description.
702      * @param int $slot the number used to identify this question within this attempt.
703      * @return bool whether that question is a real question.
704      */
705     public function is_question_flagged($slot) {
706         return $this->quba->get_question_attempt($slot)->is_flagged();
707     }
709     /**
710      * Return the grade obtained on a particular question, if the user is permitted to see it.
711      * You must previously have called load_question_states to load the state data about this question.
712      *
713      * @param int $slot the number used to identify this question within this attempt.
714      * @return string the formatted grade, to the number of decimal places specified by the quiz.
715      */
716     public function get_question_number($slot) {
717         return $this->quba->get_question($slot)->_number;
718     }
720     /**
721      * Return the grade obtained on a particular question, if the user is permitted to see it.
722      * You must previously have called load_question_states to load the state data about this question.
723      *
724      * @param int $slot the number used to identify this question within this attempt.
725      * @return string the formatted grade, to the number of decimal places specified by the quiz.
726      */
727     public function get_question_name($slot) {
728         return $this->quba->get_question($slot)->name;
729     }
731     /**
732      * Return the grade obtained on a particular question, if the user is permitted to see it.
733      * You must previously have called load_question_states to load the state data about this question.
734      *
735      * @param int $slot the number used to identify this question within this attempt.
736      * @param bool $showcorrectness Whether right/partial/wrong states should
737      * be distinguised.
738      * @return string the formatted grade, to the number of decimal places specified by the quiz.
739      */
740     public function get_question_status($slot, $showcorrectness) {
741         return $this->quba->get_question_state_string($slot, $showcorrectness);
742     }
744     /**
745      * Return the grade obtained on a particular question.
746      * You must previously have called load_question_states to load the state
747      * data about this question.
748      *
749      * @param int $slot the number used to identify this question within this attempt.
750      * @return string the formatted grade, to the number of decimal places specified by the quiz.
751      */
752     public function get_question_mark($slot) {
753         return quiz_format_question_grade($this->get_quiz(), $this->quba->get_question_mark($slot));
754     }
756     /**
757      * Get the time of the most recent action performed on a question.
758      * @param int $slot the number used to identify this question within this usage.
759      * @return int timestamp.
760      */
761     public function get_question_action_time($slot) {
762         return $this->quba->get_question_action_time($slot);
763     }
765     // URLs related to this attempt ========================================================
766     /**
767      * @return string quiz view url.
768      */
769     public function view_url() {
770         return $this->quizobj->view_url();
771     }
773     /**
774      * @return string the URL of this quiz's edit page. Needs to be POSTed to with a cmid parameter.
775      */
776     public function start_attempt_url() {
777         return $this->quizobj->start_attempt_url();
778     }
780     /**
781      * @param int $slot if speified, the slot number of a specific question to link to.
782      * @param int $page if specified, a particular page to link to. If not givem deduced
783      *      from $slot, or goes to the first page.
784      * @param int $questionid a question id. If set, will add a fragment to the URL
785      * to jump to a particuar question on the page.
786      * @param int $thispage if not -1, the current page. Will cause links to other things on
787      * this page to be output as only a fragment.
788      * @return string the URL to continue this attempt.
789      */
790     public function attempt_url($slot = null, $page = -1, $thispage = -1) {
791         return $this->page_and_question_url('attempt', $slot, $page, false, $thispage);
792     }
794     /**
795      * @return string the URL of this quiz's summary page.
796      */
797     public function summary_url() {
798         return new moodle_url('/mod/quiz/summary.php', array('attempt' => $this->attempt->id));
799     }
801     /**
802      * @return string the URL of this quiz's summary page.
803      */
804     public function processattempt_url() {
805         return new moodle_url('/mod/quiz/processattempt.php');
806     }
808     /**
809      * @param int $slot indicates which question to link to.
810      * @param int $page if specified, the URL of this particular page of the attempt, otherwise
811      * the URL will go to the first page.  If -1, deduce $page from $slot.
812      * @param bool $showall if true, the URL will be to review the entire attempt on one page,
813      * and $page will be ignored.
814      * @param int $thispage if not -1, the current page. Will cause links to other things on
815      * this page to be output as only a fragment.
816      * @return string the URL to review this attempt.
817      */
818     public function review_url($slot = null, $page = -1, $showall = false, $thispage = -1) {
819         return $this->page_and_question_url('review', $slot, $page, $showall, $thispage);
820     }
822     // Bits of content =====================================================================
824     /**
825      * Initialise the JS etc. required all the questions on a page..
826      * @param mixed $page a page number, or 'all'.
827      */
828     public function get_html_head_contributions($page = 'all', $showall = false) {
829         if ($showall) {
830             $page = 'all';
831         }
832         $result = '';
833         foreach ($this->get_slots($page) as $slot) {
834             $result .= $this->quba->render_question_head_html($slot);
835         }
836         $result .= question_engine::initialise_js();
837         return $result;
838     }
840     /**
841      * Initialise the JS etc. required by one question.
842      * @param int $questionid the question id.
843      */
844     public function get_question_html_head_contributions($slot) {
845         return $this->quba->render_question_head_html($slot) .
846                 question_engine::initialise_js();
847     }
849     /**
850      * Print the HTML for the start new preview button.
851      */
852     public function print_restart_preview_button() {
853         global $CFG, $OUTPUT;
854         echo $OUTPUT->container_start('controls');
855         $url = new moodle_url($this->start_attempt_url(), array('forcenew' => true));
856         echo $OUTPUT->single_button($url, get_string('startagain', 'quiz'));
857         echo $OUTPUT->container_end();
858     }
860     /**
861      * Return the HTML of the quiz timer.
862      * @return string HTML content.
863      */
864     public function get_timer_html() {
865         return '<div id="quiz-timer">' . get_string('timeleft', 'quiz') .
866                 ' <span id="quiz-time-left"></span></div>';
867     }
869     /**
870      * Generate the HTML that displayes the question in its current state, with
871      * the appropriate display options.
872      *
873      * @param int $id the id of a question in this quiz attempt.
874      * @param bool $reviewing is the being printed on an attempt or a review page.
875      * @param string $thispageurl the URL of the page this question is being printed on.
876      * @return string HTML for the question in its current state.
877      */
878     public function render_question($slot, $reviewing, $thispageurl = '') {
879         return $this->quba->render_question($slot,
880                 $this->get_display_options($reviewing),
881                 $this->quba->get_question($slot)->_number);
882     }
884     /**
885      * Like {@link render_question()} but displays the question at the past step
886      * indicated by $seq, rather than showing the latest step.
887      *
888      * @param int $id the id of a question in this quiz attempt.
889      * @param int $seq the seq number of the past state to display.
890      * @param bool $reviewing is the being printed on an attempt or a review page.
891      * @param string $thispageurl the URL of the page this question is being printed on.
892      * @return string HTML for the question in its current state.
893      */
894     public function render_question_at_step($slot, $seq, $reviewing, $thispageurl = '') {
895         return $this->quba->render_question_at_step($slot, $seq,
896                 $this->get_display_options($reviewing),
897                 $this->quba->get_question($slot)->_number);
898     }
900     /**
901      * Wrapper round print_question from lib/questionlib.php.
902      *
903      * @param int $id the id of a question in this quiz attempt.
904      * @param bool $reviewing is the being printed on an attempt or a review page.
905      * @param string $thispageurl the URL of the page this question is being printed on.
906      */
907     public function render_question_for_commenting($slot) {
908         $options = $this->get_display_options(true);
909         $options->hide_all_feedback();
910         $options->manualcomment = question_display_options::EDITABLE;
911         return $this->quba->render_question($slot, $options, $this->quba->get_question($slot)->_number);
912     }
914     /**
915      * Check wheter access should be allowed to a particular file.
916      *
917      * @param int $id the id of a question in this quiz attempt.
918      * @param bool $reviewing is the being printed on an attempt or a review page.
919      * @param string $thispageurl the URL of the page this question is being printed on.
920      * @return string HTML for the question in its current state.
921      */
922     public function check_file_access($slot, $reviewing, $contextid, $component,
923             $filearea, $args, $forcedownload) {
924         return $this->quba->check_file_access($slot, $this->get_display_options($reviewing),
925                 $component, $filearea, $args, $forcedownload);
926     }
928     /**
929      * Triggers the sending of the notification emails at the end of this attempt.
930      */
931     public function quiz_send_notification_emails() {
932         quiz_send_notification_emails($this->get_course(), $this->get_quiz(), $this->attempt,
933                 $this->quizobj->get_context(), $this->get_cm());
934     }
936     /**
937      * Get the navigation panel object for this attempt.
938      *
939      * @param $panelclass The type of panel, quiz_attempt_nav_panel or quiz_review_nav_panel
940      * @param $page the current page number.
941      * @param $showall whether we are showing the whole quiz on one page. (Used by review.php)
942      * @return quiz_nav_panel_base the requested object.
943      */
944     public function get_navigation_panel($panelclass, $page, $showall = false) {
945         $panel = new $panelclass($this, $this->get_display_options(true), $page, $showall);
946         return $panel->get_contents();
947     }
949     /**
950      * Given a URL containing attempt={this attempt id}, return an array of variant URLs
951      * @param $url a URL.
952      * @return string HTML fragment. Comma-separated list of links to the other
953      * attempts with the attempt number as the link text. The curent attempt is
954      * included but is not a link.
955      */
956     public function links_to_other_attempts($url) {
957         $search = '/\battempt=' . $this->attempt->id . '\b/';
958         $attempts = quiz_get_user_attempts($this->get_quiz()->id, $this->attempt->userid, 'all');
959         if (count($attempts) <= 1) {
960             return false;
961         }
962         $attemptlist = array();
963         foreach ($attempts as $at) {
964             if ($at->id == $this->attempt->id) {
965                 $attemptlist[] = '<strong>' . $at->attempt . '</strong>';
966             } else {
967                 $changedurl = preg_replace($search, 'attempt=' . $at->id, $url);
968                 $attemptlist[] = '<a href="' . s($changedurl) . '">' . $at->attempt . '</a>';
969             }
970         }
971         return implode(', ', $attemptlist);
972     }
974     // Methods for processing ==================================================
976     /**
977      * Process all the actions that were submitted as part of the current request.
978      *
979      * @param int $timestamp the timestamp that should be stored as the modifed
980      * time in the database for these actions. If null, will use the current time.
981      */
982     public function process_all_actions($timestamp) {
983         global $DB;
984         $this->quba->process_all_actions($timestamp);
985         question_engine::save_questions_usage_by_activity($this->quba);
987         $this->attempt->timemodified = $timestamp;
988         if ($this->attempt->timefinish) {
989             $this->attempt->sumgrades = $this->quba->get_total_mark();
990         }
991         $DB->update_record('quiz_attempts', $this->attempt);
993         if (!$this->is_preview() && $this->attempt->timefinish) {
994             quiz_save_best_grade($this->get_quiz(), $this->get_userid());
995         }
996     }
998     /**
999      * Update the flagged state for all question_attempts in this usage, if their
1000      * flagged state was changed in the request.
1001      */
1002     public function save_question_flags() {
1003         $this->quba->update_question_flags();
1004         question_engine::save_questions_usage_by_activity($this->quba);
1005     }
1007     public function finish_attempt($timestamp) {
1008         global $DB;
1009         $this->quba->process_all_actions($timestamp);
1010         $this->quba->finish_all_questions($timestamp);
1012         question_engine::save_questions_usage_by_activity($this->quba);
1014         $this->attempt->timemodified = $timestamp;
1015         $this->attempt->timefinish = $timestamp;
1016         $this->attempt->sumgrades = $this->quba->get_total_mark();
1017         $DB->update_record('quiz_attempts', $this->attempt);
1019         if (!$this->is_preview()) {
1020             quiz_save_best_grade($this->get_quiz());
1021             $this->quiz_send_notification_emails();
1022         }
1023     }
1025     /**
1026      * Print the fields of the comment form for questions in this attempt.
1027      * @param $slot which question to output the fields for.
1028      * @param $prefix Prefix to add to all field names.
1029      */
1030     public function question_print_comment_fields($slot, $prefix) {
1031         // Work out a nice title.
1032         $student = get_record('user', 'id', $this->get_userid());
1033         $a = new object();
1034         $a->fullname = fullname($student, true);
1035         $a->attempt = $this->get_attempt_number();
1037         question_print_comment_fields($this->quba->get_question_attempt($slot),
1038                 $prefix, $this->get_display_options(true)->markdp,
1039                 get_string('gradingattempt', 'quiz_grading', $a));
1040     }
1042     // Private methods =====================================================================
1044     /**
1045      * Get a URL for a particular question on a particular page of the quiz.
1046      * Used by {@link attempt_url()} and {@link review_url()}.
1047      *
1048      * @param string $script. Used in the URL like /mod/quiz/$script.php
1049      * @param int $slot identifies the specific question on the page to jump to. 0 to just use the $page parameter.
1050      * @param int $page -1 to look up the page number from the slot, otherwise the page number to go to.
1051      * @param bool $showall if true, return a URL with showall=1, and not page number
1052      * @param int $thispage the page we are currently on. Links to questions on this
1053      *      page will just be a fragment #q123. -1 to disable this.
1054      * @return The requested URL.
1055      */
1056     protected function page_and_question_url($script, $slot, $page, $showall, $thispage) {
1057         // Fix up $page
1058         if ($page == -1) {
1059             if (!is_null($slot) && !$showall) {
1060                 $page = $this->quba->get_question($slot)->_page;
1061             } else {
1062                 $page = 0;
1063             }
1064         }
1066         if ($showall) {
1067             $page = 0;
1068         }
1070         // Add a fragment to scroll down to the question.
1071         $fragment = '';
1072         if (!is_null($slot)) {
1073             if ($slot == reset($this->pagelayout[$page])) {
1074                 // First question on page, go to top.
1075                 $fragment = '#';
1076             } else {
1077                 $fragment = '#q' . $slot;
1078             }
1079         }
1081         // Work out the correct start to the URL.
1082         if ($thispage == $page) {
1083             return new moodle_url($fragment);
1085         } else {
1086             $url = new moodle_url('/mod/quiz/' . $script . '.php' . $fragment,
1087                     array('attempt' => $this->attempt->id));
1088             if ($showall) {
1089                 $url->param('showall', 1);
1090             } else if ($page > 0) {
1091                 $url->param('page', $page);
1092             }
1093             return $url;
1094         }
1095     }
1099 /**
1100  * Represents the navigation panel, and builds a {@link block_contents} to allow
1101  * it to be output.
1102  *
1103  * @copyright  2008 Tim Hunt
1104  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1105  * @since      Moodle 2.0
1106  */
1107 abstract class quiz_nav_panel_base {
1108     /** @var quiz_attempt */
1109     protected $attemptobj;
1110     /** @var question_display_options */
1111     protected $options;
1112     /** @var integer */
1113     protected $page;
1114     /** @var boolean */
1115     protected $showall;
1117     public function __construct(quiz_attempt $attemptobj,
1118             question_display_options $options, $page, $showall) {
1119         $this->attemptobj = $attemptobj;
1120         $this->options = $options;
1121         $this->page = $page;
1122         $this->showall = $showall;
1123     }
1125     protected function get_question_buttons() {
1126         $html = '<div class="qn_buttons">' . "\n";
1127         foreach ($this->attemptobj->get_slots() as $slot) {
1128             $qa = $this->attemptobj->get_question_attempt($slot);
1129             $showcorrectness = $this->options->correctness && $qa->has_marks();
1130             $html .= $this->get_question_button($qa, $qa->get_question()->_number,
1131                     $showcorrectness) . "\n";
1132         }
1133         $html .= "</div>\n";
1134         return $html;
1135     }
1137     protected function get_button_id(question_attempt $qa) {
1138         // The id to put on the button element in the HTML.
1139         return 'quiznavbutton' . $qa->get_slot();
1140     }
1142     protected function get_question_button(question_attempt $qa, $number, $showcorrectness) {
1143         $attributes = $this->get_attributes($qa, $showcorrectness);
1145         if (is_numeric($number)) {
1146             $qnostring = 'questionnonav';
1147         } else {
1148             $qnostring = 'questionnonavinfo';
1149         }
1151         $a = new stdClass();
1152         $a->number = $number;
1153         $a->attributes = implode(' ', $attributes);
1155         return '<a href="' . $this->get_question_url($qa->get_slot()) .
1156                 '" class="qnbutton ' . implode(' ', array_keys($attributes)) .
1157                 '" id="' . $this->get_button_id($qa) . '" title="' .
1158                 $qa->get_state_string($showcorrectness) . '">' .
1159                 '<span class="thispageholder"></span><span class="trafficlight"></span>' .
1160                 get_string($qnostring, 'quiz', $a) . '</a>';
1161     }
1163     /**
1164      * @param question_attempt $qa
1165      * @param bool $showcorrectness
1166      * @return array class name => descriptive string.
1167      */
1168     protected function get_attributes(question_attempt $qa, $showcorrectness) {
1169         // The current status of the question.
1170         $attributes = array();
1172         // On the current page?
1173         if ($qa->get_question()->_page == $this->page) {
1174             $attributes['thispage'] = get_string('onthispage', 'quiz');
1175         }
1177         // Question state.
1178         $stateclass = $qa->get_state()->get_state_class($showcorrectness);
1179         if (!$showcorrectness && $stateclass == 'notanswered') {
1180             $stateclass = 'complete';
1181         }
1182         $attributes[$stateclass] = $qa->get_state_string($showcorrectness);
1184         // Flagged?
1185         if ($qa->is_flagged()) {
1186             $attributes['flagged'] = '<span class="flagstate">' .
1187                     get_string('flagged', 'question') . '</span>';
1188         } else {
1189             $attributes[''] = '<span class="flagstate"></span>';
1190         }
1192         return $attributes;
1193     }
1195     protected function get_before_button_bits() {
1196         return '';
1197     }
1199     abstract protected function get_end_bits();
1201     abstract protected function get_question_url($slot);
1203     protected function get_user_picture() {
1204         global $DB, $OUTPUT;
1205         $user = $DB->get_record('user', array('id' => $this->attemptobj->get_userid()));
1206         $output = '';
1207         $output .= '<div id="user-picture" class="clearfix">';
1208         $output .= $OUTPUT->user_picture($user, array('courseid'=>$this->attemptobj->get_courseid()));
1209         $output .= ' ' . fullname($user);
1210         $output .= '</div>';
1211         return $output;
1212     }
1214     public function get_contents() {
1215         global $PAGE;
1216         $PAGE->requires->js_init_call('M.mod_quiz.nav.init', null, false, quiz_get_js_module());
1218         $content = '';
1219         if (!empty($this->attemptobj->get_quiz()->showuserpicture)) {
1220             $content .= $this->get_user_picture() . "\n";
1221         }
1222         $content .= $this->get_before_button_bits();
1223         $content .= $this->get_question_buttons() . "\n";
1224         $content .= '<div class="othernav">' . "\n" . $this->get_end_bits() . "\n</div>\n";
1226         $bc = new block_contents();
1227         $bc->id = 'quiznavigation';
1228         $bc->title = get_string('quiznavigation', 'quiz');
1229         $bc->content = $content;
1230         return $bc;
1231     }
1235 /**
1236  * Specialisation of {@link quiz_nav_panel_base} for the attempt quiz page.
1237  *
1238  * @copyright  2008 Tim Hunt
1239  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1240  * @since      Moodle 2.0
1241  */
1242 class quiz_attempt_nav_panel extends quiz_nav_panel_base {
1243     protected function get_question_url($slot) {
1244         return $this->attemptobj->attempt_url($slot, -1, $this->page);
1245     }
1247     protected function get_before_button_bits() {
1248         return '<div id="quiznojswarning">' . get_string('navnojswarning', 'quiz') . "</div>\n";
1249     }
1251     protected function get_end_bits() {
1252         global $PAGE;
1253         $output = '';
1254         $output .= '<a href="' . s($this->attemptobj->summary_url()) . '" id="endtestlink">' . get_string('endtest', 'quiz') . '</a>';
1255         $output .= $this->attemptobj->get_timer_html();
1256         return $output;
1257     }
1261 /**
1262  * Specialisation of {@link quiz_nav_panel_base} for the review quiz page.
1263  *
1264  * @copyright  2008 Tim Hunt
1265  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1266  * @since      Moodle 2.0
1267  */
1268 class quiz_review_nav_panel extends quiz_nav_panel_base {
1269     protected function get_question_url($slot) {
1270         return $this->attemptobj->review_url($slot, -1, $this->showall, $this->page);
1271     }
1273     protected function get_end_bits() {
1274         $html = '';
1275         if ($this->attemptobj->get_num_pages() > 1) {
1276             if ($this->showall) {
1277                 $html .= '<a href="' . $this->attemptobj->review_url(null, 0, false) . '">' . get_string('showeachpage', 'quiz') . '</a>';
1278             } else {
1279                 $html .= '<a href="' . $this->attemptobj->review_url(null, 0, true) . '">' . get_string('showall', 'quiz') . '</a>';
1280             }
1281         }
1282         $accessmanager = $this->attemptobj->get_access_manager(time());
1283         $html .= $accessmanager->print_finish_review_link($this->attemptobj->is_preview_user(), true);
1284         return $html;
1285     }