3 // This file is part of Moodle - http://moodle.org/
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.
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.
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/>.
19 * Back-end code for handling data about quizzes and the current user's attempt.
21 * There are classes for loading all the information about a quiz and attempts,
22 * and for displaying the navigation panel.
26 * @copyright 2008 onwards Tim Hunt
27 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
31 defined('MOODLE_INTERNAL') || die();
35 * Class for quiz exceptions. Just saves a couple of arguments on the
36 * constructor for a moodle_exception.
38 * @copyright 2008 Tim Hunt
39 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
42 class moodle_quiz_exception extends moodle_exception {
43 function __construct($quizobj, $errorcode, $a = NULL, $link = '', $debuginfo = null) {
45 $link = $quizobj->view_url();
47 parent::__construct($errorcode, 'quiz', $link, $a, $debuginfo);
53 * A class encapsulating a quiz and the questions it contains, and making the
54 * information available to scripts like view.php.
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
60 * @copyright 2008 Tim Hunt
61 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
65 // Fields initialised in the constructor.
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 =========================================================================
79 * Constructor, assuming we already have the necessary data loaded.
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.
86 function __construct($quiz, $cm, $course, $getcontext = true) {
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);
94 $this->questionids = explode(',', quiz_questions_in_quiz($this->quiz->questions));
98 * Static function to create a new quiz object for a specific user.
100 * @param int $quizid the the quiz id.
101 * @param int $userid the the userid.
102 * @return quiz the new quiz object
104 static public function create($quizid, $userid) {
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);
117 // Functions for loading more data =====================================================
120 * Load just basic information about all the questions in this quiz.
122 public function preload_questions() {
123 if (empty($this->questionids)) {
124 throw new moodle_quiz_exception($this, 'noquestions', $this->edit_url());
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));
133 * Fully load some or all of the questions for this quiz. You must call {@link preload_questions()} first.
135 * @param array $questionids question ids of the questions to load. null for all.
137 public function load_questions($questionids = null) {
138 if (is_null($questionids)) {
139 $questionids = $this->questionids;
141 $questionstoprocess = array();
142 foreach ($questionids as $id) {
143 if (array_key_exists($id, $this->questions)) {
144 $questionstoprocess[$id] = $this->questions[$id];
147 get_question_options($questionstoprocess);
150 // Simple getters ======================================================================
151 /** @return int the course id. */
152 public function get_courseid() {
153 return $this->course->id;
156 /** @return object the row of the course table. */
157 public function get_course() {
158 return $this->course;
161 /** @return int the quiz id. */
162 public function get_quizid() {
163 return $this->quiz->id;
166 /** @return object the row of the quiz table. */
167 public function get_quiz() {
171 /** @return string the name of this quiz. */
172 public function get_quiz_name() {
173 return $this->quiz->name;
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;
181 /** @return int the course_module id. */
182 public function get_cmid() {
183 return $this->cm->id;
186 /** @return object the course_module object. */
187 public function get_cm() {
191 /** @return object the module context for this quiz. */
192 public function get_context() {
193 return $this->context;
197 * @return bool wether the current user is someone who previews the quiz,
198 * rather than attempting it.
200 public function is_preview_user() {
201 if (is_null($this->ispreviewuser)) {
202 $this->ispreviewuser = has_capability('mod/quiz:preview', $this->context);
204 return $this->ispreviewuser;
208 * @return whether any questions have been added to this quiz.
210 public function has_questions() {
211 return !empty($this->questionids);
215 * @param int $id the question id.
216 * @return object the question object with that id.
218 public function get_question($id) {
219 return $this->questions[$id];
223 * @param array $questionids question ids of the questions to load. null for all.
225 public function get_questions($questionids = null) {
226 if (is_null($questionids)) {
227 $questionids = $this->questionids;
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());
234 $questions[$id] = $this->questions[$id];
235 $this->ensure_question_loaded($id);
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.
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));
249 return $this->accessmanager;
253 * Wrapper round the has_capability funciton that automatically passes in the quiz context.
255 public function has_capability($capability, $userid = NULL, $doanything = true) {
256 return has_capability($capability, $this->context, $userid, $doanything);
260 * Wrapper round the require_capability funciton that automatically passes in the quiz context.
262 public function require_capability($capability, $userid = NULL, $doanything = true) {
263 return require_capability($capability, $this->context, $userid, $doanything);
266 // URLs related to this attempt ========================================================
268 * @return string the URL of this quiz's view page.
270 public function view_url() {
272 return $CFG->wwwroot . '/mod/quiz/view.php?id=' . $this->cm->id;
276 * @return string the URL of this quiz's edit page.
278 public function edit_url() {
280 return $CFG->wwwroot . '/mod/quiz/edit.php?cmid=' . $this->cm->id;
284 * @param int $attemptid the id of an attempt.
285 * @return string the URL of that attempt.
287 public function attempt_url($attemptid) {
289 return $CFG->wwwroot . '/mod/quiz/attempt.php?attempt=' . $attemptid;
293 * @return string the URL of this quiz's edit page. Needs to be POSTed to with a cmid parameter.
295 public function start_attempt_url() {
296 return new moodle_url('/mod/quiz/startattempt.php',
297 array('cmid' => $this->cm->id, 'sesskey' => sesskey()));
301 * @param int $attemptid the id of an attempt.
302 * @return string the URL of the review of that attempt.
304 public function review_url($attemptid) {
305 return new moodle_url('/mod/quiz/review.php', array('attempt' => $attemptid));
308 // Bits of content =====================================================================
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
315 public function navigation($title) {
317 $PAGE->navbar->add($title);
321 // Private methods =====================================================================
323 * Check that the definition of a particular question is loaded, and if not throw an exception.
324 * @param $id a questionid.
326 protected function ensure_question_loaded($id) {
327 if (isset($this->questions[$id]->_partiallyloaded)) {
328 throw new moodle_quiz_exception($this, 'questionnotloaded', $id);
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.
338 * @copyright 2008 Tim Hunt
339 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
343 // Fields initialised in the constructor.
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 =========================================================================
354 * Constructor assuming we already have the necessary data loaded.
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.
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();
370 * Used by {create()} and {create_from_usage_id()}.
371 * @param array $conditions passed to $DB->get_record('quiz_attempts', $conditions).
373 static protected function create_helper($conditions) {
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);
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);
394 * Static function to create a new quiz_attempt object given an attemptid.
396 * @param int $attemptid the attempt id.
397 * @return quiz_attempt the new quiz_attempt object
399 static public function create($attemptid) {
400 return self::create_helper(array('id' => $attemptid));
404 * Static function to create a new quiz_attempt object given a usage id.
406 * @param int $usageid the attempt usage id.
407 * @return quiz_attempt the new quiz_attempt object
409 static public function create_from_usage_id($usageid) {
410 return self::create_helper(array('uniqueid' => $usageid));
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);
424 // File the ids into the arrays.
425 $this->pagelayout = array();
426 foreach ($pagelayouts as $page => $pagelayout) {
427 $pagelayout = trim($pagelayout, ',');
428 if ($pagelayout == '') {
431 $this->pagelayout[$page] = explode(',', $pagelayout);
435 // Number the questions.
436 private function number_questions() {
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;
445 $question->_number = get_string('infoshort', 'quiz');
447 $question->_page = $page;
452 // Simple getters ======================================================================
453 public function get_quiz() {
454 return $this->quizobj->get_quiz();
457 public function get_quizobj() {
458 return $this->quizobj;
461 /** @return int the course id. */
462 public function get_courseid() {
463 return $this->quizobj->get_courseid();
466 /** @return int the course id. */
467 public function get_course() {
468 return $this->quizobj->get_course();
471 /** @return int the quiz id. */
472 public function get_quizid() {
473 return $this->quizobj->get_quizid();
476 /** @return string the name of this quiz. */
477 public function get_quiz_name() {
478 return $this->quizobj->get_quiz_name();
481 /** @return object the course_module object. */
482 public function get_cm() {
483 return $this->quizobj->get_cm();
486 /** @return object the course_module object. */
487 public function get_cmid() {
488 return $this->quizobj->get_cmid();
492 * @return bool wether the current user is someone who previews the quiz,
493 * rather than attempting it.
495 public function is_preview_user() {
496 return $this->quizobj->is_preview_user();
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();
504 /** @return int number fo pages in this quiz. */
505 public function get_num_pages() {
506 return count($this->pagelayout);
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.
513 public function get_access_manager($timenow) {
514 return $this->quizobj->get_access_manager($timenow);
517 /** @return int the attempt id. */
518 public function get_attemptid() {
519 return $this->attempt->id;
522 /** @return int the attempt unique id. */
523 public function get_uniqueid() {
524 return $this->attempt->uniqueid;
527 /** @return object the row from the quiz_attempts table. */
528 public function get_attempt() {
529 return $this->attempt;
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;
537 /** @return int the id of the user this attempt belongs to. */
538 public function get_userid() {
539 return $this->attempt->userid;
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;
547 /** @return bool whether this attempt is a preview attempt. */
548 public function is_preview() {
549 return $this->attempt->preview;
553 * Is this a student dealing with their own attempt/teacher previewing,
554 * or someone with 'mod/quiz:viewreports' reviewing someone elses attempt.
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.
559 public function is_own_attempt() {
561 return $this->attempt->userid == $USER->id &&
562 (!$this->is_preview_user() || $this->attempt->preview);
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.
570 public function is_review_allowed() {
571 if (!$this->has_capability('mod/quiz:viewreports')) {
575 $cm = $this->get_cm();
576 if ($this->has_capability('moodle/site:accessallgroups') ||
577 groups_get_activity_groupmode($cm) != SEPARATEGROUPS) {
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));
589 * Get the overall feedback corresponding to a particular mark.
590 * @param $grade a particular grade.
592 public function get_overall_feedback($grade) {
593 return quiz_feedback_for_grade($grade, $this->get_quiz(),
594 $this->quizobj->get_context());
598 * Wrapper round the has_capability funciton that automatically passes in the quiz context.
600 public function has_capability($capability, $userid = NULL, $doanything = true) {
601 return $this->quizobj->has_capability($capability, $userid, $doanything);
605 * Wrapper round the require_capability funciton that automatically passes in the quiz context.
607 public function require_capability($capability, $userid = NULL, $doanything = true) {
608 return $this->quizobj->require_capability($capability, $userid, $doanything);
612 * Check the appropriate capability to see whether this user may review their own attempt.
613 * If not, prints an error.
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');
620 $this->require_capability('mod/quiz:reviewmyattempts');
626 * @return int one of the mod_quiz_display_options::DURING,
627 * IMMEDIATELY_AFTER, LATER_WHILE_OPEN or AFTER_CLOSE constants.
629 public function get_attempt_state() {
630 return quiz_attempt_state($this->get_quiz(), $this->attempt);
634 * Wrapper that the correct mod_quiz_display_options for this quiz at the
637 * @return question_display_options the render options for this user on this attempt.
639 public function get_display_options($reviewing) {
641 if (is_null($this->reviewoptions)) {
642 $this->reviewoptions = quiz_get_review_options($this->get_quiz(),
643 $this->attempt, $this->quizobj->get_context());
645 return $this->reviewoptions;
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());
656 * @param int $page page number
657 * @return bool true if this is the last page of the quiz.
659 public function is_last_page($page) {
660 return $page == count($this->pagelayout) - 1;
664 * Return the list of question ids for either a given page of the quiz, or for the
667 * @param mixed $page string 'all' or integer page number.
668 * @return array the reqested list of question ids.
670 public function get_slots($page = 'all') {
671 if ($page === 'all') {
673 foreach ($this->pagelayout as $numbersonpage) {
674 $numbers = array_merge($numbers, $numbersonpage);
678 return $this->pagelayout[$page];
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
687 public function get_question_attempt($slot) {
688 return $this->quba->get_question_attempt($slot);
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.
696 public function is_real_question($slot) {
697 return $this->quba->get_question($slot)->length != 0;
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.
705 public function is_question_flagged($slot) {
706 return $this->quba->get_question_attempt($slot)->is_flagged();
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.
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.
716 public function get_question_number($slot) {
717 return $this->quba->get_question($slot)->_number;
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.
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.
727 public function get_question_name($slot) {
728 return $this->quba->get_question($slot)->name;
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.
735 * @param int $slot the number used to identify this question within this attempt.
736 * @param bool $showcorrectness Whether right/partial/wrong states should
738 * @return string the formatted grade, to the number of decimal places specified by the quiz.
740 public function get_question_status($slot, $showcorrectness) {
741 return $this->quba->get_question_state_string($slot, $showcorrectness);
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.
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.
752 public function get_question_mark($slot) {
753 return quiz_format_question_grade($this->get_quiz(), $this->quba->get_question_mark($slot));
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.
761 public function get_question_action_time($slot) {
762 return $this->quba->get_question_action_time($slot);
765 // URLs related to this attempt ========================================================
767 * @return string quiz view url.
769 public function view_url() {
770 return $this->quizobj->view_url();
774 * @return string the URL of this quiz's edit page. Needs to be POSTed to with a cmid parameter.
776 public function start_attempt_url() {
777 return $this->quizobj->start_attempt_url();
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.
790 public function attempt_url($slot = null, $page = -1, $thispage = -1) {
791 return $this->page_and_question_url('attempt', $slot, $page, false, $thispage);
795 * @return string the URL of this quiz's summary page.
797 public function summary_url() {
798 return new moodle_url('/mod/quiz/summary.php', array('attempt' => $this->attempt->id));
802 * @return string the URL of this quiz's summary page.
804 public function processattempt_url() {
805 return new moodle_url('/mod/quiz/processattempt.php');
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.
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);
822 // Bits of content =====================================================================
825 * Initialise the JS etc. required all the questions on a page..
826 * @param mixed $page a page number, or 'all'.
828 public function get_html_head_contributions($page = 'all', $showall = false) {
833 foreach ($this->get_slots($page) as $slot) {
834 $result .= $this->quba->render_question_head_html($slot);
836 $result .= question_engine::initialise_js();
841 * Initialise the JS etc. required by one question.
842 * @param int $questionid the question id.
844 public function get_question_html_head_contributions($slot) {
845 return $this->quba->render_question_head_html($slot) .
846 question_engine::initialise_js();
850 * Print the HTML for the start new preview button.
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();
861 * Return the HTML of the quiz timer.
862 * @return string HTML content.
864 public function get_timer_html() {
865 return '<div id="quiz-timer">' . get_string('timeleft', 'quiz') .
866 ' <span id="quiz-time-left"></span></div>';
870 * Generate the HTML that displayes the question in its current state, with
871 * the appropriate display options.
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.
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);
885 * Like {@link render_question()} but displays the question at the past step
886 * indicated by $seq, rather than showing the latest step.
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.
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);
901 * Wrapper round print_question from lib/questionlib.php.
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.
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);
915 * Check wheter access should be allowed to a particular file.
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.
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);
929 * Triggers the sending of the notification emails at the end of this attempt.
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());
937 * Get the navigation panel object for this attempt.
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.
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();
950 * Given a URL containing attempt={this attempt id}, return an array of variant URLs
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.
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) {
962 $attemptlist = array();
963 foreach ($attempts as $at) {
964 if ($at->id == $this->attempt->id) {
965 $attemptlist[] = '<strong>' . $at->attempt . '</strong>';
967 $changedurl = preg_replace($search, 'attempt=' . $at->id, $url);
968 $attemptlist[] = '<a href="' . s($changedurl) . '">' . $at->attempt . '</a>';
971 return implode(', ', $attemptlist);
974 // Methods for processing ==================================================
977 * Process all the actions that were submitted as part of the current request.
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.
982 public function process_all_actions($timestamp) {
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();
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());
999 * Update the flagged state for all question_attempts in this usage, if their
1000 * flagged state was changed in the request.
1002 public function save_question_flags() {
1003 $this->quba->update_question_flags();
1004 question_engine::save_questions_usage_by_activity($this->quba);
1007 public function finish_attempt($timestamp) {
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();
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.
1030 public function question_print_comment_fields($slot, $prefix) {
1031 // Work out a nice title.
1032 $student = get_record('user', 'id', $this->get_userid());
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));
1042 // Private methods =====================================================================
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()}.
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.
1056 protected function page_and_question_url($script, $slot, $page, $showall, $thispage) {
1059 if (!is_null($slot) && !$showall) {
1060 $page = $this->quba->get_question($slot)->_page;
1070 // Add a fragment to scroll down to the question.
1072 if (!is_null($slot)) {
1073 if ($slot == reset($this->pagelayout[$page])) {
1074 // First question on page, go to top.
1077 $fragment = '#q' . $slot;
1081 // Work out the correct start to the URL.
1082 if ($thispage == $page) {
1083 return new moodle_url($fragment);
1086 $url = new moodle_url('/mod/quiz/' . $script . '.php' . $fragment,
1087 array('attempt' => $this->attempt->id));
1089 $url->param('showall', 1);
1090 } else if ($page > 0) {
1091 $url->param('page', $page);
1100 * Represents the navigation panel, and builds a {@link block_contents} to allow
1103 * @copyright 2008 Tim Hunt
1104 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1107 abstract class quiz_nav_panel_base {
1108 /** @var quiz_attempt */
1109 protected $attemptobj;
1110 /** @var question_display_options */
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;
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";
1133 $html .= "</div>\n";
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();
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';
1148 $qnostring = 'questionnonavinfo';
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>';
1164 * @param question_attempt $qa
1165 * @param bool $showcorrectness
1166 * @return array class name => descriptive string.
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');
1178 $stateclass = $qa->get_state()->get_state_class($showcorrectness);
1179 if (!$showcorrectness && $stateclass == 'notanswered') {
1180 $stateclass = 'complete';
1182 $attributes[$stateclass] = $qa->get_state_string($showcorrectness);
1185 if ($qa->is_flagged()) {
1186 $attributes['flagged'] = '<span class="flagstate">' .
1187 get_string('flagged', 'question') . '</span>';
1189 $attributes[''] = '<span class="flagstate"></span>';
1195 protected function get_before_button_bits() {
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()));
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>';
1214 public function get_contents() {
1216 $PAGE->requires->js_init_call('M.mod_quiz.nav.init', null, false, quiz_get_js_module());
1219 if (!empty($this->attemptobj->get_quiz()->showuserpicture)) {
1220 $content .= $this->get_user_picture() . "\n";
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;
1236 * Specialisation of {@link quiz_nav_panel_base} for the attempt quiz page.
1238 * @copyright 2008 Tim Hunt
1239 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
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);
1247 protected function get_before_button_bits() {
1248 return '<div id="quiznojswarning">' . get_string('navnojswarning', 'quiz') . "</div>\n";
1251 protected function get_end_bits() {
1254 $output .= '<a href="' . s($this->attemptobj->summary_url()) . '" id="endtestlink">' . get_string('endtest', 'quiz') . '</a>';
1255 $output .= $this->attemptobj->get_timer_html();
1262 * Specialisation of {@link quiz_nav_panel_base} for the review quiz page.
1264 * @copyright 2008 Tim Hunt
1265 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
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);
1273 protected function get_end_bits() {
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>';
1279 $html .= '<a href="' . $this->attemptobj->review_url(null, 0, true) . '">' . get_string('showall', 'quiz') . '</a>';
1282 $accessmanager = $this->attemptobj->get_access_manager(time());
1283 $html .= $accessmanager->print_finish_review_link($this->attemptobj->is_preview_user(), true);