65294f9f5b12d42d4cf4e66847fcacb44f5e8837
[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         if (!$quiz = $DB->get_record('quiz', array('id' => $quizid))) {
108             throw new moodle_exception('invalidquizid', 'quiz');
109         }
110         if (!$course = $DB->get_record('course', array('id' => $quiz->course))) {
111             throw new moodle_exception('invalidcoursemodule');
112         }
113         if (!$cm = get_coursemodule_from_instance('quiz', $quiz->id, $course->id)) {
114             throw new moodle_exception('invalidcoursemodule');
115         }
117         // Update quiz with override information
118         $quiz = quiz_update_effective_access($quiz, $userid);
120         return new quiz($quiz, $cm, $course);
121     }
123     // Functions for loading more data =====================================================
125     /**
126      * Load just basic information about all the questions in this quiz.
127      */
128     public function preload_questions() {
129         if (empty($this->questionids)) {
130             throw new moodle_quiz_exception($this, 'noquestions', $this->edit_url());
131         }
132         $this->questions = question_preload_questions($this->questionids,
133                 'qqi.grade AS maxmark, qqi.id AS instance',
134                 '{quiz_question_instances} qqi ON qqi.quiz = :quizid AND q.id = qqi.question',
135                 array('quizid' => $this->quiz->id));
136     }
138    /**
139      * Fully load some or all of the questions for this quiz. You must call {@link preload_questions()} first.
140      *
141      * @param array $questionids question ids of the questions to load. null for all.
142      */
143     public function load_questions($questionids = null) {
144         if (is_null($questionids)) {
145             $questionids = $this->questionids;
146         }
147         $questionstoprocess = array();
148         foreach ($questionids as $id) {
149             if (array_key_exists($id, $this->questions)) {
150                 $questionstoprocess[$id] = $this->questions[$id];
151             }
152         }
153         get_question_options($questionstoprocess);
154     }
156     // Simple getters ======================================================================
157     /** @return int the course id. */
158     public function get_courseid() {
159         return $this->course->id;
160     }
162     /** @return object the row of the course table. */
163     public function get_course() {
164         return $this->course;
165     }
167     /** @return int the quiz id. */
168     public function get_quizid() {
169         return $this->quiz->id;
170     }
172     /** @return object the row of the quiz table. */
173     public function get_quiz() {
174         return $this->quiz;
175     }
177     /** @return string the name of this quiz. */
178     public function get_quiz_name() {
179         return $this->quiz->name;
180     }
182     /** @return int the number of attempts allowed at this quiz (0 = infinite). */
183     public function get_num_attempts_allowed() {
184         return $this->quiz->attempts;
185     }
187     /** @return int the course_module id. */
188     public function get_cmid() {
189         return $this->cm->id;
190     }
192     /** @return object the course_module object. */
193     public function get_cm() {
194         return $this->cm;
195     }
197     /** @return object the module context for this quiz. */
198     public function get_context() {
199         return $this->context;
200     }
202     /**
203      * @return bool wether the current user is someone who previews the quiz,
204      * rather than attempting it.
205      */
206     public function is_preview_user() {
207         if (is_null($this->ispreviewuser)) {
208             $this->ispreviewuser = has_capability('mod/quiz:preview', $this->context);
209         }
210         return $this->ispreviewuser;
211     }
213     /**
214      * @return whether any questions have been added to this quiz.
215      */
216     public function has_questions() {
217         return !empty($this->questionids);
218     }
220     /**
221      * @param int $id the question id.
222      * @return object the question object with that id.
223      */
224     public function get_question($id) {
225         return $this->questions[$id];
226     }
228     /**
229      * @param array $questionids question ids of the questions to load. null for all.
230      */
231     public function get_questions($questionids = null) {
232         if (is_null($questionids)) {
233             $questionids = $this->questionids;
234         }
235         $questions = array();
236         foreach ($questionids as $id) {
237             if (!array_key_exists($id, $this->questions)) {
238                 throw new moodle_exception('cannotstartmissingquestion', 'quiz', $this->view_url());
239             }
240             $questions[$id] = $this->questions[$id];
241             $this->ensure_question_loaded($id);
242         }
243         return $questions;
244     }
246     /**
247      * @param int $timenow the current time as a unix timestamp.
248      * @return quiz_access_manager and instance of the quiz_access_manager class for this quiz at this time.
249      */
250     public function get_access_manager($timenow) {
251         if (is_null($this->accessmanager)) {
252             $this->accessmanager = new quiz_access_manager($this, $timenow,
253                     has_capability('mod/quiz:ignoretimelimits', $this->context, NULL, false));
254         }
255         return $this->accessmanager;
256     }
258     /**
259      * Wrapper round the has_capability funciton that automatically passes in the quiz context.
260      */
261     public function has_capability($capability, $userid = NULL, $doanything = true) {
262         return has_capability($capability, $this->context, $userid, $doanything);
263     }
265     /**
266      * Wrapper round the require_capability funciton that automatically passes in the quiz context.
267      */
268     public function require_capability($capability, $userid = NULL, $doanything = true) {
269         return require_capability($capability, $this->context, $userid, $doanything);
270     }
272     // URLs related to this attempt ========================================================
273     /**
274      * @return string the URL of this quiz's view page.
275      */
276     public function view_url() {
277         global $CFG;
278         return $CFG->wwwroot . '/mod/quiz/view.php?id=' . $this->cm->id;
279     }
281     /**
282      * @return string the URL of this quiz's edit page.
283      */
284     public function edit_url() {
285         global $CFG;
286         return $CFG->wwwroot . '/mod/quiz/edit.php?cmid=' . $this->cm->id;
287     }
289     /**
290      * @param int $attemptid the id of an attempt.
291      * @return string the URL of that attempt.
292      */
293     public function attempt_url($attemptid) {
294         global $CFG;
295         return $CFG->wwwroot . '/mod/quiz/attempt.php?attempt=' . $attemptid;
296     }
298     /**
299      * @return string the URL of this quiz's edit page. Needs to be POSTed to with a cmid parameter.
300      */
301     public function start_attempt_url() {
302         return new moodle_url('/mod/quiz/startattempt.php',
303                 array('cmid' => $this->cm->id, 'sesskey' => sesskey()));
304     }
306     /**
307      * @param int $attemptid the id of an attempt.
308      * @return string the URL of the review of that attempt.
309      */
310     public function review_url($attemptid) {
311         return new moodle_url('/mod/quiz/review.php', array('attempt' => $attemptid));
312     }
314     // Bits of content =====================================================================
316     /**
317      * @param string $title the name of this particular quiz page.
318      * @return array the data that needs to be sent to print_header_simple as the $navigation
319      * parameter.
320      */
321     public function navigation($title) {
322         global $PAGE;
323         $PAGE->navbar->add($title);
324         return '';
325     }
327     // Private methods =====================================================================
328     /**
329      *  Check that the definition of a particular question is loaded, and if not throw an exception.
330      *  @param $id a questionid.
331      */
332     protected function ensure_question_loaded($id) {
333         if (isset($this->questions[$id]->_partiallyloaded)) {
334             throw new moodle_quiz_exception($this, 'questionnotloaded', $id);
335         }
336     }
340 /**
341  * This class extends the quiz class to hold data about the state of a particular attempt,
342  * in addition to the data about the quiz.
343  *
344  * @copyright  2008 Tim Hunt
345  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
346  * @since      Moodle 2.0
347  */
348 class quiz_attempt {
349     // Fields initialised in the constructor.
350     protected $quizobj;
351     protected $attempt;
352     protected $quba;
354     // Fields set later if that data is needed.
355     protected $pagelayout; // array page no => array of numbers on the page in order.
356     protected $reviewoptions = null;
358     // Constructor =========================================================================
359     /**
360      * Constructor assuming we already have the necessary data loaded.
361      *
362      * @param object $attempt the row of the quiz_attempts table.
363      * @param object $quiz the quiz object for this attempt and user.
364      * @param object $cm the course_module object for this quiz.
365      * @param object $course the row from the course table for the course we belong to.
366      */
367     function __construct($attempt, $quiz, $cm, $course) {
368         $this->attempt = $attempt;
369         $this->quizobj = new quiz($quiz, $cm, $course);
370         $this->quba = question_engine::load_questions_usage_by_activity($this->attempt->uniqueid);
371         $this->determine_layout();
372         $this->number_questions();
373     }
375     /**
376      * Used by {create()} and {create_from_usage_id()}.
377      * @param array $conditions passed to $DB->get_record('quiz_attempts', $conditions).
378      */
379     static protected function create_helper($conditions) {
380         global $DB;
382 // TODO deal with the issue that makes this necessary.
383 //    if (!$DB->record_exists('question_sessions', array('attemptid' => $attempt->uniqueid))) {
384 //        // this attempt has not yet been upgraded to the new model
385 //        quiz_upgrade_states($attempt);
386 //    }
388         if (!$attempt = $DB->get_record('quiz_attempts', $conditions)) {
389             throw new moodle_exception('invalidattemptid', 'quiz');
390         }
391         if (!$quiz = $DB->get_record('quiz', array('id' => $attempt->quiz))) {
392             throw new moodle_exception('invalidquizid', 'quiz');
393         }
394         if (!$course = $DB->get_record('course', array('id' => $quiz->course))) {
395             throw new moodle_exception('invalidcoursemodule');
396         }
397         if (!$cm = get_coursemodule_from_instance('quiz', $quiz->id, $course->id)) {
398             throw new moodle_exception('invalidcoursemodule');
399         }
401         // Update quiz with override information
402         $quiz = quiz_update_effective_access($quiz, $attempt->userid);
404         return new quiz_attempt($attempt, $quiz, $cm, $course);
405     }
407     /**
408      * Static function to create a new quiz_attempt object given an attemptid.
409      *
410      * @param int $attemptid the attempt id.
411      * @return quiz_attempt the new quiz_attempt object
412      */
413     static public function create($attemptid) {
414         return self::create_helper(array('id' => $attemptid));
415     }
417     /**
418      * Static function to create a new quiz_attempt object given a usage id.
419      *
420      * @param int $usageid the attempt usage id.
421      * @return quiz_attempt the new quiz_attempt object
422      */
423     static public function create_from_usage_id($usageid) {
424         return self::create_helper(array('uniqueid' => $usageid));
425     }
427     private function determine_layout() {
428         $this->pagelayout = array();
430         // Break up the layout string into pages.
431         $pagelayouts = explode(',0', quiz_clean_layout($this->attempt->layout, true));
433         // Strip off any empty last page (normally there is one).
434         if (end($pagelayouts) == '') {
435             array_pop($pagelayouts);
436         }
438         // File the ids into the arrays.
439         $this->pagelayout = array();
440         foreach ($pagelayouts as $page => $pagelayout) {
441             $pagelayout = trim($pagelayout, ',');
442             if ($pagelayout == '') {
443                 continue;
444             }
445             $this->pagelayout[$page] = explode(',', $pagelayout);
446         }
447     }
449     // Number the questions.
450     private function number_questions() {
451         $number = 1;
452         foreach ($this->pagelayout as $page => $slots) {
453             foreach ($slots as $slot) {
454                 $question = $this->quba->get_question($slot);
455                 if ($question->length > 0) {
456                     $question->_number = $number;
457                     $number += $question->length;
458                 } else {
459                     $question->_number = get_string('infoshort', 'quiz');
460                 }
461                 $question->_page = $page;
462             }
463         }
464     }
466     // Simple getters ======================================================================
467     public function get_quiz() {
468         return $this->quizobj->get_quiz();
469     }
471     public function get_quizobj() {
472         return $this->quizobj;
473     }
475     /** @return int the course id. */
476     public function get_courseid() {
477         return $this->quizobj->get_courseid();
478     }
480     /** @return int the course id. */
481     public function get_course() {
482         return $this->quizobj->get_course();
483     }
485     /** @return int the quiz id. */
486     public function get_quizid() {
487         return $this->quizobj->get_quizid();
488     }
490     /** @return string the name of this quiz. */
491     public function get_quiz_name() {
492         return $this->quizobj->get_quiz_name();
493     }
495     /** @return object the course_module object. */
496     public function get_cm() {
497         return $this->quizobj->get_cm();
498     }
500     /** @return object the course_module object. */
501     public function get_cmid() {
502         return $this->quizobj->get_cmid();
503     }
505     /**
506      * @return bool wether the current user is someone who previews the quiz,
507      * rather than attempting it.
508      */
509     public function is_preview_user() {
510         return $this->quizobj->is_preview_user();
511     }
513     /** @return int the number of attempts allowed at this quiz (0 = infinite). */
514     public function get_num_attempts_allowed() {
515         return $this->quizobj->get_num_attempts_allowed();
516     }
518     /** @return int number fo pages in this quiz. */
519     public function get_num_pages() {
520         return count($this->pagelayout);
521     }
523     /**
524      * @param int $timenow the current time as a unix timestamp.
525      * @return quiz_access_manager and instance of the quiz_access_manager class for this quiz at this time.
526      */
527     public function get_access_manager($timenow) {
528         return $this->quizobj->get_access_manager($timenow);
529     }
531     /** @return int the attempt id. */
532     public function get_attemptid() {
533         return $this->attempt->id;
534     }
536     /** @return int the attempt unique id. */
537     public function get_uniqueid() {
538         return $this->attempt->uniqueid;
539     }
541     /** @return object the row from the quiz_attempts table. */
542     public function get_attempt() {
543         return $this->attempt;
544     }
546     /** @return int the number of this attemp (is it this user's first, second, ... attempt). */
547     public function get_attempt_number() {
548         return $this->attempt->attempt;
549     }
551     /** @return int the id of the user this attempt belongs to. */
552     public function get_userid() {
553         return $this->attempt->userid;
554     }
556     /** @return bool whether this attempt has been finished (true) or is still in progress (false). */
557     public function is_finished() {
558         return $this->attempt->timefinish != 0;
559     }
561     /** @return bool whether this attempt is a preview attempt. */
562     public function is_preview() {
563         return $this->attempt->preview;
564     }
566     /**
567      * Is this a student dealing with their own attempt/teacher previewing,
568      * or someone with 'mod/quiz:viewreports' reviewing someone elses attempt.
569      *
570      * @return bool whether this situation should be treated as someone looking at their own
571      * attempt. The distinction normally only matters when an attempt is being reviewed.
572      */
573     public function is_own_attempt() {
574         global $USER;
575         return $this->attempt->userid == $USER->id &&
576                 (!$this->is_preview_user() || $this->attempt->preview);
577     }
579     /**
580      * Is the current user allowed to review this attempt. This applies when
581      * {@link is_own_attempt()} returns false.
582      * @return bool whether the review should be allowed.
583      */
584     public function is_review_allowed() {
585         if (!$this->has_capability('mod/quiz:viewreports')) {
586             return false;
587         }
589         $cm = $this->get_cm();
590         if ($this->has_capability('moodle/site:accessallgroups') ||
591                 groups_get_activity_groupmode($cm) != SEPARATEGROUPS) {
592             return true;
593         }
595         // Check the users have at least one group in common.
596         $teachersgroups = groups_get_activity_allowed_groups($cm);
597         $studentsgroups = groups_get_all_groups($cm->course, $this->attempt->userid, $cm->groupingid);
598         return $teachersgroups && $studentsgroups &&
599                 array_intersect(array_keys($teachersgroups), array_keys($studentsgroups));
600     }
602     /**
603      * Get the overall feedback corresponding to a particular mark.
604      * @param $grade a particular grade.
605      */
606     public function get_overall_feedback($grade) {
607         return quiz_feedback_for_grade($grade, $this->get_quiz(),
608                 $this->quizobj->get_context());
609     }
611     /**
612      * Wrapper round the has_capability funciton that automatically passes in the quiz context.
613      */
614     public function has_capability($capability, $userid = NULL, $doanything = true) {
615         return $this->quizobj->has_capability($capability, $userid, $doanything);
616     }
618     /**
619      * Wrapper round the require_capability funciton that automatically passes in the quiz context.
620      */
621     public function require_capability($capability, $userid = NULL, $doanything = true) {
622         return $this->quizobj->require_capability($capability, $userid, $doanything);
623     }
625     /**
626      * Check the appropriate capability to see whether this user may review their own attempt.
627      * If not, prints an error.
628      */
629     public function check_review_capability() {
630         if (!$this->has_capability('mod/quiz:viewreports')) {
631             if ($this->get_attempt_state() == mod_quiz_display_options::IMMEDIATELY_AFTER) {
632                 $this->require_capability('mod/quiz:attempt');
633             } else {
634                 $this->require_capability('mod/quiz:reviewmyattempts');
635             }
636         }
637     }
639     /**
640      * @return int one of the mod_quiz_display_options::DURING,
641      *      IMMEDIATELY_AFTER, LATER_WHILE_OPEN or AFTER_CLOSE constants.
642      */
643     public function get_attempt_state() {
644         return quiz_attempt_state($this->get_quiz(), $this->attempt);
645     }
647     /**
648      * Wrapper that the correct mod_quiz_display_options for this quiz at the
649      * moment.
650      *
651      * @return question_display_options the render options for this user on this attempt.
652      */
653     public function get_display_options($reviewing) {
654         if ($reviewing) {
655             if (is_null($this->reviewoptions)) {
656                 $this->reviewoptions = quiz_get_review_options($this->get_quiz(),
657                         $this->attempt, $this->quizobj->get_context());
658             }
659             return $this->reviewoptions;
661         } else {
662             $options = mod_quiz_display_options::make_from_quiz($this->get_quiz(),
663                     mod_quiz_display_options::DURING);
664             $options->flags = quiz_get_flag_option($this->attempt, $this->quizobj->get_context());
665             return $options;
666         }
667     }
669     /**
670      * @param int $page page number
671      * @return bool true if this is the last page of the quiz.
672      */
673     public function is_last_page($page) {
674         return $page == count($this->pagelayout) - 1;
675     }
677     /**
678      * Return the list of question ids for either a given page of the quiz, or for the
679      * whole quiz.
680      *
681      * @param mixed $page string 'all' or integer page number.
682      * @return array the reqested list of question ids.
683      */
684     public function get_slots($page = 'all') {
685         if ($page === 'all') {
686             $numbers = array();
687             foreach ($this->pagelayout as $numbersonpage) {
688                 $numbers = array_merge($numbers, $numbersonpage);
689             }
690             return $numbers;
691         } else {
692             return $this->pagelayout[$page];
693         }
694     }
696     /**
697      * Get the question_attempt object for a particular question in this attempt.
698      * @param int $slot the number used to identify this question within this attempt.
699      * @return question_attempt
700      */
701     public function get_question_attempt($slot) {
702         return $this->quba->get_question_attempt($slot);
703     }
705     /**
706      * Is a particular question in this attempt a real question, or something like a description.
707      * @param int $slot the number used to identify this question within this attempt.
708      * @return bool whether that question is a real question.
709      */
710     public function is_real_question($slot) {
711         return $this->quba->get_question($slot)->length != 0;
712     }
714     /**
715      * Is a particular question in this attempt a real question, or something like a description.
716      * @param int $slot the number used to identify this question within this attempt.
717      * @return bool whether that question is a real question.
718      */
719     public function is_question_flagged($slot) {
720         return $this->quba->get_question_attempt($slot)->is_flagged();
721     }
723     /**
724      * Return the grade obtained on a particular question, if the user is permitted to see it.
725      * You must previously have called load_question_states to load the state data about this question.
726      *
727      * @param int $slot the number used to identify this question within this attempt.
728      * @return string the formatted grade, to the number of decimal places specified by the quiz.
729      */
730     public function get_question_number($slot) {
731         return $this->quba->get_question($slot)->_number;
732     }
734     /**
735      * Return the grade obtained on a particular question, if the user is permitted to see it.
736      * You must previously have called load_question_states to load the state data about this question.
737      *
738      * @param int $slot the number used to identify this question within this attempt.
739      * @return string the formatted grade, to the number of decimal places specified by the quiz.
740      */
741     public function get_question_name($slot) {
742         return $this->quba->get_question($slot)->name;
743     }
745     /**
746      * Return the grade obtained on a particular question, if the user is permitted to see it.
747      * You must previously have called load_question_states to load the state data about this question.
748      *
749      * @param int $slot the number used to identify this question within this attempt.
750      * @param bool $showcorrectness Whether right/partial/wrong states should
751      * be distinguised.
752      * @return string the formatted grade, to the number of decimal places specified by the quiz.
753      */
754     public function get_question_status($slot, $showcorrectness) {
755         return $this->quba->get_question_state_string($slot, $showcorrectness);
756     }
758     /**
759      * Return the grade obtained on a particular question.
760      * You must previously have called load_question_states to load the state
761      * data about this question.
762      *
763      * @param int $slot the number used to identify this question within this attempt.
764      * @return string the formatted grade, to the number of decimal places specified by the quiz.
765      */
766     public function get_question_mark($slot) {
767         return quiz_format_question_grade($this->get_quiz(), $this->quba->get_question_mark($slot));
768     }
770     /**
771      * Get the time of the most recent action performed on a question.
772      * @param int $slot the number used to identify this question within this usage.
773      * @return int timestamp.
774      */
775     public function get_question_action_time($slot) {
776         return $this->quba->get_question_action_time($slot);
777     }
779     // URLs related to this attempt ========================================================
780     /**
781      * @return string quiz view url.
782      */
783     public function view_url() {
784         return $this->quizobj->view_url();
785     }
787     /**
788      * @return string the URL of this quiz's edit page. Needs to be POSTed to with a cmid parameter.
789      */
790     public function start_attempt_url() {
791         return $this->quizobj->start_attempt_url();
792     }
794     /**
795      * @param int $slot if speified, the slot number of a specific question to link to.
796      * @param int $page if specified, a particular page to link to. If not givem deduced
797      *      from $slot, or goes to the first page.
798      * @param int $questionid a question id. If set, will add a fragment to the URL
799      * to jump to a particuar question on the page.
800      * @param int $thispage if not -1, the current page. Will cause links to other things on
801      * this page to be output as only a fragment.
802      * @return string the URL to continue this attempt.
803      */
804     public function attempt_url($slot = null, $page = -1, $thispage = -1) {
805         return $this->page_and_question_url('attempt', $slot, $page, false, $thispage);
806     }
808     /**
809      * @return string the URL of this quiz's summary page.
810      */
811     public function summary_url() {
812         return new moodle_url('/mod/quiz/summary.php', array('attempt' => $this->attempt->id));
813     }
815     /**
816      * @return string the URL of this quiz's summary page.
817      */
818     public function processattempt_url() {
819         return new moodle_url('/mod/quiz/processattempt.php');
820     }
822     /**
823      * @param int $slot indicates which question to link to.
824      * @param int $page if specified, the URL of this particular page of the attempt, otherwise
825      * the URL will go to the first page.  If -1, deduce $page from $slot.
826      * @param bool $showall if true, the URL will be to review the entire attempt on one page,
827      * and $page will be ignored.
828      * @param int $thispage if not -1, the current page. Will cause links to other things on
829      * this page to be output as only a fragment.
830      * @return string the URL to review this attempt.
831      */
832     public function review_url($slot = null, $page = -1, $showall = false, $thispage = -1) {
833         return $this->page_and_question_url('review', $slot, $page, $showall, $thispage);
834     }
836     // Bits of content =====================================================================
838     /**
839      * Initialise the JS etc. required all the questions on a page..
840      * @param mixed $page a page number, or 'all'.
841      */
842     public function get_html_head_contributions($page = 'all', $showall = false) {
843         if ($showall) {
844             $page = 'all';
845         }
846         $result = '';
847         foreach ($this->get_slots($page) as $slot) {
848             $result .= $this->quba->render_question_head_html($slot);
849         }
850         $result .= question_engine::initialise_js();
851         return $result;
852     }
854     /**
855      * Initialise the JS etc. required by one question.
856      * @param int $questionid the question id.
857      */
858     public function get_question_html_head_contributions($slot) {
859         return $this->quba->render_question_head_html($slot) .
860                 question_engine::initialise_js();
861     }
863     /**
864      * Print the HTML for the start new preview button.
865      */
866     public function print_restart_preview_button() {
867         global $CFG, $OUTPUT;
868         echo $OUTPUT->container_start('controls');
869         $url = new moodle_url($this->start_attempt_url(), array('forcenew' => true));
870         echo $OUTPUT->single_button($url, get_string('startagain', 'quiz'));
871         echo $OUTPUT->container_end();
872     }
874     /**
875      * Return the HTML of the quiz timer.
876      * @return string HTML content.
877      */
878     public function get_timer_html() {
879         return '<div id="quiz-timer">' . get_string('timeleft', 'quiz') .
880                 ' <span id="quiz-time-left"></span></div>';
881     }
883     /**
884      * Generate the HTML that displayes the question in its current state, with
885      * the appropriate display options.
886      *
887      * @param int $id the id of a question in this quiz attempt.
888      * @param bool $reviewing is the being printed on an attempt or a review page.
889      * @param string $thispageurl the URL of the page this question is being printed on.
890      * @return string HTML for the question in its current state.
891      */
892     public function render_question($slot, $reviewing, $thispageurl = '') {
893         return $this->quba->render_question($slot,
894                 $this->get_display_options($reviewing),
895                 $this->quba->get_question($slot)->_number);
896     }
898     /**
899      * Like {@link render_question()} but displays the question at the past step
900      * indicated by $seq, rather than showing the latest step.
901      *
902      * @param int $id the id of a question in this quiz attempt.
903      * @param int $seq the seq number of the past state to display.
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      * @return string HTML for the question in its current state.
907      */
908     public function render_question_at_step($slot, $seq, $reviewing, $thispageurl = '') {
909         return $this->quba->render_question_at_step($slot, $seq,
910                 $this->get_display_options($reviewing),
911                 $this->quba->get_question($slot)->_number);
912     }
914     /**
915      * Wrapper round print_question from lib/questionlib.php.
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      */
921     public function render_question_for_commenting($slot) {
922         $options = $this->get_display_options(true);
923         $options->hide_all_feedback();
924         $options->manualcomment = question_display_options::EDITABLE;
925         return $this->quba->render_question($slot, $options, $this->quba->get_question($slot)->_number);
926     }
928     /**
929      * Check wheter access should be allowed to a particular file.
930      *
931      * @param int $id the id of a question in this quiz attempt.
932      * @param bool $reviewing is the being printed on an attempt or a review page.
933      * @param string $thispageurl the URL of the page this question is being printed on.
934      * @return string HTML for the question in its current state.
935      */
936     public function check_file_access($slot, $reviewing, $contextid, $component,
937             $filearea, $args, $forcedownload) {
938         return $this->quba->check_file_access($slot, $this->get_display_options($reviewing),
939                 $component, $filearea, $args, $forcedownload);
940     }
942     /**
943      * Triggers the sending of the notification emails at the end of this attempt.
944      */
945     public function quiz_send_notification_emails() {
946         quiz_send_notification_emails($this->get_course(), $this->get_quiz(), $this->attempt,
947                 $this->quizobj->get_context(), $this->get_cm());
948     }
950     /**
951      * Get the navigation panel object for this attempt.
952      *
953      * @param $panelclass The type of panel, quiz_attempt_nav_panel or quiz_review_nav_panel
954      * @param $page the current page number.
955      * @param $showall whether we are showing the whole quiz on one page. (Used by review.php)
956      * @return quiz_nav_panel_base the requested object.
957      */
958     public function get_navigation_panel($panelclass, $page, $showall = false) {
959         $panel = new $panelclass($this, $this->get_display_options(true), $page, $showall);
960         return $panel->get_contents();
961     }
963     /**
964      * Given a URL containing attempt={this attempt id}, return an array of variant URLs
965      * @param $url a URL.
966      * @return string HTML fragment. Comma-separated list of links to the other
967      * attempts with the attempt number as the link text. The curent attempt is
968      * included but is not a link.
969      */
970     public function links_to_other_attempts($url) {
971         $search = '/\battempt=' . $this->attempt->id . '\b/';
972         $attempts = quiz_get_user_attempts($this->get_quiz()->id, $this->attempt->userid, 'all');
973         if (count($attempts) <= 1) {
974             return false;
975         }
976         $attemptlist = array();
977         foreach ($attempts as $at) {
978             if ($at->id == $this->attempt->id) {
979                 $attemptlist[] = '<strong>' . $at->attempt . '</strong>';
980             } else {
981                 $changedurl = preg_replace($search, 'attempt=' . $at->id, $url);
982                 $attemptlist[] = '<a href="' . s($changedurl) . '">' . $at->attempt . '</a>';
983             }
984         }
985         return implode(', ', $attemptlist);
986     }
988     // Methods for processing ==================================================
990     /**
991      * Process all the actions that were submitted as part of the current request.
992      *
993      * @param int $timestamp the timestamp that should be stored as the modifed
994      * time in the database for these actions. If null, will use the current time.
995      */
996     public function process_all_actions($timestamp) {
997         global $DB;
998         $this->quba->process_all_actions($timestamp);
999         question_engine::save_questions_usage_by_activity($this->quba);
1001         $this->attempt->timemodified = $timestamp;
1002         if ($this->attempt->timefinish) {
1003             $this->attempt->sumgrades = $this->quba->get_total_mark();
1004         }
1005         if (!$DB->update_record('quiz_attempts', $this->attempt)) {
1006             throw new moodle_quiz_exception($this->get_quizobj(), 'saveattemptfailed');
1007         }
1008         if (!$this->is_preview() && $this->attempt->timefinish) {
1009             quiz_save_best_grade($this->get_quiz(), $this->get_userid());
1010         }
1011     }
1013     /**
1014      * Update the flagged state for all question_attempts in this usage, if their
1015      * flagged state was changed in the request.
1016      */
1017     public function save_question_flags() {
1018         $this->quba->update_question_flags();
1019         question_engine::save_questions_usage_by_activity($this->quba);
1020     }
1022     public function finish_attempt($timestamp) {
1023         global $DB;
1024         $this->quba->process_all_actions($timestamp);
1025         $this->quba->finish_all_questions($timestamp);
1027         question_engine::save_questions_usage_by_activity($this->quba);
1029         $this->attempt->timemodified = $timestamp;
1030         $this->attempt->timefinish = $timestamp;
1031         $this->attempt->sumgrades = $this->quba->get_total_mark();
1032         if (!$DB->update_record('quiz_attempts', $this->attempt)) {
1033             throw new moodle_quiz_exception($this->get_quizobj(), 'saveattemptfailed');
1034         }
1036         if (!$this->is_preview()) {
1037             quiz_save_best_grade($this->get_quiz());
1038             $this->quiz_send_notification_emails();
1039         }
1040     }
1042     /**
1043      * Print the fields of the comment form for questions in this attempt.
1044      * @param $slot which question to output the fields for.
1045      * @param $prefix Prefix to add to all field names.
1046      */
1047     public function question_print_comment_fields($slot, $prefix) {
1048         // Work out a nice title.
1049         $student = get_record('user', 'id', $this->get_userid());
1050         $a = new object();
1051         $a->fullname = fullname($student, true);
1052         $a->attempt = $this->get_attempt_number();
1054         question_print_comment_fields($this->quba->get_question_attempt($slot),
1055                 $prefix, $this->get_display_options(true)->markdp,
1056                 get_string('gradingattempt', 'quiz_grading', $a));
1057     }
1059     // Private methods =====================================================================
1061     /**
1062      * Get a URL for a particular question on a particular page of the quiz.
1063      * Used by {@link attempt_url()} and {@link review_url()}.
1064      *
1065      * @param string $script. Used in the URL like /mod/quiz/$script.php
1066      * @param int $slot identifies the specific question on the page to jump to. 0 to just use the $page parameter.
1067      * @param int $page -1 to look up the page number from the slot, otherwise the page number to go to.
1068      * @param bool $showall if true, return a URL with showall=1, and not page number
1069      * @param int $thispage the page we are currently on. Links to questions on this
1070      *      page will just be a fragment #q123. -1 to disable this.
1071      * @return The requested URL.
1072      */
1073     protected function page_and_question_url($script, $slot, $page, $showall, $thispage) {
1074         // Fix up $page
1075         if ($page == -1) {
1076             if (!is_null($slot) && !$showall) {
1077                 $page = $this->quba->get_question($slot)->_page;
1078             } else {
1079                 $page = 0;
1080             }
1081         }
1083         if ($showall) {
1084             $page = 0;
1085         }
1087         // Add a fragment to scroll down to the question.
1088         $fragment = '';
1089         if (!is_null($slot)) {
1090             if ($slot == reset($this->pagelayout[$page])) {
1091                 // First question on page, go to top.
1092                 $fragment = '#';
1093             } else {
1094                 $fragment = '#q' . $slot;
1095             }
1096         }
1098         // Work out the correct start to the URL.
1099         if ($thispage == $page) {
1100             return new moodle_url($fragment);
1102         } else {
1103             $url = new moodle_url('/mod/quiz/' . $script . '.php' . $fragment,
1104                     array('attempt' => $this->attempt->id));
1105             if ($showall) {
1106                 $url->param('showall', 1);
1107             } else if ($page > 0) {
1108                 $url->param('page', $page);
1109             }
1110             return $url;
1111         }
1112     }
1116 /**
1117  * Represents the navigation panel, and builds a {@link block_contents} to allow
1118  * it to be output.
1119  *
1120  * @copyright  2008 Tim Hunt
1121  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1122  * @since      Moodle 2.0
1123  */
1124 abstract class quiz_nav_panel_base {
1125     /** @var quiz_attempt */
1126     protected $attemptobj;
1127     /** @var question_display_options */
1128     protected $options;
1129     /** @var integer */
1130     protected $page;
1131     /** @var boolean */
1132     protected $showall;
1134     public function __construct(quiz_attempt $attemptobj,
1135             question_display_options $options, $page, $showall) {
1136         $this->attemptobj = $attemptobj;
1137         $this->options = $options;
1138         $this->page = $page;
1139         $this->showall = $showall;
1140     }
1142     protected function get_question_buttons() {
1143         $html = '<div class="qn_buttons">' . "\n";
1144         foreach ($this->attemptobj->get_slots() as $slot) {
1145             $qa = $this->attemptobj->get_question_attempt($slot);
1146             $showcorrectness = $this->options->correctness && $qa->has_marks();
1147             $html .= $this->get_question_button($qa, $qa->get_question()->_number,
1148                     $showcorrectness) . "\n";
1149         }
1150         $html .= "</div>\n";
1151         return $html;
1152     }
1154     protected function get_button_id(question_attempt $qa) {
1155         // The id to put on the button element in the HTML.
1156         return 'quiznavbutton' . $qa->get_slot();
1157     }
1159     protected function get_question_button(question_attempt $qa, $number, $showcorrectness) {
1160         $attributes = $this->get_attributes($qa, $showcorrectness);
1162         if (is_numeric($number)) {
1163             $qnostring = 'questionnonav';
1164         } else {
1165             $qnostring = 'questionnonavinfo';
1166         }
1168         $a = new stdClass();
1169         $a->number = $number;
1170         $a->attributes = implode(' ', $attributes);
1172         return '<a href="' . $this->get_question_url($qa->get_slot()) .
1173                 '" class="qnbutton ' . implode(' ', array_keys($attributes)) .
1174                 '" id="' . $this->get_button_id($qa) . '" title="' .
1175                 $qa->get_state_string($showcorrectness) . '">' .
1176                 '<span class="thispageholder"></span><span class="trafficlight"></span>' .
1177                 get_string($qnostring, 'quiz', $a) . '</a>';
1178     }
1180     /**
1181      * @param question_attempt $qa
1182      * @param bool $showcorrectness
1183      * @return array class name => descriptive string.
1184      */
1185     protected function get_attributes(question_attempt $qa, $showcorrectness) {
1186         // The current status of the question.
1187         $attributes = array();
1189         // On the current page?
1190         if ($qa->get_question()->_page == $this->page) {
1191             $attributes['thispage'] = get_string('onthispage', 'quiz');
1192         }
1194         // Question state.
1195         $stateclass = $qa->get_state()->get_state_class($showcorrectness);
1196         if (!$showcorrectness && $stateclass == 'notanswered') {
1197             $stateclass = 'complete';
1198         }
1199         $attributes[$stateclass] = $qa->get_state_string($showcorrectness);
1201         // Flagged?
1202         if ($qa->is_flagged()) {
1203             $attributes['flagged'] = '<span class="flagstate">' .
1204                     get_string('flagged', 'question') . '</span>';
1205         } else {
1206             $attributes[''] = '<span class="flagstate"></span>';
1207         }
1209         return $attributes;
1210     }
1212     protected function get_before_button_bits() {
1213         return '';
1214     }
1216     abstract protected function get_end_bits();
1218     abstract protected function get_question_url($slot);
1220     protected function get_user_picture() {
1221         global $DB, $OUTPUT;
1222         $user = $DB->get_record('user', array('id' => $this->attemptobj->get_userid()));
1223         $output = '';
1224         $output .= '<div id="user-picture" class="clearfix">';
1225         $output .= $OUTPUT->user_picture($user, array('courseid'=>$this->attemptobj->get_courseid()));
1226         $output .= ' ' . fullname($user);
1227         $output .= '</div>';
1228         return $output;
1229     }
1231     public function get_contents() {
1232         global $PAGE;
1233         $PAGE->requires->js_init_call('M.mod_quiz.nav.init', null, false, quiz_get_js_module());
1235         $content = '';
1236         if (!empty($this->attemptobj->get_quiz()->showuserpicture)) {
1237             $content .= $this->get_user_picture() . "\n";
1238         }
1239         $content .= $this->get_before_button_bits();
1240         $content .= $this->get_question_buttons() . "\n";
1241         $content .= '<div class="othernav">' . "\n" . $this->get_end_bits() . "\n</div>\n";
1243         $bc = new block_contents();
1244         $bc->id = 'quiznavigation';
1245         $bc->title = get_string('quiznavigation', 'quiz');
1246         $bc->content = $content;
1247         return $bc;
1248     }
1252 /**
1253  * Specialisation of {@link quiz_nav_panel_base} for the attempt quiz page.
1254  *
1255  * @copyright  2008 Tim Hunt
1256  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1257  * @since      Moodle 2.0
1258  */
1259 class quiz_attempt_nav_panel extends quiz_nav_panel_base {
1260     protected function get_question_url($slot) {
1261         return $this->attemptobj->attempt_url($slot, -1, $this->page);
1262     }
1264     protected function get_before_button_bits() {
1265         return '<div id="quiznojswarning">' . get_string('navnojswarning', 'quiz') . "</div>\n";
1266     }
1268     protected function get_end_bits() {
1269         global $PAGE;
1270         $output = '';
1271         $output .= '<a href="' . s($this->attemptobj->summary_url()) . '" id="endtestlink">' . get_string('endtest', 'quiz') . '</a>';
1272         $output .= $this->attemptobj->get_timer_html();
1273         return $output;
1274     }
1278 /**
1279  * Specialisation of {@link quiz_nav_panel_base} for the review quiz page.
1280  *
1281  * @copyright  2008 Tim Hunt
1282  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1283  * @since      Moodle 2.0
1284  */
1285 class quiz_review_nav_panel extends quiz_nav_panel_base {
1286     protected function get_question_url($slot) {
1287         return $this->attemptobj->review_url($slot, -1, $this->showall, $this->page);
1288     }
1290     protected function get_end_bits() {
1291         $html = '';
1292         if ($this->attemptobj->get_num_pages() > 1) {
1293             if ($this->showall) {
1294                 $html .= '<a href="' . $this->attemptobj->review_url(null, 0, false) . '">' . get_string('showeachpage', 'quiz') . '</a>';
1295             } else {
1296                 $html .= '<a href="' . $this->attemptobj->review_url(null, 0, true) . '">' . get_string('showall', 'quiz') . '</a>';
1297             }
1298         }
1299         $accessmanager = $this->attemptobj->get_access_manager(time());
1300         $html .= $accessmanager->print_finish_review_link($this->attemptobj->is_preview_user(), true);
1301         return $html;
1302     }