filemanager MDL-16597 fix YUI repository requires and remove style='display:none...
[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.
7b6757b0 39
36e413e3 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.
739b0711 52 * @param boolean $getcontext intended for testing - stops the constructor getting the context.
b10c38a3 53 */
739b0711 54 function __construct($quiz, $cm, $course, $getcontext = true) {
36e413e3 55 $this->quiz = $quiz;
56 $this->cm = $cm;
a18ba12c 57 $this->quiz->cmid = $this->cm->id;
36e413e3 58 $this->course = $course;
739b0711 59 if ($getcontext && !empty($cm->id)) {
60 $this->context = get_context_instance(CONTEXT_MODULE, $cm->id);
61 }
78e7a3dd 62 $this->determine_layout();
36e413e3 63 }
64
65 // Functions for loading more data =====================================================
66 public function load_questions_on_page($page) {
78e7a3dd 67 $this->load_questions($this->pagequestionids[$page]);
68 }
69
70 public function preload_questions() {
71 if (empty($this->questionids)) {
72 throw new moodle_quiz_exception($this, 'noquestions', $this->edit_url());
73 }
74 $this->questions = question_preload_questions($this->questionids,
75 'qqi.grade AS maxgrade, qqi.id AS instance',
76 '{quiz_question_instances} qqi ON qqi.quiz = :quizid AND q.id = qqi.question',
77 array('quizid' => $this->quiz->id));
78 $this->number_questions();
36e413e3 79 }
80
81d833ad 81 /**
fbcf399e 82 * Load some or all of the questions for this quiz.
36e413e3 83 *
78e7a3dd 84 * @param array $questionids question ids of the questions to load. null for all.
36e413e3 85 */
78e7a3dd 86 public function load_questions($questionids = null) {
87 if (is_null($questionids)) {
88 $questionids = $this->questionids;
89 }
90 $questionstoprocess = array();
91 foreach ($questionids as $id) {
92 $questionstoprocess[$id] = $this->questions[$id];
36e413e3 93 }
78e7a3dd 94 if (!get_question_options($questionstoprocess)) {
95 throw new moodle_quiz_exception($this, 'loadingquestionsfailed', implode(', ', $questionids));
36e413e3 96 }
36e413e3 97 }
98
99 // Simple getters ======================================================================
b10c38a3 100 /** @return integer the course id. */
36e413e3 101 public function get_courseid() {
102 return $this->course->id;
103 }
104
78e7a3dd 105 /** @return object the row of the course table. */
106 public function get_course() {
107 return $this->course;
108 }
109
b10c38a3 110 /** @return integer the quiz id. */
36e413e3 111 public function get_quizid() {
112 return $this->quiz->id;
113 }
114
b10c38a3 115 /** @return object the row of the quiz table. */
36e413e3 116 public function get_quiz() {
117 return $this->quiz;
118 }
119
b10c38a3 120 /** @return string the name of this quiz. */
36e413e3 121 public function get_quiz_name() {
122 return $this->quiz->name;
123 }
124
b9b3aa94 125 /** @return integer the number of attempts allowed at this quiz (0 = infinite). */
126 public function get_num_attempts_allowed() {
127 return $this->quiz->attempts;
128 }
129
b10c38a3 130 /** @return integer the course_module id. */
36e413e3 131 public function get_cmid() {
132 return $this->cm->id;
133 }
134
b10c38a3 135 /** @return object the course_module object. */
36e413e3 136 public function get_cm() {
137 return $this->cm;
138 }
139
b10c38a3 140 /**
7b6757b0 141 * @return boolean wether the current user is someone who previews the quiz,
b10c38a3 142 * rather than attempting it.
143 */
144 public function is_preview_user() {
145 if (is_null($this->ispreviewuser)) {
146 $this->ispreviewuser = has_capability('mod/quiz:preview', $this->context);
147 }
148 return $this->ispreviewuser;
149 }
150
78e7a3dd 151 /**
152 * @return integer number fo pages in this quiz.
153 */
154 public function get_num_pages() {
155 return count($this->pagequestionids);
156 }
7b6757b0 157
78e7a3dd 158
159 /**
160 * @param int $page page number
161 * @return boolean true if this is the last page of the quiz.
162 */
163 public function is_last_page($page) {
164 return $page == count($this->pagequestionids) - 1;
165 }
166
b10c38a3 167 /**
168 * @param integer $id the question id.
169 * @return object the question object with that id.
170 */
36e413e3 171 public function get_question($id) {
36e413e3 172 return $this->questions[$id];
173 }
174
78e7a3dd 175 /**
176 * @param array $questionids question ids of the questions to load. null for all.
177 */
178 public function get_questions($questionids = null) {
179 if (is_null($questionids)) {
180 $questionids = $this->questionids;
181 }
182 $questions = array();
183 foreach ($questionids as $id) {
184 $questions[$id] = $this->questions[$id];
185 $this->ensure_question_loaded($id);
186 }
187 return $questions;
188 }
189
190 /**
7b6757b0 191 * Return the list of question ids for either a given page of the quiz, or for the
78e7a3dd 192 * whole quiz.
193 *
194 * @param mixed $page string 'all' or integer page number.
195 * @return array the reqested list of question ids.
196 */
197 public function get_question_ids($page = 'all') {
793f3a0e 198 if ($page === 'all') {
78e7a3dd 199 $list = $this->questionids;
200 } else {
e153801c 201 $list = $this->pagequestionids[$page];
78e7a3dd 202 }
203 // Clone the array, so our private arrays cannot be modified.
204 $result = array();
205 foreach ($list as $id) {
206 $result[] = $id;
207 }
208 return $result;
209 }
210
b10c38a3 211 /**
212 * @param integer $timenow the current time as a unix timestamp.
213 * @return object and instance of the quiz_access_manager class for this quiz at this time.
214 */
36e413e3 215 public function get_access_manager($timenow) {
216 if (is_null($this->accessmanager)) {
78e7a3dd 217 $this->accessmanager = new quiz_access_manager($this, $timenow,
36e413e3 218 has_capability('mod/quiz:ignoretimelimits', $this->context, NULL, false));
219 }
220 return $this->accessmanager;
221 }
222
78e7a3dd 223 /**
224 * Wrapper round the has_capability funciton that automatically passes in the quiz context.
225 */
226 public function has_capability($capability, $userid = NULL, $doanything = true) {
227 return has_capability($capability, $this->context, $userid, $doanything);
228 }
229
230 /**
231 * Wrapper round the require_capability funciton that automatically passes in the quiz context.
232 */
233 public function require_capability($capability, $userid = NULL, $doanything = true) {
234 return require_capability($capability, $this->context, $userid, $doanything);
235 }
236
36e413e3 237 // URLs related to this attempt ========================================================
b10c38a3 238 /**
239 * @return string the URL of this quiz's view page.
240 */
36e413e3 241 public function view_url() {
242 global $CFG;
243 return $CFG->wwwroot . '/mod/quiz/view.php?id=' . $this->cm->id;
244 }
245
78e7a3dd 246 /**
247 * @return string the URL of this quiz's edit page.
248 */
249 public function edit_url() {
250 global $CFG;
251 return $CFG->wwwroot . '/mod/quiz/edit.php?cmid=' . $this->cm->id;
252 }
253
254 /**
255 * @param integer $attemptid the id of an attempt.
256 * @return string the URL of that attempt.
257 */
258 public function attempt_url($attemptid) {
259 global $CFG;
3c168fbb 260 return $CFG->wwwroot . '/mod/quiz/attempt.php?attempt=' . $attemptid;
78e7a3dd 261 }
262
263 /**
264 * @return string the URL of this quiz's edit page. Needs to be POSTed to with a cmid parameter.
265 */
266 public function start_attempt_url() {
267 global $CFG;
268 return $CFG->wwwroot . '/mod/quiz/startattempt.php';
269 }
270
271 /**
272 * @param integer $attemptid the id of an attempt.
273 * @return string the URL of the review of that attempt.
274 */
275 public function review_url($attemptid) {
276 global $CFG;
277 return $CFG->wwwroot . '/mod/quiz/review.php?attempt=' . $attemptid;
278 }
279
36e413e3 280 // Bits of content =====================================================================
b10c38a3 281 /**
282 * @return string the HTML snipped that needs to be supplied to print_header_simple
283 * as the $button parameter.
284 */
36e413e3 285 public function update_module_button() {
286 if (has_capability('moodle/course:manageactivities',
287 get_context_instance(CONTEXT_COURSE, $this->course->id))) {
288 return update_module_button($this->cm->id, $this->course->id, get_string('modulename', 'quiz'));
289 } else {
290 return '';
291 }
292 }
293
b10c38a3 294 /**
295 * @param string $title the name of this particular quiz page.
296 * @return array the data that needs to be sent to print_header_simple as the $navigation
297 * parameter.
298 */
36e413e3 299 public function navigation($title) {
300 return build_navigation($title, $this->cm);
301 }
302
303 // Private methods =====================================================================
b10c38a3 304 // Check that the definition of a particular question is loaded, and if not throw an exception.
78e7a3dd 305 protected function ensure_question_loaded($id) {
306 if (isset($this->questions[$id]->_partiallyloaded)) {
36e413e3 307 throw new moodle_quiz_exception($this, 'questionnotloaded', $id);
308 }
309 }
78e7a3dd 310
311 private function determine_layout() {
312 $this->questionids = array();
313 $this->pagequestionids = array();
314
315 // Get the appropriate layout string (from quiz or attempt).
316 $layout = $this->get_layout_string();
317 if (empty($layout)) {
318 // Nothing to do.
319 return;
320 }
321
322 // Break up the layout string into pages.
edd5246b 323 $pagelayouts = explode(',0', quiz_clean_layout($layout, true));
78e7a3dd 324
325 // Strip off any empty last page (normally there is one).
326 if (end($pagelayouts) == '') {
327 array_pop($pagelayouts);
328 }
329
330 // File the ids into the arrays.
331 $this->questionids = array();
332 $this->pagequestionids = array();
333 foreach ($pagelayouts as $page => $pagelayout) {
334 $pagelayout = trim($pagelayout, ',');
335 if ($pagelayout == '') continue;
336 $this->pagequestionids[$page] = explode(',', $pagelayout);
337 foreach ($this->pagequestionids[$page] as $id) {
338 $this->questionids[] = $id;
339 }
340 }
341 }
342
343 // Number the questions.
344 private function number_questions() {
345 $number = 1;
346 foreach ($this->pagequestionids as $page => $questionids) {
347 foreach ($questionids as $id) {
348 if ($this->questions[$id]->length > 0) {
349 $this->questions[$id]->_number = $number;
350 $number += $this->questions[$id]->length;
351 } else {
352 $this->questions[$id]->_number = get_string('infoshort', 'quiz');
353 }
354 $this->questions[$id]->_page = $page;
355 }
356 }
357 }
358
359 /**
360 * @return string the layout of this quiz. Used by number_questions to
7b6757b0 361 * work out which questions are on which pages.
78e7a3dd 362 */
363 protected function get_layout_string() {
364 return $this->quiz->questions;
365 }
36e413e3 366}
367
b10c38a3 368/**
369 * This class extends the quiz class to hold data about the state of a particular attempt,
370 * in addition to the data about the quiz.
371 */
36e413e3 372class quiz_attempt extends quiz {
373 // Fields initialised in the constructor.
374 protected $attempt;
375
376 // Fields set later if that data is needed.
36e413e3 377 protected $states = array();
4fc3d7e5 378 protected $reviewoptions = null;
36e413e3 379
380 // Constructor =========================================================================
b10c38a3 381 /**
382 * Constructor from just an attemptid.
383 *
384 * @param integer $attemptid the id of the attempt to load. We automatically load the
385 * associated quiz, course, etc.
386 */
36e413e3 387 function __construct($attemptid) {
388 global $DB;
389 if (!$this->attempt = quiz_load_attempt($attemptid)) {
390 throw new moodle_exception('invalidattemptid', 'quiz');
391 }
392 if (!$quiz = $DB->get_record('quiz', array('id' => $this->attempt->quiz))) {
393 throw new moodle_exception('invalidquizid', 'quiz');
394 }
395 if (!$course = $DB->get_record('course', array('id' => $quiz->course))) {
396 throw new moodle_exception('invalidcoursemodule');
397 }
398 if (!$cm = get_coursemodule_from_instance('quiz', $quiz->id, $course->id)) {
399 throw new moodle_exception('invalidcoursemodule');
400 }
401 parent::__construct($quiz, $cm, $course);
78e7a3dd 402 $this->preload_questions();
81d833ad 403 $this->preload_question_states();
36e413e3 404 }
405
406 // Functions for loading more data =====================================================
36e413e3 407 /**
78e7a3dd 408 * Load the state of a number of questions that have already been loaded.
36e413e3 409 *
78e7a3dd 410 * @param array $questionids question ids to process. Blank = all.
36e413e3 411 */
78e7a3dd 412 public function load_question_states($questionids = null) {
413 if (is_null($questionids)) {
414 $questionids = $this->questionids;
36e413e3 415 }
78e7a3dd 416 $questionstoprocess = array();
417 foreach ($questionids as $id) {
418 $this->ensure_question_loaded($id);
419 $questionstoprocess[$id] = $this->questions[$id];
420 }
40d1feeb 421 if (!question_load_states($questionstoprocess, $this->states,
81d833ad 422 $this->quiz, $this->attempt)) {
36e413e3 423 throw new moodle_quiz_exception($this, 'cannotrestore');
424 }
81d833ad 425 }
426
427
428 public function preload_question_states() {
429 if (empty($this->questionids)) {
430 throw new moodle_quiz_exception($this, 'noquestions', $this->edit_url());
431 }
432 $this->states = question_preload_states($this->attempt->uniqueid);
433 if (!$this->states) {
434 $this->states = array();
435 }
36e413e3 436 }
437
b55797b8 438 public function load_specific_question_state($questionid, $stateid) {
439 global $DB;
440 $state = question_load_specific_state($this->questions[$questionid],
441 $this->quiz, $this->attempt, $stateid);
442 if ($state === false) {
443 throw new moodle_quiz_exception($this, 'invalidstateid');
444 }
445 $this->states[$questionid] = $state;
446 }
447
36e413e3 448 // Simple getters ======================================================================
b10c38a3 449 /** @return integer the attempt id. */
36e413e3 450 public function get_attemptid() {
451 return $this->attempt->id;
452 }
453
766df8f7 454 /** @return integer the attempt unique id. */
455 public function get_uniqueid() {
456 return $this->attempt->uniqueid;
457 }
458
b10c38a3 459 /** @return object the row from the quiz_attempts table. */
36e413e3 460 public function get_attempt() {
461 return $this->attempt;
462 }
463
78e7a3dd 464 /** @return integer the number of this attemp (is it this user's first, second, ... attempt). */
465 public function get_attempt_number() {
466 return $this->attempt->attempt;
467 }
468
b10c38a3 469 /** @return integer the id of the user this attempt belongs to. */
36e413e3 470 public function get_userid() {
471 return $this->attempt->userid;
472 }
473
aafdb447 474 /** @return boolean whether this attempt has been finished (true) or is still in progress (false). */
36e413e3 475 public function is_finished() {
476 return $this->attempt->timefinish != 0;
477 }
478
4fc3d7e5 479 /** @return boolean whether this attemp is a preview attempt. */
480 public function is_preview() {
481 return $this->attempt->preview;
482 }
483
b55797b8 484 /**
485 * Is this a student dealing with their own attempt/teacher previewing,
486 * or someone with 'mod/quiz:viewreports' reviewing someone elses attempt.
7b6757b0 487 *
b55797b8 488 * @return boolean whether this situation should be treated as someone looking at their own
489 * attempt. The distinction normally only matters when an attempt is being reviewed.
490 */
491 public function is_own_attempt() {
492 global $USER;
493 return $this->attempt->userid == $USER->id &&
494 (!$this->is_preview_user() || $this->attempt->preview);
b55797b8 495 }
496
96c7d771 497 /**
498 * Check the appropriate capability to see whether this user may review their own attempt.
499 * If not, prints an error.
500 */
501 public function check_review_capability() {
502 if (!$this->has_capability('mod/quiz:viewreports')) {
503 if ($this->get_review_options()->quizstate == QUIZ_STATE_IMMEDIATELY) {
504 $this->require_capability('mod/quiz:attempt');
505 } else {
506 $this->require_capability('mod/quiz:reviewmyattempts');
507 }
508 }
509 }
510
78e7a3dd 511 public function get_question_state($questionid) {
512 $this->ensure_state_loaded($questionid);
513 return $this->states[$questionid];
514 }
515
b10c38a3 516 /**
517 * Wrapper that calls quiz_get_reviewoptions with the appropriate arguments.
518 *
4fc3d7e5 519 * @return object the review options for this user on this attempt.
b10c38a3 520 */
36e413e3 521 public function get_review_options() {
522 if (is_null($this->reviewoptions)) {
523 $this->reviewoptions = quiz_get_reviewoptions($this->quiz, $this->attempt, $this->context);
524 }
525 return $this->reviewoptions;
526 }
527
4fc3d7e5 528 /**
529 * Wrapper that calls get_render_options with the appropriate arguments.
530 *
531 * @return object the render options for this user on this attempt.
532 */
533 public function get_render_options($state) {
62e76c67 534 return quiz_get_renderoptions($this->quiz, $this->attempt, $this->context, $state);
4fc3d7e5 535 }
536
b10c38a3 537 /**
538 * Get a quiz_attempt_question_iterator for either a page of the quiz, or a whole quiz.
539 * You must have called load_questions with an appropriate argument first.
540 *
541 * @param mixed $page as for the @see{get_question_ids} method.
542 * @return quiz_attempt_question_iterator the requested iterator.
543 */
36e413e3 544 public function get_question_iterator($page = 'all') {
545 return new quiz_attempt_question_iterator($this, $page);
546 }
547
b10c38a3 548 /**
549 * Return a summary of the current state of a question in this attempt. You must previously
550 * have called load_question_states to load the state data about this question.
551 *
552 * @param integer $questionid question id of a question that belongs to this quiz.
553 * @return string a brief string (that could be used as a CSS class name, for example)
554 * that describes the current state of a question in this attempt. Possible results are:
555 * open|saved|closed|correct|partiallycorrect|incorrect.
556 */
36e413e3 557 public function get_question_status($questionid) {
b10c38a3 558 $state = $this->states[$questionid];
559 switch ($state->event) {
560 case QUESTION_EVENTOPEN:
561 return 'open';
562
563 case QUESTION_EVENTSAVE:
564 case QUESTION_EVENTGRADE:
dee2bdc0 565 case QUESTION_EVENTSUBMIT:
66d07f81 566 return 'answered';
b10c38a3 567
568 case QUESTION_EVENTCLOSEANDGRADE:
569 case QUESTION_EVENTCLOSE:
570 case QUESTION_EVENTMANUALGRADE:
62e76c67 571 $options = $this->get_render_options($this->states[$questionid]);
1aea04dc 572 if ($options->scores && $this->questions[$questionid]->maxgrade > 0) {
b10c38a3 573 return question_get_feedback_class($state->last_graded->raw_grade /
574 $this->questions[$questionid]->maxgrade);
575 } else {
576 return 'closed';
577 }
578
579 default:
580 $a = new stdClass;
581 $a->event = $state->event;
582 $a->questionid = $questionid;
583 $a->attemptid = $this->attempt->id;
584 throw new moodle_quiz_exception($this, 'errorunexpectedevent', $a);
585 }
36e413e3 586 }
587
62e76c67 588 /**
589 * @param integer $questionid question id of a question that belongs to this quiz.
590 * @return boolean whether this question hss been flagged by the attempter.
591 */
592 public function is_question_flagged($questionid) {
62e76c67 593 $state = $this->states[$questionid];
594 return $state->flagged;
595 }
596
36e413e3 597 /**
b10c38a3 598 * Return the grade obtained on a particular question, if the user is permitted to see it.
599 * You must previously have called load_question_states to load the state data about this question.
36e413e3 600 *
b10c38a3 601 * @param integer $questionid question id of a question that belongs to this quiz.
36e413e3 602 * @return string the formatted grade, to the number of decimal places specified by the quiz.
603 */
604 public function get_question_score($questionid) {
fd9f17a1 605 $options = $this->get_render_options($this->states[$questionid]);
36e413e3 606 if ($options->scores) {
84e628a0 607 return quiz_format_question_grade($this->quiz, $this->states[$questionid]->last_graded->grade);
36e413e3 608 } else {
609 return '';
610 }
611 }
612
613 // URLs related to this attempt ========================================================
b10c38a3 614 /**
615 * @param integer $page if specified, the URL of this particular page of the attempt, otherwise
616 * the URL will go to the first page.
78e7a3dd 617 * @param integer $questionid a question id. If set, will add a fragment to the URL
b10c38a3 618 * to jump to a particuar question on the page.
619 * @return string the URL to continue this attempt.
620 */
78e7a3dd 621 public function attempt_url($questionid = 0, $page = -1) {
36e413e3 622 global $CFG;
3c168fbb 623 return $CFG->wwwroot . '/mod/quiz/attempt.php?attempt=' . $this->attempt->id .
78e7a3dd 624 $this->page_and_question_fragment($questionid, $page);
36e413e3 625 }
626
b10c38a3 627 /**
628 * @return string the URL of this quiz's summary page.
629 */
36e413e3 630 public function summary_url() {
631 global $CFG;
632 return $CFG->wwwroot . '/mod/quiz/summary.php?attempt=' . $this->attempt->id;
633 }
634
9f9eec1e 635 /**
636 * @return string the URL of this quiz's summary page.
637 */
638 public function processattempt_url() {
639 global $CFG;
640 return $CFG->wwwroot . '/mod/quiz/processattempt.php';
641 }
642
b10c38a3 643 /**
644 * @param integer $page if specified, the URL of this particular page of the attempt, otherwise
645 * the URL will go to the first page.
78e7a3dd 646 * @param integer $questionid a question id. If set, will add a fragment to the URL
b10c38a3 647 * to jump to a particuar question on the page.
648 * @param boolean $showall if true, the URL will be to review the entire attempt on one page,
649 * and $page will be ignored.
b9b3aa94 650 * @param $otherattemptid if given, link to another attempt, instead of the one we represent.
b10c38a3 651 * @return string the URL to review this attempt.
652 */
b55797b8 653 public function review_url($questionid = 0, $page = -1, $showall = false) {
36e413e3 654 global $CFG;
b55797b8 655 return $CFG->wwwroot . '/mod/quiz/review.php?attempt=' . $this->attempt->id .
78e7a3dd 656 $this->page_and_question_fragment($questionid, $page, $showall);
657 }
658
659 // Bits of content =====================================================================
660 public function get_html_head_contributions($page = 'all') {
661 return get_html_head_contributions($this->get_question_ids($page),
662 $this->questions, $this->states);
663 }
664
b826bcef 665 public function get_question_html_head_contributions($questionid) {
b5f8c377 666 return get_html_head_contributions(array($questionid),
b826bcef 667 $this->questions, $this->states);
668 }
669
78e7a3dd 670 public function print_restart_preview_button() {
671 global $CFG;
672 echo '<div class="controls">';
673 print_single_button($this->start_attempt_url(), array('cmid' => $this->cm->id,
674 'forcenew' => true, 'sesskey' => sesskey()), get_string('startagain', 'quiz'), 'post');
675 echo '</div>';
36e413e3 676 }
677
692e0c33 678 public function get_timer_html() {
679 return '<div id="quiz-timer">' . get_string('timeleft', 'quiz') .
680 ' <span id="quiz-time-left"></span></div>';
681 }
682
aafdb447 683 /**
684 * Wrapper round print_question from lib/questionlib.php.
685 *
686 * @param integer $id the id of a question in this quiz attempt.
687 * @param boolean $reviewing is the being printed on an attempt or a review page.
688 * @param string $thispageurl the URL of the page this question is being printed on.
689 */
690 public function print_question($id, $reviewing, $thispageurl = '') {
691 if ($reviewing) {
4fc3d7e5 692 $options = $this->get_review_options();
693 } else {
694 $options = $this->get_render_options($this->states[$id]);
695 }
aafdb447 696 if ($thispageurl) {
697 $this->quiz->thispageurl = $thispageurl;
698 } else {
699 unset($thispageurl);
700 }
78e7a3dd 701 print_question($this->questions[$id], $this->states[$id], $this->questions[$id]->_number,
702 $this->quiz, $options);
703 }
704
705 public function quiz_send_notification_emails() {
706 quiz_send_notification_emails($this->course, $this->quiz, $this->attempt,
707 $this->context, $this->cm);
708 }
3c168fbb 709
d4a03c00 710 public function get_navigation_panel($panelclass, $page) {
baef998b 711 $panel = new $panelclass($this, $this->get_review_options(), $page);
d4a03c00 712 return $panel->get_contents();
3c168fbb 713 }
36e413e3 714
f88fb62c 715 /// List of all this user's attempts for people who can see reports.
b55797b8 716 public function links_to_other_attempts($url) {
717 $search = '/\battempt=' . $this->attempt->id . '\b/';
718 $attempts = quiz_get_user_attempts($this->quiz->id, $this->attempt->userid, 'all');
f88fb62c 719 if (count($attempts) <= 1) {
720 return false;
721 }
b55797b8 722 $attemptlist = array();
723 foreach ($attempts as $at) {
724 if ($at->id == $this->attempt->id) {
725 $attemptlist[] = '<strong>' . $at->attempt . '</strong>';
726 } else {
727 $changedurl = preg_replace($search, 'attempt=' . $at->id, $url);
d4ad9adf 728 $attemptlist[] = '<a href="' . s($changedurl) . '">' . $at->attempt . '</a>';
b55797b8 729 }
730 }
731 return implode(', ', $attemptlist);
732 }
733
766df8f7 734 // Methods for processing manual comments ==============================================
735 // I am not sure it is a good idea to have update methods here - this class is only
736 // about getting data out of the question engine, and helping to display it, apart from
737 // this.
738 public function process_comment($questionid, $comment, $grade) {
739 $this->ensure_question_loaded($questionid);
740 $this->ensure_state_loaded($questionid);
741 $state = $this->states[$questionid];
742
743 $error = question_process_comment($this->questions[$questionid],
744 $state, $this->attempt, $comment, $grade);
745
746 // If the state was update (successfully), save the changes.
747 if (!is_string($error) && $state->changed) {
748 if (!save_question_session($this->questions[$questionid], $state)) {
749 $error = get_string('errorudpatingquestionsession', 'quiz');
750 }
751 if (!quiz_save_best_grade($this->quiz, $this->attempt->userid)) {
752 $error = get_string('errorudpatingbestgrade', 'quiz');
753 }
754 }
755 return $error;
756 }
757
758 public function question_print_comment_fields($questionid, $prefix) {
e8f99abc 759 global $DB;
760
766df8f7 761 $this->ensure_question_loaded($questionid);
762 $this->ensure_state_loaded($questionid);
e8f99abc 763
764 /// Work out a nice title.
765 $student = $DB->get_record('user', array('id' => $this->get_userid()));
766 $a = new object();
767 $a->fullname = fullname($student, true);
768 $a->attempt = $this->get_attempt_number();
769
766df8f7 770 question_print_comment_fields($this->questions[$questionid],
e8f99abc 771 $this->states[$questionid], $prefix, $this->quiz, get_string('gradingattempt', 'quiz_grading', $a));
766df8f7 772 }
773
36e413e3 774 // Private methods =====================================================================
b10c38a3 775 // Check that the state of a particular question is loaded, and if not throw an exception.
36e413e3 776 private function ensure_state_loaded($id) {
81d833ad 777 if (!array_key_exists($id, $this->states) || isset($this->states[$id]->_partiallyloaded)) {
36e413e3 778 throw new moodle_quiz_exception($this, 'statenotloaded', $id);
779 }
780 }
78e7a3dd 781
782 /**
783 * @return string the layout of this quiz. Used by number_questions to
7b6757b0 784 * work out which questions are on which pages.
78e7a3dd 785 */
786 protected function get_layout_string() {
787 return $this->attempt->layout;
788 }
789
790 /**
791 * Enter description here...
792 *
793 * @param unknown_type $questionid the id of a particular question on the page to jump to.
794 * @param integer $page -1 to look up the page number from the questionid, otherwise the page number to use.
795 * @param boolean $showall
796 * @return string bit to add to the end of a URL.
797 */
798 private function page_and_question_fragment($questionid, $page, $showall = false) {
3c168fbb 799 if ($page == -1) {
78e7a3dd 800 if ($questionid) {
801 $page = $this->questions[$questionid]->_page;
802 } else {
803 $page = 0;
804 }
805 }
806 if ($showall) {
807 $page = 0;
808 }
809 $fragment = '';
d4a03c00 810 if ($questionid) {
811 if ($questionid == reset($this->pagequestionids[$page])) {
812 // First question on page, go to top.
813 $fragment = '#';
814 } else {
815 $fragment = '#q' . $questionid;
816 }
78e7a3dd 817 }
818 $param = '';
819 if ($showall) {
d4ad9adf 820 $param = '&showall=1';
3c168fbb 821 } else if ($page > 0) {
d4ad9adf 822 $param = '&page=' . $page;
78e7a3dd 823 }
824 return $param . $fragment;
825 }
36e413e3 826}
827
b10c38a3 828/**
829 * A PHP Iterator for conviniently looping over the questions in a quiz. The keys are the question
830 * numbers (with 'i' for descriptions) and the values are the question objects.
831 */
36e413e3 832class quiz_attempt_question_iterator implements Iterator {
b10c38a3 833 private $attemptobj; // Reference to the quiz_attempt object we provide access to.
834 private $questionids; // Array of the question ids within that attempt we are iterating over.
835
836 /**
837 * Constructor. Normally, you don't want to call this directly. Instead call
838 * quiz_attempt::get_question_iterator
839 *
840 * @param quiz_attempt $attemptobj the quiz_attempt object we will be providing access to.
841 * @param mixed $page as for @see{quiz_attempt::get_question_iterator}.
842 */
36e413e3 843 public function __construct(quiz_attempt $attemptobj, $page = 'all') {
844 $this->attemptobj = $attemptobj;
36e413e3 845 $this->questionids = $attemptobj->get_question_ids($page);
846 }
847
b10c38a3 848 // Implementation of the Iterator interface ============================================
36e413e3 849 public function rewind() {
850 reset($this->questionids);
851 }
852
853 public function current() {
854 $id = current($this->questionids);
855 if ($id) {
856 return $this->attemptobj->get_question($id);
857 } else {
858 return false;
859 }
860 }
861
862 public function key() {
863 $id = current($this->questionids);
864 if ($id) {
78e7a3dd 865 return $this->attemptobj->get_question($id)->_number;
36e413e3 866 } else {
867 return false;
868 }
36e413e3 869 }
870
871 public function next() {
872 $id = next($this->questionids);
873 if ($id) {
874 return $this->attemptobj->get_question($id);
875 } else {
876 return false;
877 }
878 }
879
880 public function valid() {
881 return $this->current() !== false;
882 }
883}
3c168fbb 884
885abstract class quiz_nav_panel_base {
886 protected $attemptobj;
887 protected $options;
888 protected $page;
889
890 protected function __construct(quiz_attempt $attemptobj, $options, $page) {
891 $this->attemptobj = $attemptobj;
892 $this->options = $options;
893 $this->page = $page;
894 }
895
896 protected function get_question_buttons() {
cf615522 897 global $PAGE;
c752264f 898 $html = '<div class="qn_buttons">' . "\n";
3c168fbb 899 foreach ($this->attemptobj->get_question_iterator() as $number => $question) {
cf615522 900 $html .= $this->get_question_button($number, $question) . "\n";
901 $PAGE->requires->js_function_call('quiz_init_nav_button',
902 array($this->get_button_id($question), $question->id));
3c168fbb 903 }
c752264f 904 $html .= "</div>\n";
3c168fbb 905 return $html;
906 }
907
c752264f 908 protected function get_button_id($question) {
909 // The id to put on the button element in the HTML.
910 return 'quiznavbutton' . $question->id;
911 }
912
3c168fbb 913 abstract protected function get_question_button($number, $question);
914
915 abstract protected function get_end_bits();
916
a733c4b9 917 protected function get_user_picture() {
918 global $DB;
919 $user = $DB->get_record('user', array('id' => $this->attemptobj->get_userid()));
920 $output = '';
921 $output .= '<div id="user-picture" class="clearfix">';
922 $output .= print_user_picture($user, $this->attemptobj->get_courseid(), NULL, 0, true, false);
923 $output .= ' ' . fullname($user);
924 $output .= '</div>';
925 return $output;
926 }
927
62e76c67 928 protected function get_question_state_classes($question) {
929 // The current status of the question.
930 $classes = $this->attemptobj->get_question_status($question->id);
931
932 // Plus a marker for the current page.
3c168fbb 933 if ($question->_page == $this->page) {
62e76c67 934 $classes .= ' thispage';
935 }
936
937 // Plus a marker for flagged questions.
938 if ($this->attemptobj->is_question_flagged($question->id)) {
939 $classes .= ' flagged';
3c168fbb 940 }
62e76c67 941 return $classes;
3c168fbb 942 }
943
d4a03c00 944 public function get_contents() {
a733c4b9 945 $content = '';
946 if ($this->attemptobj->get_quiz()->showuserpicture) {
947 $content .= $this->get_user_picture() . "\n";
948 }
949 $content .= $this->get_question_buttons() . "\n";
950 $content .= '<div class="othernav">' . "\n" . $this->get_end_bits() . "\n</div>\n";
d4a03c00 951
952 $bc = new block_contents();
953 $bc->id = 'quiznavigation';
954 $bc->title = get_string('quiznavigation', 'quiz');
955 $bc->content = $content;
956 return $bc;
3c168fbb 957 }
958}
959
960class quiz_attempt_nav_panel extends quiz_nav_panel_base {
961 public function __construct(quiz_attempt $attemptobj, $options, $page) {
962 parent::__construct($attemptobj, $options, $page);
963 }
964
965 protected function get_question_button($number, $question) {
966 $questionsonpage = $this->attemptobj->get_question_ids($question->_page);
967 $onclick = '';
968 if ($question->id != reset($questionsonpage)) {
969 $onclick = ' onclick="form.action = form.action + \'#q' . $question->id .
970 '\'; return true;"';
971 }
972 return '<input type="submit" name="gotopage' . $question->_page .
973 '" value="' . $number . '" class="qnbutton ' .
c752264f 974 $this->get_question_state_classes($question) . '" id="' .
975 $this->get_button_id($question) . '" ' . $onclick . '/>';
3c168fbb 976 }
977
978 protected function get_end_bits() {
5533791a 979 $output = '';
980 $output .= '<input type="submit" name="gotosummary" value="' .
3c168fbb 981 get_string('endtest', 'quiz') . '" class="endtestlink" />';
692e0c33 982 $output .= $this->attemptobj->get_timer_html();
5533791a 983 return $output;
3c168fbb 984 }
985}
986
987class quiz_review_nav_panel extends quiz_nav_panel_base {
988 public function __construct(quiz_attempt $attemptobj, $options, $page) {
989 parent::__construct($attemptobj, $options, $page);
990 }
991
992 protected function get_question_button($number, $question) {
5f9c9122 993 $strstate = get_string($this->attemptobj->get_question_status($question->id), 'quiz');
3c168fbb 994 return '<a href="' . $this->attemptobj->review_url($question->id) .
c752264f 995 '" class="qnbutton ' . $this->get_question_state_classes($question) . '" id="' .
5f9c9122 996 $this->get_button_id($question) . '" title="' . $strstate . '">' . $number . '<span class="accesshide">(' . $strstate . '</span></a>';
3c168fbb 997 }
998
999 protected function get_end_bits() {
baef998b 1000 $accessmanager = $this->attemptobj->get_access_manager(time());
3c168fbb 1001 $html = '<a href="' . $this->attemptobj->review_url(0, 0, true) . '">' .
1002 get_string('showall', 'quiz') . '</a>';
baef998b 1003 $html .= $accessmanager->print_finish_review_link($this->attemptobj->is_preview_user(), true);
3c168fbb 1004 return $html;
1005 }
1006}
7b6757b0 1007?>