MDL-14741: load editor automatically, from footer/after page is loaded.
[moodle.git] / mod / quiz / attemptlib.php
CommitLineData
36e413e3 1<?php
36e413e3 2/**
3 * This class handles loading all the information about a quiz attempt into memory,
4 * and making it available for attemtp.php, summary.php and review.php.
5 * Initially, it only loads a minimal amout of information about each attempt - loading
6 * extra information only when necessary or when asked. The class tracks which questions
7 * are loaded.
b10c38a3 8 *//** */
36e413e3 9
66d07f81 10if (!defined('MOODLE_INTERNAL')) {
11 die('Direct access to this script is forbidden.'); /// It must be included from a Moodle page.
12}
36e413e3 13
14/**
b10c38a3 15 * Class for quiz exceptions. Just saves a couple of arguments on the
16 * constructor for a moodle_exception.
36e413e3 17 */
18class moodle_quiz_exception extends moodle_exception {
19 function __construct($quizobj, $errorcode, $a = NULL, $link = '', $debuginfo = null) {
20 if (!$link) {
21 $link = $quizobj->view_url();
22 }
23 parent::__construct($errorcode, 'quiz', $link, $a, $debuginfo);
24 }
25}
26
b10c38a3 27/**
28 * A base class for holding and accessing information about a quiz and its questions,
29 * before details of a particular attempt are loaded.
30 */
36e413e3 31class quiz {
32 // Fields initialised in the constructor.
33 protected $course;
34 protected $cm;
35 protected $quiz;
36 protected $context;
78e7a3dd 37 protected $questionids; // All question ids in order that they appear in the quiz.
38 protected $pagequestionids; // array page no => array of questionids on the page in order.
36e413e3 39
40 // Fields set later if that data is needed.
78e7a3dd 41 protected $questions = null;
36e413e3 42 protected $accessmanager = null;
b10c38a3 43 protected $ispreviewuser = null;
36e413e3 44
45 // Constructor =========================================================================
b10c38a3 46 /**
47 * Constructor, assuming we already have the necessary data loaded.
48 *
49 * @param object $quiz the row from the quiz table.
50 * @param object $cm the course_module object for this quiz.
51 * @param object $course the row from the course table for the course we belong to.
52 */
36e413e3 53 function __construct($quiz, $cm, $course) {
54 $this->quiz = $quiz;
55 $this->cm = $cm;
56 $this->course = $course;
57 $this->context = get_context_instance(CONTEXT_MODULE, $cm->id);
78e7a3dd 58 $this->determine_layout();
36e413e3 59 }
60
61 // Functions for loading more data =====================================================
62 public function load_questions_on_page($page) {
78e7a3dd 63 $this->load_questions($this->pagequestionids[$page]);
64 }
65
66 public function preload_questions() {
67 if (empty($this->questionids)) {
68 throw new moodle_quiz_exception($this, 'noquestions', $this->edit_url());
69 }
70 $this->questions = question_preload_questions($this->questionids,
71 'qqi.grade AS maxgrade, qqi.id AS instance',
72 '{quiz_question_instances} qqi ON qqi.quiz = :quizid AND q.id = qqi.question',
73 array('quizid' => $this->quiz->id));
74 $this->number_questions();
36e413e3 75 }
76
77 /**
78 * Load some or all of the queestions for this quiz.
79 *
78e7a3dd 80 * @param array $questionids question ids of the questions to load. null for all.
36e413e3 81 */
78e7a3dd 82 public function load_questions($questionids = null) {
83 if (is_null($questionids)) {
84 $questionids = $this->questionids;
85 }
86 $questionstoprocess = array();
87 foreach ($questionids as $id) {
88 $questionstoprocess[$id] = $this->questions[$id];
36e413e3 89 }
78e7a3dd 90 if (!get_question_options($questionstoprocess)) {
91 throw new moodle_quiz_exception($this, 'loadingquestionsfailed', implode(', ', $questionids));
36e413e3 92 }
36e413e3 93 }
94
95 // Simple getters ======================================================================
b10c38a3 96 /** @return integer the course id. */
36e413e3 97 public function get_courseid() {
98 return $this->course->id;
99 }
100
78e7a3dd 101 /** @return object the row of the course table. */
102 public function get_course() {
103 return $this->course;
104 }
105
b10c38a3 106 /** @return integer the quiz id. */
36e413e3 107 public function get_quizid() {
108 return $this->quiz->id;
109 }
110
b10c38a3 111 /** @return object the row of the quiz table. */
36e413e3 112 public function get_quiz() {
113 return $this->quiz;
114 }
115
b10c38a3 116 /** @return string the name of this quiz. */
36e413e3 117 public function get_quiz_name() {
118 return $this->quiz->name;
119 }
120
b9b3aa94 121 /** @return integer the number of attempts allowed at this quiz (0 = infinite). */
122 public function get_num_attempts_allowed() {
123 return $this->quiz->attempts;
124 }
125
b10c38a3 126 /** @return integer the course_module id. */
36e413e3 127 public function get_cmid() {
128 return $this->cm->id;
129 }
130
b10c38a3 131 /** @return object the course_module object. */
36e413e3 132 public function get_cm() {
133 return $this->cm;
134 }
135
b10c38a3 136 /**
137 * @return boolean wether the current user is someone who previews the quiz,
138 * rather than attempting it.
139 */
140 public function is_preview_user() {
141 if (is_null($this->ispreviewuser)) {
142 $this->ispreviewuser = has_capability('mod/quiz:preview', $this->context);
143 }
144 return $this->ispreviewuser;
145 }
146
78e7a3dd 147 /**
148 * @return integer number fo pages in this quiz.
149 */
150 public function get_num_pages() {
151 return count($this->pagequestionids);
152 }
153
154
155 /**
156 * @param int $page page number
157 * @return boolean true if this is the last page of the quiz.
158 */
159 public function is_last_page($page) {
160 return $page == count($this->pagequestionids) - 1;
161 }
162
b10c38a3 163 /**
164 * @param integer $id the question id.
165 * @return object the question object with that id.
166 */
36e413e3 167 public function get_question($id) {
36e413e3 168 return $this->questions[$id];
169 }
170
78e7a3dd 171 /**
172 * @param array $questionids question ids of the questions to load. null for all.
173 */
174 public function get_questions($questionids = null) {
175 if (is_null($questionids)) {
176 $questionids = $this->questionids;
177 }
178 $questions = array();
179 foreach ($questionids as $id) {
180 $questions[$id] = $this->questions[$id];
181 $this->ensure_question_loaded($id);
182 }
183 return $questions;
184 }
185
186 /**
187 * Return the list of question ids for either a given page of the quiz, or for the
188 * whole quiz.
189 *
190 * @param mixed $page string 'all' or integer page number.
191 * @return array the reqested list of question ids.
192 */
193 public function get_question_ids($page = 'all') {
793f3a0e 194 if ($page === 'all') {
78e7a3dd 195 $list = $this->questionids;
196 } else {
e153801c 197 $list = $this->pagequestionids[$page];
78e7a3dd 198 }
199 // Clone the array, so our private arrays cannot be modified.
200 $result = array();
201 foreach ($list as $id) {
202 $result[] = $id;
203 }
204 return $result;
205 }
206
b10c38a3 207 /**
208 * @param integer $timenow the current time as a unix timestamp.
209 * @return object and instance of the quiz_access_manager class for this quiz at this time.
210 */
36e413e3 211 public function get_access_manager($timenow) {
212 if (is_null($this->accessmanager)) {
78e7a3dd 213 $this->accessmanager = new quiz_access_manager($this, $timenow,
36e413e3 214 has_capability('mod/quiz:ignoretimelimits', $this->context, NULL, false));
215 }
216 return $this->accessmanager;
217 }
218
78e7a3dd 219 /**
220 * Wrapper round the has_capability funciton that automatically passes in the quiz context.
221 */
222 public function has_capability($capability, $userid = NULL, $doanything = true) {
223 return has_capability($capability, $this->context, $userid, $doanything);
224 }
225
226 /**
227 * Wrapper round the require_capability funciton that automatically passes in the quiz context.
228 */
229 public function require_capability($capability, $userid = NULL, $doanything = true) {
230 return require_capability($capability, $this->context, $userid, $doanything);
231 }
232
36e413e3 233 // URLs related to this attempt ========================================================
b10c38a3 234 /**
235 * @return string the URL of this quiz's view page.
236 */
36e413e3 237 public function view_url() {
238 global $CFG;
239 return $CFG->wwwroot . '/mod/quiz/view.php?id=' . $this->cm->id;
240 }
241
78e7a3dd 242 /**
243 * @return string the URL of this quiz's edit page.
244 */
245 public function edit_url() {
246 global $CFG;
247 return $CFG->wwwroot . '/mod/quiz/edit.php?cmid=' . $this->cm->id;
248 }
249
250 /**
251 * @param integer $attemptid the id of an attempt.
252 * @return string the URL of that attempt.
253 */
254 public function attempt_url($attemptid) {
255 global $CFG;
3c168fbb 256 return $CFG->wwwroot . '/mod/quiz/attempt.php?attempt=' . $attemptid;
78e7a3dd 257 }
258
259 /**
260 * @return string the URL of this quiz's edit page. Needs to be POSTed to with a cmid parameter.
261 */
262 public function start_attempt_url() {
263 global $CFG;
264 return $CFG->wwwroot . '/mod/quiz/startattempt.php';
265 }
266
267 /**
268 * @param integer $attemptid the id of an attempt.
269 * @return string the URL of the review of that attempt.
270 */
271 public function review_url($attemptid) {
272 global $CFG;
273 return $CFG->wwwroot . '/mod/quiz/review.php?attempt=' . $attemptid;
274 }
275
36e413e3 276 // Bits of content =====================================================================
b10c38a3 277 /**
278 * @return string the HTML snipped that needs to be supplied to print_header_simple
279 * as the $button parameter.
280 */
36e413e3 281 public function update_module_button() {
282 if (has_capability('moodle/course:manageactivities',
283 get_context_instance(CONTEXT_COURSE, $this->course->id))) {
284 return update_module_button($this->cm->id, $this->course->id, get_string('modulename', 'quiz'));
285 } else {
286 return '';
287 }
288 }
289
b10c38a3 290 /**
291 * @param string $title the name of this particular quiz page.
292 * @return array the data that needs to be sent to print_header_simple as the $navigation
293 * parameter.
294 */
36e413e3 295 public function navigation($title) {
296 return build_navigation($title, $this->cm);
297 }
298
299 // Private methods =====================================================================
b10c38a3 300 // Check that the definition of a particular question is loaded, and if not throw an exception.
78e7a3dd 301 protected function ensure_question_loaded($id) {
302 if (isset($this->questions[$id]->_partiallyloaded)) {
36e413e3 303 throw new moodle_quiz_exception($this, 'questionnotloaded', $id);
304 }
305 }
78e7a3dd 306
307 private function determine_layout() {
308 $this->questionids = array();
309 $this->pagequestionids = array();
310
311 // Get the appropriate layout string (from quiz or attempt).
312 $layout = $this->get_layout_string();
313 if (empty($layout)) {
314 // Nothing to do.
315 return;
316 }
317
318 // Break up the layout string into pages.
319 $pagelayouts = explode(',0', $layout);
320
321 // Strip off any empty last page (normally there is one).
322 if (end($pagelayouts) == '') {
323 array_pop($pagelayouts);
324 }
325
326 // File the ids into the arrays.
327 $this->questionids = array();
328 $this->pagequestionids = array();
329 foreach ($pagelayouts as $page => $pagelayout) {
330 $pagelayout = trim($pagelayout, ',');
331 if ($pagelayout == '') continue;
332 $this->pagequestionids[$page] = explode(',', $pagelayout);
333 foreach ($this->pagequestionids[$page] as $id) {
334 $this->questionids[] = $id;
335 }
336 }
337 }
338
339 // Number the questions.
340 private function number_questions() {
341 $number = 1;
342 foreach ($this->pagequestionids as $page => $questionids) {
343 foreach ($questionids as $id) {
344 if ($this->questions[$id]->length > 0) {
345 $this->questions[$id]->_number = $number;
346 $number += $this->questions[$id]->length;
347 } else {
348 $this->questions[$id]->_number = get_string('infoshort', 'quiz');
349 }
350 $this->questions[$id]->_page = $page;
351 }
352 }
353 }
354
355 /**
356 * @return string the layout of this quiz. Used by number_questions to
357 * work out which questions are on which pages.
358 */
359 protected function get_layout_string() {
360 return $this->quiz->questions;
361 }
36e413e3 362}
363
b10c38a3 364/**
365 * This class extends the quiz class to hold data about the state of a particular attempt,
366 * in addition to the data about the quiz.
367 */
36e413e3 368class quiz_attempt extends quiz {
369 // Fields initialised in the constructor.
370 protected $attempt;
371
372 // Fields set later if that data is needed.
36e413e3 373 protected $states = array();
4fc3d7e5 374 protected $reviewoptions = null;
36e413e3 375
376 // Constructor =========================================================================
b10c38a3 377 /**
378 * Constructor from just an attemptid.
379 *
380 * @param integer $attemptid the id of the attempt to load. We automatically load the
381 * associated quiz, course, etc.
382 */
36e413e3 383 function __construct($attemptid) {
384 global $DB;
385 if (!$this->attempt = quiz_load_attempt($attemptid)) {
386 throw new moodle_exception('invalidattemptid', 'quiz');
387 }
388 if (!$quiz = $DB->get_record('quiz', array('id' => $this->attempt->quiz))) {
389 throw new moodle_exception('invalidquizid', 'quiz');
390 }
391 if (!$course = $DB->get_record('course', array('id' => $quiz->course))) {
392 throw new moodle_exception('invalidcoursemodule');
393 }
394 if (!$cm = get_coursemodule_from_instance('quiz', $quiz->id, $course->id)) {
395 throw new moodle_exception('invalidcoursemodule');
396 }
397 parent::__construct($quiz, $cm, $course);
78e7a3dd 398 $this->preload_questions();
36e413e3 399 }
400
401 // Functions for loading more data =====================================================
36e413e3 402 /**
78e7a3dd 403 * Load the state of a number of questions that have already been loaded.
36e413e3 404 *
78e7a3dd 405 * @param array $questionids question ids to process. Blank = all.
36e413e3 406 */
78e7a3dd 407 public function load_question_states($questionids = null) {
408 if (is_null($questionids)) {
409 $questionids = $this->questionids;
36e413e3 410 }
78e7a3dd 411 $questionstoprocess = array();
412 foreach ($questionids as $id) {
413 $this->ensure_question_loaded($id);
414 $questionstoprocess[$id] = $this->questions[$id];
415 }
416 if (!$newstates = get_question_states($questionstoprocess, $this->quiz, $this->attempt)) {
36e413e3 417 throw new moodle_quiz_exception($this, 'cannotrestore');
418 }
66d07f81 419 $this->states = $newstates + $this->states;
36e413e3 420 }
421
36e413e3 422 // Simple getters ======================================================================
b10c38a3 423 /** @return integer the attempt id. */
36e413e3 424 public function get_attemptid() {
425 return $this->attempt->id;
426 }
427
b10c38a3 428 /** @return object the row from the quiz_attempts table. */
36e413e3 429 public function get_attempt() {
430 return $this->attempt;
431 }
432
78e7a3dd 433 /** @return integer the number of this attemp (is it this user's first, second, ... attempt). */
434 public function get_attempt_number() {
435 return $this->attempt->attempt;
436 }
437
b10c38a3 438 /** @return integer the id of the user this attempt belongs to. */
36e413e3 439 public function get_userid() {
440 return $this->attempt->userid;
441 }
442
b10c38a3 443 /** @return boolean whether this attemp has been finished (true) or is still in progress (false). */
36e413e3 444 public function is_finished() {
445 return $this->attempt->timefinish != 0;
446 }
447
4fc3d7e5 448 /** @return boolean whether this attemp is a preview attempt. */
449 public function is_preview() {
450 return $this->attempt->preview;
451 }
452
78e7a3dd 453 public function get_question_state($questionid) {
454 $this->ensure_state_loaded($questionid);
455 return $this->states[$questionid];
456 }
457
b10c38a3 458 /**
459 * Wrapper that calls quiz_get_reviewoptions with the appropriate arguments.
460 *
4fc3d7e5 461 * @return object the review options for this user on this attempt.
b10c38a3 462 */
36e413e3 463 public function get_review_options() {
464 if (is_null($this->reviewoptions)) {
465 $this->reviewoptions = quiz_get_reviewoptions($this->quiz, $this->attempt, $this->context);
466 }
467 return $this->reviewoptions;
468 }
469
4fc3d7e5 470 /**
471 * Wrapper that calls get_render_options with the appropriate arguments.
472 *
473 * @return object the render options for this user on this attempt.
474 */
475 public function get_render_options($state) {
476 return quiz_get_renderoptions($this->quiz->review, $state);
477 }
478
b10c38a3 479 /**
480 * Get a quiz_attempt_question_iterator for either a page of the quiz, or a whole quiz.
481 * You must have called load_questions with an appropriate argument first.
482 *
483 * @param mixed $page as for the @see{get_question_ids} method.
484 * @return quiz_attempt_question_iterator the requested iterator.
485 */
36e413e3 486 public function get_question_iterator($page = 'all') {
487 return new quiz_attempt_question_iterator($this, $page);
488 }
489
b10c38a3 490 /**
491 * Return a summary of the current state of a question in this attempt. You must previously
492 * have called load_question_states to load the state data about this question.
493 *
494 * @param integer $questionid question id of a question that belongs to this quiz.
495 * @return string a brief string (that could be used as a CSS class name, for example)
496 * that describes the current state of a question in this attempt. Possible results are:
497 * open|saved|closed|correct|partiallycorrect|incorrect.
498 */
36e413e3 499 public function get_question_status($questionid) {
b10c38a3 500 $this->ensure_state_loaded($questionid);
501 $state = $this->states[$questionid];
502 switch ($state->event) {
503 case QUESTION_EVENTOPEN:
504 return 'open';
505
506 case QUESTION_EVENTSAVE:
507 case QUESTION_EVENTGRADE:
66d07f81 508 return 'answered';
b10c38a3 509
510 case QUESTION_EVENTCLOSEANDGRADE:
511 case QUESTION_EVENTCLOSE:
512 case QUESTION_EVENTMANUALGRADE:
513 $options = quiz_get_renderoptions($this->quiz->review, $this->states[$questionid]);
514 if ($options->scores) {
515 return question_get_feedback_class($state->last_graded->raw_grade /
516 $this->questions[$questionid]->maxgrade);
517 } else {
518 return 'closed';
519 }
520
521 default:
522 $a = new stdClass;
523 $a->event = $state->event;
524 $a->questionid = $questionid;
525 $a->attemptid = $this->attempt->id;
526 throw new moodle_quiz_exception($this, 'errorunexpectedevent', $a);
527 }
36e413e3 528 }
529
530 /**
b10c38a3 531 * Return the grade obtained on a particular question, if the user is permitted to see it.
532 * You must previously have called load_question_states to load the state data about this question.
36e413e3 533 *
b10c38a3 534 * @param integer $questionid question id of a question that belongs to this quiz.
36e413e3 535 * @return string the formatted grade, to the number of decimal places specified by the quiz.
536 */
537 public function get_question_score($questionid) {
538 $this->ensure_state_loaded($questionid);
539 $options = quiz_get_renderoptions($this->quiz->review, $this->states[$questionid]);
540 if ($options->scores) {
541 return round($this->states[$questionid]->last_graded->grade, $this->quiz->decimalpoints);
542 } else {
543 return '';
544 }
545 }
546
547 // URLs related to this attempt ========================================================
b10c38a3 548 /**
549 * @param integer $page if specified, the URL of this particular page of the attempt, otherwise
550 * the URL will go to the first page.
78e7a3dd 551 * @param integer $questionid a question id. If set, will add a fragment to the URL
b10c38a3 552 * to jump to a particuar question on the page.
553 * @return string the URL to continue this attempt.
554 */
78e7a3dd 555 public function attempt_url($questionid = 0, $page = -1) {
36e413e3 556 global $CFG;
3c168fbb 557 return $CFG->wwwroot . '/mod/quiz/attempt.php?attempt=' . $this->attempt->id .
78e7a3dd 558 $this->page_and_question_fragment($questionid, $page);
36e413e3 559 }
560
b10c38a3 561 /**
562 * @return string the URL of this quiz's summary page.
563 */
36e413e3 564 public function summary_url() {
565 global $CFG;
566 return $CFG->wwwroot . '/mod/quiz/summary.php?attempt=' . $this->attempt->id;
567 }
568
b10c38a3 569 /**
570 * @param integer $page if specified, the URL of this particular page of the attempt, otherwise
571 * the URL will go to the first page.
78e7a3dd 572 * @param integer $questionid a question id. If set, will add a fragment to the URL
b10c38a3 573 * to jump to a particuar question on the page.
574 * @param boolean $showall if true, the URL will be to review the entire attempt on one page,
575 * and $page will be ignored.
b9b3aa94 576 * @param $otherattemptid if given, link to another attempt, instead of the one we represent.
b10c38a3 577 * @return string the URL to review this attempt.
578 */
4fc3d7e5 579 public function review_url($questionid = 0, $page = -1, $showall = false, $otherattemptid = null) {
36e413e3 580 global $CFG;
4fc3d7e5 581 if (is_null($otherattemptid)) {
582 $otherattemptid = $this->attempt->id;
583 }
3c168fbb 584 return $CFG->wwwroot . '/mod/quiz/review.php?attempt=' . $otherattemptid .
78e7a3dd 585 $this->page_and_question_fragment($questionid, $page, $showall);
586 }
587
588 // Bits of content =====================================================================
589 public function get_html_head_contributions($page = 'all') {
590 return get_html_head_contributions($this->get_question_ids($page),
591 $this->questions, $this->states);
592 }
593
594 public function print_restart_preview_button() {
595 global $CFG;
596 echo '<div class="controls">';
597 print_single_button($this->start_attempt_url(), array('cmid' => $this->cm->id,
598 'forcenew' => true, 'sesskey' => sesskey()), get_string('startagain', 'quiz'), 'post');
599 echo '</div>';
36e413e3 600 }
601
78e7a3dd 602 public function print_question($id) {
4fc3d7e5 603 if ($this->is_finished()) {
604 $options = $this->get_review_options();
605 } else {
606 $options = $this->get_render_options($this->states[$id]);
607 }
78e7a3dd 608 print_question($this->questions[$id], $this->states[$id], $this->questions[$id]->_number,
609 $this->quiz, $options);
610 }
611
612 public function quiz_send_notification_emails() {
613 quiz_send_notification_emails($this->course, $this->quiz, $this->attempt,
614 $this->context, $this->cm);
615 }
3c168fbb 616
617 public function print_navigation_panel($panelclass, $page) {
baef998b 618 $panel = new $panelclass($this, $this->get_review_options(), $page);
3c168fbb 619 $panel->display();
620 }
36e413e3 621
622 // Private methods =====================================================================
b10c38a3 623 // Check that the state of a particular question is loaded, and if not throw an exception.
36e413e3 624 private function ensure_state_loaded($id) {
625 if (!array_key_exists($id, $this->states)) {
626 throw new moodle_quiz_exception($this, 'statenotloaded', $id);
627 }
628 }
78e7a3dd 629
630 /**
631 * @return string the layout of this quiz. Used by number_questions to
632 * work out which questions are on which pages.
633 */
634 protected function get_layout_string() {
635 return $this->attempt->layout;
636 }
637
638 /**
639 * Enter description here...
640 *
641 * @param unknown_type $questionid the id of a particular question on the page to jump to.
642 * @param integer $page -1 to look up the page number from the questionid, otherwise the page number to use.
643 * @param boolean $showall
644 * @return string bit to add to the end of a URL.
645 */
646 private function page_and_question_fragment($questionid, $page, $showall = false) {
3c168fbb 647 if ($page == -1) {
78e7a3dd 648 if ($questionid) {
649 $page = $this->questions[$questionid]->_page;
650 } else {
651 $page = 0;
652 }
653 }
654 if ($showall) {
655 $page = 0;
656 }
657 $fragment = '';
658 if ($questionid && $questionid != reset($this->pagequestionids[$page])) {
659 $fragment = '#q' . $questionid;
660 }
661 $param = '';
662 if ($showall) {
3c168fbb 663 $param = '&showall=1';
664 } else if ($page > 0) {
665 $param = '&page=' . $page;
78e7a3dd 666 }
667 return $param . $fragment;
668 }
36e413e3 669}
670
b10c38a3 671/**
672 * A PHP Iterator for conviniently looping over the questions in a quiz. The keys are the question
673 * numbers (with 'i' for descriptions) and the values are the question objects.
674 */
36e413e3 675class quiz_attempt_question_iterator implements Iterator {
b10c38a3 676 private $attemptobj; // Reference to the quiz_attempt object we provide access to.
677 private $questionids; // Array of the question ids within that attempt we are iterating over.
678
679 /**
680 * Constructor. Normally, you don't want to call this directly. Instead call
681 * quiz_attempt::get_question_iterator
682 *
683 * @param quiz_attempt $attemptobj the quiz_attempt object we will be providing access to.
684 * @param mixed $page as for @see{quiz_attempt::get_question_iterator}.
685 */
36e413e3 686 public function __construct(quiz_attempt $attemptobj, $page = 'all') {
687 $this->attemptobj = $attemptobj;
36e413e3 688 $this->questionids = $attemptobj->get_question_ids($page);
689 }
690
b10c38a3 691 // Implementation of the Iterator interface ============================================
36e413e3 692 public function rewind() {
693 reset($this->questionids);
694 }
695
696 public function current() {
697 $id = current($this->questionids);
698 if ($id) {
699 return $this->attemptobj->get_question($id);
700 } else {
701 return false;
702 }
703 }
704
705 public function key() {
706 $id = current($this->questionids);
707 if ($id) {
78e7a3dd 708 return $this->attemptobj->get_question($id)->_number;
36e413e3 709 } else {
710 return false;
711 }
36e413e3 712 }
713
714 public function next() {
715 $id = next($this->questionids);
716 if ($id) {
717 return $this->attemptobj->get_question($id);
718 } else {
719 return false;
720 }
721 }
722
723 public function valid() {
724 return $this->current() !== false;
725 }
726}
3c168fbb 727
728abstract class quiz_nav_panel_base {
729 protected $attemptobj;
730 protected $options;
731 protected $page;
732
733 protected function __construct(quiz_attempt $attemptobj, $options, $page) {
734 $this->attemptobj = $attemptobj;
735 $this->options = $options;
736 $this->page = $page;
737 }
738
739 protected function get_question_buttons() {
740 $html = '<div class="qn_buttons">';
741 foreach ($this->attemptobj->get_question_iterator() as $number => $question) {
742 $html .= $this->get_question_button($number, $question);
743 }
744 $html .= '</div>';
745 return $html;
746 }
747
748 abstract protected function get_question_button($number, $question);
749
750 abstract protected function get_end_bits();
751
752 protected function get_question_state($question) {
753 $state = 'todo'; // TODO
754 if ($question->_page == $this->page) {
755 $state .= ' thispage';
756 }
757 return $state;
758 }
759
760 public function display() {
761 $strquiznavigation = get_string('quiznavigation', 'quiz');
baef998b 762 $content = $this->get_question_buttons() . '<div class="othernav">' .
763 $this->get_end_bits() . '</div>';
3c168fbb 764 print_side_block($strquiznavigation, $content, NULL, NULL, '', array('id' => 'quiznavigation'), $strquiznavigation);
765 }
766}
767
768class quiz_attempt_nav_panel extends quiz_nav_panel_base {
769 public function __construct(quiz_attempt $attemptobj, $options, $page) {
770 parent::__construct($attemptobj, $options, $page);
771 }
772
773 protected function get_question_button($number, $question) {
774 $questionsonpage = $this->attemptobj->get_question_ids($question->_page);
775 $onclick = '';
776 if ($question->id != reset($questionsonpage)) {
777 $onclick = ' onclick="form.action = form.action + \'#q' . $question->id .
778 '\'; return true;"';
779 }
780 return '<input type="submit" name="gotopage' . $question->_page .
781 '" value="' . $number . '" class="qnbutton ' .
782 $this->get_question_state($question) . '"' . $onclick . '/>';
783 }
784
785 protected function get_end_bits() {
786 return '<input type="submit" name="gotosummary" value="' .
787 get_string('endtest', 'quiz') . '" class="endtestlink" />';
788 }
789}
790
791class quiz_review_nav_panel extends quiz_nav_panel_base {
792 public function __construct(quiz_attempt $attemptobj, $options, $page) {
793 parent::__construct($attemptobj, $options, $page);
794 }
795
796 protected function get_question_button($number, $question) {
797 return '<a href="' . $this->attemptobj->review_url($question->id) .
798 '" class="qnbutton ' . $this->get_question_state($question) .
799 '">' . $number . '</a>';
800 }
801
802 protected function get_end_bits() {
baef998b 803 $accessmanager = $this->attemptobj->get_access_manager(time());
3c168fbb 804 $html = '<a href="' . $this->attemptobj->review_url(0, 0, true) . '">' .
805 get_string('showall', 'quiz') . '</a>';
baef998b 806 $html .= $accessmanager->print_finish_review_link($this->attemptobj->is_preview_user(), true);
3c168fbb 807 return $html;
808 }
809}
36e413e3 810?>