MDL-3030 quiz overdue handling: show the attempt state in the reports.
[moodle.git] / mod / quiz / attemptlib.php
CommitLineData
36e413e3 1<?php
d18675a8 2// This file is part of Moodle - http://moodle.org/
3//
4// Moodle is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Moodle is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
16
36e413e3 17/**
d18675a8 18 * Back-end code for handling data about quizzes and the current user's attempt.
19 *
20 * There are classes for loading all the information about a quiz and attempts,
21 * and for displaying the navigation panel.
22 *
ba643847 23 * @package mod
a1eb3a44 24 * @subpackage quiz
ba643847
TH
25 * @copyright 2008 onwards Tim Hunt
26 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
d18675a8 27 */
36e413e3 28
36e413e3 29
a17b297d 30defined('MOODLE_INTERNAL') || die();
a1eb3a44
TH
31
32
36e413e3 33/**
b10c38a3 34 * Class for quiz exceptions. Just saves a couple of arguments on the
35 * constructor for a moodle_exception.
d18675a8 36 *
f7970e3c
TH
37 * @copyright 2008 Tim Hunt
38 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
39 * @since Moodle 2.0
36e413e3 40 */
41class moodle_quiz_exception extends moodle_exception {
25a03faa 42 public function __construct($quizobj, $errorcode, $a = null, $link = '', $debuginfo = null) {
36e413e3 43 if (!$link) {
44 $link = $quizobj->view_url();
45 }
46 parent::__construct($errorcode, 'quiz', $link, $a, $debuginfo);
47 }
48}
49
f7970e3c 50
b10c38a3 51/**
d18675a8 52 * A class encapsulating a quiz and the questions it contains, and making the
53 * information available to scripts like view.php.
54 *
55 * Initially, it only loads a minimal amout of information about each question - loading
56 * extra information only when necessary or when asked. The class tracks which questions
57 * are loaded.
58 *
f7970e3c
TH
59 * @copyright 2008 Tim Hunt
60 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
61 * @since Moodle 2.0
b10c38a3 62 */
36e413e3 63class quiz {
64 // Fields initialised in the constructor.
65 protected $course;
66 protected $cm;
67 protected $quiz;
68 protected $context;
a1eb3a44 69 protected $questionids;
7b6757b0 70
36e413e3 71 // Fields set later if that data is needed.
78e7a3dd 72 protected $questions = null;
36e413e3 73 protected $accessmanager = null;
b10c38a3 74 protected $ispreviewuser = null;
36e413e3 75
76 // Constructor =========================================================================
b10c38a3 77 /**
78 * Constructor, assuming we already have the necessary data loaded.
79 *
80 * @param object $quiz the row from the quiz table.
81 * @param object $cm the course_module object for this quiz.
82 * @param object $course the row from the course table for the course we belong to.
f7970e3c 83 * @param bool $getcontext intended for testing - stops the constructor getting the context.
b10c38a3 84 */
c7df5006 85 public function __construct($quiz, $cm, $course, $getcontext = true) {
36e413e3 86 $this->quiz = $quiz;
87 $this->cm = $cm;
a18ba12c 88 $this->quiz->cmid = $this->cm->id;
36e413e3 89 $this->course = $course;
739b0711 90 if ($getcontext && !empty($cm->id)) {
91 $this->context = get_context_instance(CONTEXT_MODULE, $cm->id);
92 }
389cb589
TH
93 $questionids = quiz_questions_in_quiz($this->quiz->questions);
94 if ($questionids) {
95 $this->questionids = explode(',', quiz_questions_in_quiz($this->quiz->questions));
96 } else {
97 $this->questionids = array(); // Which idiot made explode(',', '') = array('')?
98 }
36e413e3 99 }
100
990650f9
TH
101 /**
102 * Static function to create a new quiz object for a specific user.
103 *
f7970e3c
TH
104 * @param int $quizid the the quiz id.
105 * @param int $userid the the userid.
a88ba570 106 * @return quiz the new quiz object
990650f9 107 */
c7df5006 108 public static function create($quizid, $userid) {
990650f9
TH
109 global $DB;
110
dd70d561 111 $quiz = quiz_access_manager::load_quiz_and_settings($quizid);
88f0eb15
TH
112 $course = $DB->get_record('course', array('id' => $quiz->course), '*', MUST_EXIST);
113 $cm = get_coursemodule_from_instance('quiz', $quiz->id, $course->id, false, MUST_EXIST);
990650f9
TH
114
115 // Update quiz with override information
116 $quiz = quiz_update_effective_access($quiz, $userid);
117
118 return new quiz($quiz, $cm, $course);
119 }
120
36e413e3 121 // Functions for loading more data =====================================================
78e7a3dd 122
d18675a8 123 /**
124 * Load just basic information about all the questions in this quiz.
125 */
78e7a3dd 126 public function preload_questions() {
127 if (empty($this->questionids)) {
128 throw new moodle_quiz_exception($this, 'noquestions', $this->edit_url());
129 }
130 $this->questions = question_preload_questions($this->questionids,
a1eb3a44 131 'qqi.grade AS maxmark, qqi.id AS instance',
78e7a3dd 132 '{quiz_question_instances} qqi ON qqi.quiz = :quizid AND q.id = qqi.question',
133 array('quizid' => $this->quiz->id));
36e413e3 134 }
135
25a03faa
TH
136 /**
137 * Fully load some or all of the questions for this quiz. You must call
138 * {@link preload_questions()} first.
36e413e3 139 *
78e7a3dd 140 * @param array $questionids question ids of the questions to load. null for all.
36e413e3 141 */
78e7a3dd 142 public function load_questions($questionids = null) {
143 if (is_null($questionids)) {
144 $questionids = $this->questionids;
145 }
146 $questionstoprocess = array();
147 foreach ($questionids as $id) {
a1eb3a44
TH
148 if (array_key_exists($id, $this->questions)) {
149 $questionstoprocess[$id] = $this->questions[$id];
150 }
36e413e3 151 }
a8a8ec51 152 get_question_options($questionstoprocess);
36e413e3 153 }
154
155 // Simple getters ======================================================================
f7970e3c 156 /** @return int the course id. */
36e413e3 157 public function get_courseid() {
158 return $this->course->id;
159 }
160
78e7a3dd 161 /** @return object the row of the course table. */
162 public function get_course() {
163 return $this->course;
164 }
165
f7970e3c 166 /** @return int the quiz id. */
36e413e3 167 public function get_quizid() {
168 return $this->quiz->id;
169 }
170
b10c38a3 171 /** @return object the row of the quiz table. */
36e413e3 172 public function get_quiz() {
173 return $this->quiz;
174 }
175
b10c38a3 176 /** @return string the name of this quiz. */
36e413e3 177 public function get_quiz_name() {
178 return $this->quiz->name;
179 }
180
33c8d37b
CF
181 /** @return int the quiz navigation method. */
182 public function get_navigation_method() {
183 return $this->quiz->navmethod;
184 }
185
f7970e3c 186 /** @return int the number of attempts allowed at this quiz (0 = infinite). */
b9b3aa94 187 public function get_num_attempts_allowed() {
188 return $this->quiz->attempts;
189 }
190
f7970e3c 191 /** @return int the course_module id. */
36e413e3 192 public function get_cmid() {
193 return $this->cm->id;
194 }
195
b10c38a3 196 /** @return object the course_module object. */
36e413e3 197 public function get_cm() {
198 return $this->cm;
199 }
200
a1eb3a44
TH
201 /** @return object the module context for this quiz. */
202 public function get_context() {
203 return $this->context;
204 }
205
b10c38a3 206 /**
f7970e3c 207 * @return bool wether the current user is someone who previews the quiz,
b10c38a3 208 * rather than attempting it.
209 */
210 public function is_preview_user() {
211 if (is_null($this->ispreviewuser)) {
212 $this->ispreviewuser = has_capability('mod/quiz:preview', $this->context);
213 }
214 return $this->ispreviewuser;
215 }
216
78e7a3dd 217 /**
a1eb3a44 218 * @return whether any questions have been added to this quiz.
78e7a3dd 219 */
a1eb3a44
TH
220 public function has_questions() {
221 return !empty($this->questionids);
78e7a3dd 222 }
223
b10c38a3 224 /**
f7970e3c 225 * @param int $id the question id.
b10c38a3 226 * @return object the question object with that id.
227 */
36e413e3 228 public function get_question($id) {
36e413e3 229 return $this->questions[$id];
230 }
231
78e7a3dd 232 /**
233 * @param array $questionids question ids of the questions to load. null for all.
234 */
235 public function get_questions($questionids = null) {
236 if (is_null($questionids)) {
237 $questionids = $this->questionids;
238 }
239 $questions = array();
240 foreach ($questionids as $id) {
a1eb3a44
TH
241 if (!array_key_exists($id, $this->questions)) {
242 throw new moodle_exception('cannotstartmissingquestion', 'quiz', $this->view_url());
243 }
78e7a3dd 244 $questions[$id] = $this->questions[$id];
245 $this->ensure_question_loaded($id);
246 }
247 return $questions;
248 }
249
b10c38a3 250 /**
f7970e3c 251 * @param int $timenow the current time as a unix timestamp.
25a03faa
TH
252 * @return quiz_access_manager and instance of the quiz_access_manager class
253 * for this quiz at this time.
b10c38a3 254 */
36e413e3 255 public function get_access_manager($timenow) {
256 if (is_null($this->accessmanager)) {
78e7a3dd 257 $this->accessmanager = new quiz_access_manager($this, $timenow,
25a03faa 258 has_capability('mod/quiz:ignoretimelimits', $this->context, null, false));
36e413e3 259 }
260 return $this->accessmanager;
261 }
262
78e7a3dd 263 /**
264 * Wrapper round the has_capability funciton that automatically passes in the quiz context.
265 */
25a03faa 266 public function has_capability($capability, $userid = null, $doanything = true) {
78e7a3dd 267 return has_capability($capability, $this->context, $userid, $doanything);
268 }
269
270 /**
271 * Wrapper round the require_capability funciton that automatically passes in the quiz context.
272 */
25a03faa 273 public function require_capability($capability, $userid = null, $doanything = true) {
78e7a3dd 274 return require_capability($capability, $this->context, $userid, $doanything);
275 }
276
36e413e3 277 // URLs related to this attempt ========================================================
b10c38a3 278 /**
279 * @return string the URL of this quiz's view page.
280 */
36e413e3 281 public function view_url() {
282 global $CFG;
283 return $CFG->wwwroot . '/mod/quiz/view.php?id=' . $this->cm->id;
284 }
285
78e7a3dd 286 /**
287 * @return string the URL of this quiz's edit page.
288 */
289 public function edit_url() {
290 global $CFG;
291 return $CFG->wwwroot . '/mod/quiz/edit.php?cmid=' . $this->cm->id;
292 }
293
294 /**
f7970e3c 295 * @param int $attemptid the id of an attempt.
da729916 296 * @param int $page optional page number to go to in the attempt.
78e7a3dd 297 * @return string the URL of that attempt.
298 */
da729916 299 public function attempt_url($attemptid, $page = 0) {
78e7a3dd 300 global $CFG;
da729916
TH
301 $url = $CFG->wwwroot . '/mod/quiz/attempt.php?attempt=' . $attemptid;
302 if ($page) {
303 $url .= '&page=' . $page;
304 }
305 return $url;
78e7a3dd 306 }
307
308 /**
309 * @return string the URL of this quiz's edit page. Needs to be POSTed to with a cmid parameter.
310 */
da729916
TH
311 public function start_attempt_url($page = 0) {
312 $params = array('cmid' => $this->cm->id, 'sesskey' => sesskey());
313 if ($page) {
314 $params['page'] = $page;
315 }
316 return new moodle_url('/mod/quiz/startattempt.php', $params);
78e7a3dd 317 }
318
319 /**
f7970e3c 320 * @param int $attemptid the id of an attempt.
78e7a3dd 321 * @return string the URL of the review of that attempt.
322 */
323 public function review_url($attemptid) {
ea9dbf24 324 return new moodle_url('/mod/quiz/review.php', array('attempt' => $attemptid));
78e7a3dd 325 }
326
36e413e3 327 // Bits of content =====================================================================
36e413e3 328
d755b0f5
TH
329 /**
330 * @param bool $unfinished whether there is currently an unfinished attempt active.
331 * @return string if the quiz policies merit it, return a warning string to
332 * be displayed in a javascript alert on the start attempt button.
333 */
334 public function confirm_start_attempt_message($unfinished) {
335 if ($unfinished) {
336 return '';
337 }
338
339 if ($this->quiz->timelimit && $this->quiz->attempts) {
340 return get_string('confirmstartattempttimelimit', 'quiz', $this->quiz->attempts);
341 } else if ($this->quiz->timelimit) {
342 return get_string('confirmstarttimelimit', 'quiz');
343 } else if ($this->quiz->attempts) {
344 return get_string('confirmstartattemptlimit', 'quiz', $this->quiz->attempts);
345 }
346
347 return '';
348 }
349
350 /**
351 * If $reviewoptions->attempt is false, meaning that students can't review this
352 * attempt at the moment, return an appropriate string explaining why.
353 *
354 * @param int $when One of the mod_quiz_display_options::DURING,
355 * IMMEDIATELY_AFTER, LATER_WHILE_OPEN or AFTER_CLOSE constants.
356 * @param bool $short if true, return a shorter string.
357 * @return string an appropraite message.
358 */
359 public function cannot_review_message($when, $short = false) {
360
361 if ($short) {
362 $langstrsuffix = 'short';
363 $dateformat = get_string('strftimedatetimeshort', 'langconfig');
364 } else {
365 $langstrsuffix = '';
366 $dateformat = '';
367 }
368
369 if ($when == mod_quiz_display_options::DURING ||
370 $when == mod_quiz_display_options::IMMEDIATELY_AFTER) {
371 return '';
372 } else if ($when == mod_quiz_display_options::LATER_WHILE_OPEN && $this->quiz->timeclose &&
373 $this->quiz->reviewattempt & mod_quiz_display_options::AFTER_CLOSE) {
374 return get_string('noreviewuntil' . $langstrsuffix, 'quiz',
375 userdate($this->quiz->timeclose, $dateformat));
376 } else {
377 return get_string('noreview' . $langstrsuffix, 'quiz');
378 }
379 }
380
b10c38a3 381 /**
382 * @param string $title the name of this particular quiz page.
383 * @return array the data that needs to be sent to print_header_simple as the $navigation
384 * parameter.
385 */
36e413e3 386 public function navigation($title) {
2698e9c1 387 global $PAGE;
388 $PAGE->navbar->add($title);
389 return '';
36e413e3 390 }
391
392 // Private methods =====================================================================
d18675a8 393 /**
25a03faa
TH
394 * Check that the definition of a particular question is loaded, and if not throw an exception.
395 * @param $id a questionid.
d18675a8 396 */
78e7a3dd 397 protected function ensure_question_loaded($id) {
398 if (isset($this->questions[$id]->_partiallyloaded)) {
36e413e3 399 throw new moodle_quiz_exception($this, 'questionnotloaded', $id);
400 }
401 }
402}
403
f7970e3c 404
b10c38a3 405/**
406 * This class extends the quiz class to hold data about the state of a particular attempt,
407 * in addition to the data about the quiz.
d18675a8 408 *
f7970e3c
TH
409 * @copyright 2008 Tim Hunt
410 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
411 * @since Moodle 2.0
b10c38a3 412 */
a1eb3a44 413class quiz_attempt {
2de9be52
TH
414
415 /** @var string to identify the in progress state. */
416 const IN_PROGRESS = 'inprogress';
417 /** @var string to identify the overdue state. */
418 const OVERDUE = 'overdue';
419 /** @var string to identify the finished state. */
420 const FINISHED = 'finished';
421 /** @var string to identify the abandoned state. */
422 const ABANDONED = 'abandoned';
423
424 // Basic data
a1eb3a44 425 protected $quizobj;
36e413e3 426 protected $attempt;
427
2de9be52
TH
428 // More details of what happened for each question.
429 protected $quba;
a1eb3a44 430 protected $pagelayout; // array page no => array of numbers on the page in order.
4fc3d7e5 431 protected $reviewoptions = null;
36e413e3 432
433 // Constructor =========================================================================
b10c38a3 434 /**
990650f9
TH
435 * Constructor assuming we already have the necessary data loaded.
436 *
437 * @param object $attempt the row of the quiz_attempts table.
438 * @param object $quiz the quiz object for this attempt and user.
439 * @param object $cm the course_module object for this quiz.
440 * @param object $course the row from the course table for the course we belong to.
08502b57
TH
441 * @param bool $loadquestions (optional) if true, the default, load all the details
442 * of the state of each question. Else just set up the basic details of the attempt.
990650f9 443 */
08502b57 444 public function __construct($attempt, $quiz, $cm, $course, $loadquestions = true) {
990650f9 445 $this->attempt = $attempt;
a1eb3a44 446 $this->quizobj = new quiz($quiz, $cm, $course);
08502b57
TH
447
448 if (!$loadquestions) {
449 return;
450 }
451
a1eb3a44
TH
452 $this->quba = question_engine::load_questions_usage_by_activity($this->attempt->uniqueid);
453 $this->determine_layout();
454 $this->number_questions();
990650f9
TH
455 }
456
457 /**
56e82d99
TH
458 * Used by {create()} and {create_from_usage_id()}.
459 * @param array $conditions passed to $DB->get_record('quiz_attempts', $conditions).
b10c38a3 460 */
c7df5006 461 protected static function create_helper($conditions) {
36e413e3 462 global $DB;
990650f9 463
88f0eb15 464 $attempt = $DB->get_record('quiz_attempts', $conditions, '*', MUST_EXIST);
dd70d561 465 $quiz = quiz_access_manager::load_quiz_and_settings($attempt->quiz);
88f0eb15
TH
466 $course = $DB->get_record('course', array('id' => $quiz->course), '*', MUST_EXIST);
467 $cm = get_coursemodule_from_instance('quiz', $quiz->id, $course->id, false, MUST_EXIST);
a1eb3a44 468
990650f9
TH
469 // Update quiz with override information
470 $quiz = quiz_update_effective_access($quiz, $attempt->userid);
471
472 return new quiz_attempt($attempt, $quiz, $cm, $course);
36e413e3 473 }
474
56e82d99
TH
475 /**
476 * Static function to create a new quiz_attempt object given an attemptid.
477 *
f7970e3c 478 * @param int $attemptid the attempt id.
56e82d99
TH
479 * @return quiz_attempt the new quiz_attempt object
480 */
c7df5006 481 public static function create($attemptid) {
56e82d99
TH
482 return self::create_helper(array('id' => $attemptid));
483 }
484
485 /**
486 * Static function to create a new quiz_attempt object given a usage id.
487 *
f7970e3c 488 * @param int $usageid the attempt usage id.
56e82d99
TH
489 * @return quiz_attempt the new quiz_attempt object
490 */
c7df5006 491 public static function create_from_usage_id($usageid) {
56e82d99
TH
492 return self::create_helper(array('uniqueid' => $usageid));
493 }
494
b39b5d77
TH
495 /**
496 * @param string $state one of the state constants like IN_PROGRESS.
497 * @return string the human-readable state name.
498 */
499 public static function state_name($state) {
500 switch ($state) {
501 case quiz_attempt::IN_PROGRESS:
502 return get_string('stateinprogress', 'quiz');
503 case quiz_attempt::OVERDUE:
504 return get_string('stateoverdue', 'quiz');
505 case quiz_attempt::FINISHED:
506 return get_string('statefinished', 'quiz');
507 case quiz_attempt::ABANDONED:
508 return get_string('stateabandoned', 'quiz');
509 }
510 }
511
a1eb3a44
TH
512 private function determine_layout() {
513 $this->pagelayout = array();
514
515 // Break up the layout string into pages.
516 $pagelayouts = explode(',0', quiz_clean_layout($this->attempt->layout, true));
517
518 // Strip off any empty last page (normally there is one).
519 if (end($pagelayouts) == '') {
520 array_pop($pagelayouts);
36e413e3 521 }
a1eb3a44
TH
522
523 // File the ids into the arrays.
524 $this->pagelayout = array();
525 foreach ($pagelayouts as $page => $pagelayout) {
526 $pagelayout = trim($pagelayout, ',');
527 if ($pagelayout == '') {
528 continue;
529 }
530 $this->pagelayout[$page] = explode(',', $pagelayout);
78e7a3dd 531 }
a1eb3a44
TH
532 }
533
534 // Number the questions.
535 private function number_questions() {
536 $number = 1;
537 foreach ($this->pagelayout as $page => $slots) {
538 foreach ($slots as $slot) {
539 $question = $this->quba->get_question($slot);
540 if ($question->length > 0) {
541 $question->_number = $number;
542 $number += $question->length;
543 } else {
544 $question->_number = get_string('infoshort', 'quiz');
545 }
546 $question->_page = $page;
547 }
36e413e3 548 }
81d833ad 549 }
550
a1eb3a44
TH
551 // Simple getters ======================================================================
552 public function get_quiz() {
553 return $this->quizobj->get_quiz();
554 }
555
556 public function get_quizobj() {
557 return $this->quizobj;
558 }
559
f7970e3c 560 /** @return int the course id. */
a1eb3a44
TH
561 public function get_courseid() {
562 return $this->quizobj->get_courseid();
563 }
564
f7970e3c 565 /** @return int the course id. */
a1eb3a44
TH
566 public function get_course() {
567 return $this->quizobj->get_course();
568 }
569
f7970e3c 570 /** @return int the quiz id. */
a1eb3a44
TH
571 public function get_quizid() {
572 return $this->quizobj->get_quizid();
573 }
574
575 /** @return string the name of this quiz. */
576 public function get_quiz_name() {
577 return $this->quizobj->get_quiz_name();
578 }
579
33c8d37b
CF
580 /** @return int the quiz navigation method. */
581 public function get_navigation_method() {
582 return $this->quizobj->get_navigation_method();
583 }
584
a1eb3a44
TH
585 /** @return object the course_module object. */
586 public function get_cm() {
587 return $this->quizobj->get_cm();
588 }
589
590 /** @return object the course_module object. */
591 public function get_cmid() {
592 return $this->quizobj->get_cmid();
593 }
594
d18675a8 595 /**
f7970e3c 596 * @return bool wether the current user is someone who previews the quiz,
a1eb3a44 597 * rather than attempting it.
d18675a8 598 */
a1eb3a44
TH
599 public function is_preview_user() {
600 return $this->quizobj->is_preview_user();
601 }
602
f7970e3c 603 /** @return int the number of attempts allowed at this quiz (0 = infinite). */
a1eb3a44
TH
604 public function get_num_attempts_allowed() {
605 return $this->quizobj->get_num_attempts_allowed();
606 }
607
f7970e3c 608 /** @return int number fo pages in this quiz. */
a1eb3a44
TH
609 public function get_num_pages() {
610 return count($this->pagelayout);
36e413e3 611 }
612
d18675a8 613 /**
f7970e3c 614 * @param int $timenow the current time as a unix timestamp.
25a03faa
TH
615 * @return quiz_access_manager and instance of the quiz_access_manager class
616 * for this quiz at this time.
d18675a8 617 */
a1eb3a44
TH
618 public function get_access_manager($timenow) {
619 return $this->quizobj->get_access_manager($timenow);
b55797b8 620 }
621
f7970e3c 622 /** @return int the attempt id. */
36e413e3 623 public function get_attemptid() {
624 return $this->attempt->id;
625 }
626
f7970e3c 627 /** @return int the attempt unique id. */
766df8f7 628 public function get_uniqueid() {
629 return $this->attempt->uniqueid;
630 }
631
b10c38a3 632 /** @return object the row from the quiz_attempts table. */
36e413e3 633 public function get_attempt() {
634 return $this->attempt;
635 }
636
f7970e3c 637 /** @return int the number of this attemp (is it this user's first, second, ... attempt). */
78e7a3dd 638 public function get_attempt_number() {
639 return $this->attempt->attempt;
640 }
641
2de9be52
TH
642 public function get_state() {
643 return $this->attempt->state;
644 }
645
f7970e3c 646 /** @return int the id of the user this attempt belongs to. */
36e413e3 647 public function get_userid() {
648 return $this->attempt->userid;
649 }
c5fd1682 650
5db82949
CF
651 /** @return int the current page of the attempt. */
652 public function get_currentpage() {
653 return $this->attempt->currentpage;
654 }
36e413e3 655
2de9be52
TH
656 public function get_sum_marks() {
657 return $this->attempt->sumgrades;
658 }
659
25a03faa
TH
660 /**
661 * @return bool whether this attempt has been finished (true) or is still
662 * in progress (false).
663 */
36e413e3 664 public function is_finished() {
2de9be52 665 return $this->attempt->state == self::FINISHED;
36e413e3 666 }
667
f7970e3c 668 /** @return bool whether this attempt is a preview attempt. */
4fc3d7e5 669 public function is_preview() {
670 return $this->attempt->preview;
671 }
672
b55797b8 673 /**
674 * Is this a student dealing with their own attempt/teacher previewing,
675 * or someone with 'mod/quiz:viewreports' reviewing someone elses attempt.
7b6757b0 676 *
f7970e3c 677 * @return bool whether this situation should be treated as someone looking at their own
b55797b8 678 * attempt. The distinction normally only matters when an attempt is being reviewed.
679 */
680 public function is_own_attempt() {
681 global $USER;
682 return $this->attempt->userid == $USER->id &&
683 (!$this->is_preview_user() || $this->attempt->preview);
b55797b8 684 }
685
b3782c71
TH
686 /**
687 * @return bool whether this attempt is a preview belonging to the current user.
688 */
689 public function is_own_preview() {
690 global $USER;
691 return $this->attempt->userid == $USER->id &&
692 $this->is_preview_user() && $this->attempt->preview;
693 }
694
8032cd79
TH
695 /**
696 * Is the current user allowed to review this attempt. This applies when
697 * {@link is_own_attempt()} returns false.
698 * @return bool whether the review should be allowed.
699 */
700 public function is_review_allowed() {
701 if (!$this->has_capability('mod/quiz:viewreports')) {
702 return false;
703 }
704
705 $cm = $this->get_cm();
706 if ($this->has_capability('moodle/site:accessallgroups') ||
707 groups_get_activity_groupmode($cm) != SEPARATEGROUPS) {
708 return true;
709 }
710
711 // Check the users have at least one group in common.
712 $teachersgroups = groups_get_activity_allowed_groups($cm);
25a03faa
TH
713 $studentsgroups = groups_get_all_groups(
714 $cm->course, $this->attempt->userid, $cm->groupingid);
8032cd79
TH
715 return $teachersgroups && $studentsgroups &&
716 array_intersect(array_keys($teachersgroups), array_keys($studentsgroups));
717 }
718
b2607ccc
TH
719 /**
720 * Get the overall feedback corresponding to a particular mark.
721 * @param $grade a particular grade.
722 */
723 public function get_overall_feedback($grade) {
724 return quiz_feedback_for_grade($grade, $this->get_quiz(),
2709ee45 725 $this->quizobj->get_context());
b2607ccc
TH
726 }
727
a1eb3a44
TH
728 /**
729 * Wrapper round the has_capability funciton that automatically passes in the quiz context.
730 */
25a03faa 731 public function has_capability($capability, $userid = null, $doanything = true) {
a1eb3a44
TH
732 return $this->quizobj->has_capability($capability, $userid, $doanything);
733 }
734
735 /**
736 * Wrapper round the require_capability funciton that automatically passes in the quiz context.
737 */
25a03faa 738 public function require_capability($capability, $userid = null, $doanything = true) {
a1eb3a44
TH
739 return $this->quizobj->require_capability($capability, $userid, $doanything);
740 }
741
96c7d771 742 /**
743 * Check the appropriate capability to see whether this user may review their own attempt.
744 * If not, prints an error.
745 */
746 public function check_review_capability() {
747 if (!$this->has_capability('mod/quiz:viewreports')) {
a1eb3a44 748 if ($this->get_attempt_state() == mod_quiz_display_options::IMMEDIATELY_AFTER) {
96c7d771 749 $this->require_capability('mod/quiz:attempt');
750 } else {
751 $this->require_capability('mod/quiz:reviewmyattempts');
752 }
753 }
754 }
755
33c8d37b
CF
756 /**
757 * Checks whether a user may navigate to a particular slot
758 */
759 public function can_navigate_to($slot) {
760 switch ($this->get_navigation_method()) {
761 case QUIZ_NAVMETHOD_FREE:
762 return true;
763 break;
764 case QUIZ_NAVMETHOD_SEQ:
765 return false;
766 break;
767 }
768 return true;
769 }
770
d18675a8 771 /**
f7970e3c 772 * @return int one of the mod_quiz_display_options::DURING,
a1eb3a44 773 * IMMEDIATELY_AFTER, LATER_WHILE_OPEN or AFTER_CLOSE constants.
d18675a8 774 */
a1eb3a44
TH
775 public function get_attempt_state() {
776 return quiz_attempt_state($this->get_quiz(), $this->attempt);
78e7a3dd 777 }
778
b10c38a3 779 /**
a1eb3a44
TH
780 * Wrapper that the correct mod_quiz_display_options for this quiz at the
781 * moment.
b10c38a3 782 *
a1eb3a44 783 * @return question_display_options the render options for this user on this attempt.
b10c38a3 784 */
a1eb3a44
TH
785 public function get_display_options($reviewing) {
786 if ($reviewing) {
787 if (is_null($this->reviewoptions)) {
7ee80cab 788 $this->reviewoptions = quiz_get_review_options($this->get_quiz(),
a1eb3a44
TH
789 $this->attempt, $this->quizobj->get_context());
790 }
791 return $this->reviewoptions;
792
793 } else {
794 $options = mod_quiz_display_options::make_from_quiz($this->get_quiz(),
795 mod_quiz_display_options::DURING);
796 $options->flags = quiz_get_flag_option($this->attempt, $this->quizobj->get_context());
797 return $options;
36e413e3 798 }
36e413e3 799 }
800
da729916
TH
801 /**
802 * Wrapper that the correct mod_quiz_display_options for this quiz at the
803 * moment.
804 *
805 * @param bool $reviewing true for review page, else attempt page.
806 * @param int $slot which question is being displayed.
807 * @param moodle_url $thispageurl to return to after the editing form is
808 * submitted or cancelled. If null, no edit link will be generated.
809 *
810 * @return question_display_options the render options for this user on this
811 * attempt, with extra info to generate an edit link, if applicable.
812 */
813 public function get_display_options_with_edit_link($reviewing, $slot, $thispageurl) {
814 $options = clone($this->get_display_options($reviewing));
815
816 if (!$thispageurl) {
817 return $options;
818 }
819
820 if (!($reviewing || $this->is_preview())) {
821 return $options;
822 }
823
824 $question = $this->quba->get_question($slot);
825 if (!question_has_capability_on($question, 'edit', $question->category)) {
826 return $options;
827 }
828
829 $options->editquestionparams['cmid'] = $this->get_cmid();
830 $options->editquestionparams['returnurl'] = $thispageurl;
831
832 return $options;
833 }
834
4fc3d7e5 835 /**
a1eb3a44 836 * @param int $page page number
f7970e3c 837 * @return bool true if this is the last page of the quiz.
4fc3d7e5 838 */
a1eb3a44
TH
839 public function is_last_page($page) {
840 return $page == count($this->pagelayout) - 1;
4fc3d7e5 841 }
842
b10c38a3 843 /**
a1eb3a44
TH
844 * Return the list of question ids for either a given page of the quiz, or for the
845 * whole quiz.
b10c38a3 846 *
a1eb3a44
TH
847 * @param mixed $page string 'all' or integer page number.
848 * @return array the reqested list of question ids.
b10c38a3 849 */
a1eb3a44 850 public function get_slots($page = 'all') {
a1eb3a44
TH
851 if ($page === 'all') {
852 $numbers = array();
853 foreach ($this->pagelayout as $numbersonpage) {
854 $numbers = array_merge($numbers, $numbersonpage);
855 }
856 return $numbers;
857 } else {
858 return $this->pagelayout[$page];
859 }
36e413e3 860 }
861
b10c38a3 862 /**
a1eb3a44 863 * Get the question_attempt object for a particular question in this attempt.
f7970e3c 864 * @param int $slot the number used to identify this question within this attempt.
a1eb3a44 865 * @return question_attempt
b10c38a3 866 */
a1eb3a44
TH
867 public function get_question_attempt($slot) {
868 return $this->quba->get_question_attempt($slot);
869 }
b10c38a3 870
a1eb3a44
TH
871 /**
872 * Is a particular question in this attempt a real question, or something like a description.
f7970e3c
TH
873 * @param int $slot the number used to identify this question within this attempt.
874 * @return bool whether that question is a real question.
a1eb3a44
TH
875 */
876 public function is_real_question($slot) {
877 return $this->quba->get_question($slot)->length != 0;
36e413e3 878 }
879
62e76c67 880 /**
a1eb3a44 881 * Is a particular question in this attempt a real question, or something like a description.
f7970e3c
TH
882 * @param int $slot the number used to identify this question within this attempt.
883 * @return bool whether that question is a real question.
62e76c67 884 */
a1eb3a44
TH
885 public function is_question_flagged($slot) {
886 return $this->quba->get_question_attempt($slot)->is_flagged();
62e76c67 887 }
888
36e413e3 889 /**
25a03faa
TH
890 * Return the grade obtained on a particular question, if the user is permitted
891 * to see it. You must previously have called load_question_states to load the
892 * state data about this question.
36e413e3 893 *
f7970e3c 894 * @param int $slot the number used to identify this question within this attempt.
25a03faa
TH
895 * @return string the formatted grade, to the number of decimal places specified
896 * by the quiz.
36e413e3 897 */
a1eb3a44
TH
898 public function get_question_number($slot) {
899 return $this->quba->get_question($slot)->_number;
900 }
901
902 /**
25a03faa
TH
903 * Return the grade obtained on a particular question, if the user is permitted
904 * to see it. You must previously have called load_question_states to load the
905 * state data about this question.
a1eb3a44 906 *
f7970e3c 907 * @param int $slot the number used to identify this question within this attempt.
25a03faa
TH
908 * @return string the formatted grade, to the number of decimal places specified
909 * by the quiz.
a1eb3a44
TH
910 */
911 public function get_question_name($slot) {
912 return $this->quba->get_question($slot)->name;
913 }
914
915 /**
25a03faa
TH
916 * Return the grade obtained on a particular question, if the user is permitted
917 * to see it. You must previously have called load_question_states to load the
918 * state data about this question.
a1eb3a44 919 *
f7970e3c
TH
920 * @param int $slot the number used to identify this question within this attempt.
921 * @param bool $showcorrectness Whether right/partial/wrong states should
a1eb3a44 922 * be distinguised.
25a03faa
TH
923 * @return string the formatted grade, to the number of decimal places specified
924 * by the quiz.
a1eb3a44
TH
925 */
926 public function get_question_status($slot, $showcorrectness) {
927 return $this->quba->get_question_state_string($slot, $showcorrectness);
928 }
929
97cdc1de
TH
930 /**
931 * Return the grade obtained on a particular question, if the user is permitted
932 * to see it. You must previously have called load_question_states to load the
933 * state data about this question.
934 *
935 * @param int $slot the number used to identify this question within this attempt.
936 * @param bool $showcorrectness Whether right/partial/wrong states should
937 * be distinguised.
938 * @return string class name for this state.
939 */
940 public function get_question_state_class($slot, $showcorrectness) {
941 return $this->quba->get_question_state_class($slot, $showcorrectness);
942 }
943
a1eb3a44
TH
944 /**
945 * Return the grade obtained on a particular question.
946 * You must previously have called load_question_states to load the state
947 * data about this question.
948 *
f7970e3c 949 * @param int $slot the number used to identify this question within this attempt.
a1eb3a44
TH
950 * @return string the formatted grade, to the number of decimal places specified by the quiz.
951 */
b2607ccc 952 public function get_question_mark($slot) {
a1eb3a44
TH
953 return quiz_format_question_grade($this->get_quiz(), $this->quba->get_question_mark($slot));
954 }
955
956 /**
957 * Get the time of the most recent action performed on a question.
f7970e3c
TH
958 * @param int $slot the number used to identify this question within this usage.
959 * @return int timestamp.
a1eb3a44
TH
960 */
961 public function get_question_action_time($slot) {
962 return $this->quba->get_question_action_time($slot);
36e413e3 963 }
964
2b2b6458
TH
965 /**
966 * Get the time remaining for an in-progress attempt, if the time is short
967 * enought that it would be worth showing a timer.
968 * @param int $timenow the time to consider as 'now'.
969 * @return int|false the number of seconds remaining for this attempt.
970 * False if there is no limit.
971 */
972 public function get_time_left($timenow) {
973 if ($this->attempt->state != self::IN_PROGRESS) {
974 return false;
975 }
976 return $this->get_access_manager($timenow)->get_time_left($this->attempt, $timenow);
977 }
2de9be52
TH
978
979 /**
980 * @return int the time when this attempt was submitted. 0 if it has not been
981 * submitted yet.
982 */
983 public function get_submitted_date() {
984 return $this->attempt->timefinish;
985 }
986
987 /**
988 * If the attempt is in an applicable state, work out the time by which the
989 * student should next do something.
990 * @return int timestamp by which the student needs to do something.
991 */
2b2b6458
TH
992 function get_due_date() {
993 $deadlines = array();
994 if ($this->quizobj->get_quiz()->timelimit) {
995 $deadlines[] = $this->attempt->timestart + $this->quizobj->get_quiz()->timelimit;
996 }
997 if ($this->quizobj->get_quiz()->timeclose) {
998 $deadlines[] = $this->quizobj->get_quiz()->timeclose;
999 }
1000 if ($deadlines) {
1001 $duedate = min($deadlines);
1002 } else {
1003 return false;
1004 }
2de9be52 1005
2b2b6458 1006 switch ($this->attempt->state) {
2de9be52 1007 case self::IN_PROGRESS:
2b2b6458 1008 return $duedate;
2de9be52
TH
1009
1010 case self::OVERDUE:
2b2b6458 1011 return $duedate + $this->quizobj->get_quiz()->graceperiod;
2de9be52
TH
1012
1013 default:
2b2b6458 1014 throw new coding_exception('Unexpected state: ' . $this->attempt->state);
2de9be52
TH
1015 }
1016 }
1017
36e413e3 1018 // URLs related to this attempt ========================================================
b10c38a3 1019 /**
a1eb3a44
TH
1020 * @return string quiz view url.
1021 */
1022 public function view_url() {
1023 return $this->quizobj->view_url();
1024 }
1025
1026 /**
1027 * @return string the URL of this quiz's edit page. Needs to be POSTed to with a cmid parameter.
1028 */
da729916
TH
1029 public function start_attempt_url($slot = null, $page = -1) {
1030 if ($page == -1 && !is_null($slot)) {
1031 $page = $this->quba->get_question($slot)->_page;
1032 } else {
1033 $page = 0;
1034 }
1035 return $this->quizobj->start_attempt_url($page);
a1eb3a44
TH
1036 }
1037
1038 /**
f7970e3c
TH
1039 * @param int $slot if speified, the slot number of a specific question to link to.
1040 * @param int $page if specified, a particular page to link to. If not givem deduced
a1eb3a44 1041 * from $slot, or goes to the first page.
f7970e3c 1042 * @param int $questionid a question id. If set, will add a fragment to the URL
b10c38a3 1043 * to jump to a particuar question on the page.
f7970e3c 1044 * @param int $thispage if not -1, the current page. Will cause links to other things on
d18675a8 1045 * this page to be output as only a fragment.
b10c38a3 1046 * @return string the URL to continue this attempt.
1047 */
56e82d99 1048 public function attempt_url($slot = null, $page = -1, $thispage = -1) {
a1eb3a44 1049 return $this->page_and_question_url('attempt', $slot, $page, false, $thispage);
36e413e3 1050 }
1051
b10c38a3 1052 /**
1053 * @return string the URL of this quiz's summary page.
1054 */
36e413e3 1055 public function summary_url() {
a1eb3a44 1056 return new moodle_url('/mod/quiz/summary.php', array('attempt' => $this->attempt->id));
36e413e3 1057 }
1058
9f9eec1e 1059 /**
1060 * @return string the URL of this quiz's summary page.
1061 */
1062 public function processattempt_url() {
a1eb3a44 1063 return new moodle_url('/mod/quiz/processattempt.php');
9f9eec1e 1064 }
1065
b10c38a3 1066 /**
f7970e3c
TH
1067 * @param int $slot indicates which question to link to.
1068 * @param int $page if specified, the URL of this particular page of the attempt, otherwise
a1eb3a44 1069 * the URL will go to the first page. If -1, deduce $page from $slot.
f7970e3c 1070 * @param bool $showall if true, the URL will be to review the entire attempt on one page,
b10c38a3 1071 * and $page will be ignored.
f7970e3c 1072 * @param int $thispage if not -1, the current page. Will cause links to other things on
d18675a8 1073 * this page to be output as only a fragment.
b10c38a3 1074 * @return string the URL to review this attempt.
1075 */
56e82d99 1076 public function review_url($slot = null, $page = -1, $showall = false, $thispage = -1) {
a1eb3a44 1077 return $this->page_and_question_url('review', $slot, $page, $showall, $thispage);
78e7a3dd 1078 }
1079
1080 // Bits of content =====================================================================
a1eb3a44 1081
d755b0f5
TH
1082 /**
1083 * If $reviewoptions->attempt is false, meaning that students can't review this
1084 * attempt at the moment, return an appropriate string explaining why.
1085 *
1086 * @param bool $short if true, return a shorter string.
1087 * @return string an appropraite message.
1088 */
1089 public function cannot_review_message($short = false) {
1090 return $this->quizobj->cannot_review_message(
1091 $this->get_attempt_state(), $short);
1092 }
1093
d18675a8 1094 /**
1095 * Initialise the JS etc. required all the questions on a page..
1096 * @param mixed $page a page number, or 'all'.
1097 */
a1eb3a44
TH
1098 public function get_html_head_contributions($page = 'all', $showall = false) {
1099 if ($showall) {
1100 $page = 'all';
1101 }
1102 $result = '';
1103 foreach ($this->get_slots($page) as $slot) {
1104 $result .= $this->quba->render_question_head_html($slot);
1105 }
1106 $result .= question_engine::initialise_js();
1107 return $result;
78e7a3dd 1108 }
1109
d18675a8 1110 /**
1111 * Initialise the JS etc. required by one question.
f7970e3c 1112 * @param int $questionid the question id.
d18675a8 1113 */
a1eb3a44
TH
1114 public function get_question_html_head_contributions($slot) {
1115 return $this->quba->render_question_head_html($slot) .
1116 question_engine::initialise_js();
b826bcef 1117 }
1118
d18675a8 1119 /**
3c6185e9
TH
1120 * Print the HTML for the start new preview button, if the current user
1121 * is allowed to see one.
1122 */
1123 public function restart_preview_button() {
1124 global $OUTPUT;
1125 if ($this->is_preview() && $this->is_preview_user()) {
1126 return $OUTPUT->single_button(new moodle_url(
1127 $this->start_attempt_url(), array('forcenew' => true)),
1128 get_string('startnewpreview', 'quiz'));
1129 } else {
1130 return '';
1131 }
36e413e3 1132 }
1133
aafdb447 1134 /**
a1eb3a44
TH
1135 * Generate the HTML that displayes the question in its current state, with
1136 * the appropriate display options.
aafdb447 1137 *
f7970e3c
TH
1138 * @param int $id the id of a question in this quiz attempt.
1139 * @param bool $reviewing is the being printed on an attempt or a review page.
da729916 1140 * @param moodle_url $thispageurl the URL of the page this question is being printed on.
a1eb3a44 1141 * @return string HTML for the question in its current state.
aafdb447 1142 */
da729916 1143 public function render_question($slot, $reviewing, $thispageurl = null) {
a1eb3a44 1144 return $this->quba->render_question($slot,
da729916 1145 $this->get_display_options_with_edit_link($reviewing, $slot, $thispageurl),
a1eb3a44
TH
1146 $this->quba->get_question($slot)->_number);
1147 }
fb6dcdab 1148
a1eb3a44
TH
1149 /**
1150 * Like {@link render_question()} but displays the question at the past step
1151 * indicated by $seq, rather than showing the latest step.
1152 *
f7970e3c
TH
1153 * @param int $id the id of a question in this quiz attempt.
1154 * @param int $seq the seq number of the past state to display.
1155 * @param bool $reviewing is the being printed on an attempt or a review page.
a1eb3a44
TH
1156 * @param string $thispageurl the URL of the page this question is being printed on.
1157 * @return string HTML for the question in its current state.
1158 */
1159 public function render_question_at_step($slot, $seq, $reviewing, $thispageurl = '') {
1160 return $this->quba->render_question_at_step($slot, $seq,
1161 $this->get_display_options($reviewing),
1162 $this->quba->get_question($slot)->_number);
1163 }
1164
1165 /**
1166 * Wrapper round print_question from lib/questionlib.php.
1167 *
f7970e3c 1168 * @param int $id the id of a question in this quiz attempt.
a1eb3a44
TH
1169 */
1170 public function render_question_for_commenting($slot) {
1171 $options = $this->get_display_options(true);
1172 $options->hide_all_feedback();
1173 $options->manualcomment = question_display_options::EDITABLE;
25a03faa
TH
1174 return $this->quba->render_question($slot, $options,
1175 $this->quba->get_question($slot)->_number);
78e7a3dd 1176 }
1177
a1eb3a44
TH
1178 /**
1179 * Check wheter access should be allowed to a particular file.
1180 *
f7970e3c
TH
1181 * @param int $id the id of a question in this quiz attempt.
1182 * @param bool $reviewing is the being printed on an attempt or a review page.
a1eb3a44
TH
1183 * @param string $thispageurl the URL of the page this question is being printed on.
1184 * @return string HTML for the question in its current state.
1185 */
56e82d99 1186 public function check_file_access($slot, $reviewing, $contextid, $component,
fe6ce234 1187 $filearea, $args, $forcedownload) {
56e82d99
TH
1188 return $this->quba->check_file_access($slot, $this->get_display_options($reviewing),
1189 $component, $filearea, $args, $forcedownload);
fe6ce234
DC
1190 }
1191
d18675a8 1192 /**
1193 * Get the navigation panel object for this attempt.
1194 *
1195 * @param $panelclass The type of panel, quiz_attempt_nav_panel or quiz_review_nav_panel
1196 * @param $page the current page number.
1197 * @param $showall whether we are showing the whole quiz on one page. (Used by review.php)
1198 * @return quiz_nav_panel_base the requested object.
1199 */
b3782c71
TH
1200 public function get_navigation_panel(mod_quiz_renderer $output,
1201 $panelclass, $page, $showall = false) {
a1eb3a44 1202 $panel = new $panelclass($this, $this->get_display_options(true), $page, $showall);
b3782c71
TH
1203
1204 $bc = new block_contents();
83f93ec4 1205 $bc->attributes['id'] = 'mod_quiz_navblock';
b3782c71
TH
1206 $bc->title = get_string('quiznavigation', 'quiz');
1207 $bc->content = $output->navigation_panel($panel);
1208 return $bc;
3c168fbb 1209 }
36e413e3 1210
d18675a8 1211 /**
83192608 1212 * Given a URL containing attempt={this attempt id}, return an array of variant URLs
b3782c71 1213 * @param moodle_url $url a URL.
d18675a8 1214 * @return string HTML fragment. Comma-separated list of links to the other
1215 * attempts with the attempt number as the link text. The curent attempt is
1216 * included but is not a link.
1217 */
b3782c71 1218 public function links_to_other_attempts(moodle_url $url) {
a1eb3a44 1219 $attempts = quiz_get_user_attempts($this->get_quiz()->id, $this->attempt->userid, 'all');
f88fb62c 1220 if (count($attempts) <= 1) {
1221 return false;
1222 }
b3782c71
TH
1223
1224 $links = new mod_quiz_links_to_other_attempts();
b55797b8 1225 foreach ($attempts as $at) {
1226 if ($at->id == $this->attempt->id) {
b3782c71 1227 $links->links[$at->attempt] = null;
b55797b8 1228 } else {
b3782c71 1229 $links->links[$at->attempt] = new moodle_url($url, array('attempt' => $at->id));
b55797b8 1230 }
1231 }
b3782c71 1232 return $links;
b55797b8 1233 }
1234
a1eb3a44
TH
1235 // Methods for processing ==================================================
1236
d18675a8 1237 /**
a1eb3a44
TH
1238 * Process all the actions that were submitted as part of the current request.
1239 *
f7970e3c 1240 * @param int $timestamp the timestamp that should be stored as the modifed
a1eb3a44 1241 * time in the database for these actions. If null, will use the current time.
d18675a8 1242 */
a1eb3a44 1243 public function process_all_actions($timestamp) {
8f37f7fb 1244 global $DB;
a1eb3a44
TH
1245 $this->quba->process_all_actions($timestamp);
1246 question_engine::save_questions_usage_by_activity($this->quba);
1247
1248 $this->attempt->timemodified = $timestamp;
1249 if ($this->attempt->timefinish) {
1250 $this->attempt->sumgrades = $this->quba->get_total_mark();
1251 }
88f0eb15
TH
1252 $DB->update_record('quiz_attempts', $this->attempt);
1253
a1eb3a44
TH
1254 if (!$this->is_preview() && $this->attempt->timefinish) {
1255 quiz_save_best_grade($this->get_quiz(), $this->get_userid());
766df8f7 1256 }
766df8f7 1257 }
1258
d18675a8 1259 /**
a1eb3a44
TH
1260 * Update the flagged state for all question_attempts in this usage, if their
1261 * flagged state was changed in the request.
d18675a8 1262 */
a1eb3a44
TH
1263 public function save_question_flags() {
1264 $this->quba->update_question_flags();
1265 question_engine::save_questions_usage_by_activity($this->quba);
1266 }
e8f99abc 1267
a1eb3a44 1268 public function finish_attempt($timestamp) {
cbb8b55c 1269 global $DB, $USER;
a1eb3a44
TH
1270 $this->quba->process_all_actions($timestamp);
1271 $this->quba->finish_all_questions($timestamp);
e8f99abc 1272
a1eb3a44 1273 question_engine::save_questions_usage_by_activity($this->quba);
e8f99abc 1274
a1eb3a44
TH
1275 $this->attempt->timemodified = $timestamp;
1276 $this->attempt->timefinish = $timestamp;
1277 $this->attempt->sumgrades = $this->quba->get_total_mark();
88f0eb15 1278 $DB->update_record('quiz_attempts', $this->attempt);
766df8f7 1279
a1eb3a44 1280 if (!$this->is_preview()) {
704f062b 1281 quiz_save_best_grade($this->get_quiz(), $this->attempt->userid);
cbb8b55c
TH
1282
1283 // Trigger event
1284 $eventdata = new stdClass();
1285 $eventdata->component = 'mod_quiz';
1286 $eventdata->attemptid = $this->attempt->id;
1287 $eventdata->timefinish = $this->attempt->timefinish;
1288 $eventdata->userid = $this->attempt->userid;
1289 $eventdata->submitterid = $USER->id;
1290 $eventdata->quizid = $this->get_quizid();
1291 $eventdata->cmid = $this->get_cmid();
1292 $eventdata->courseid = $this->get_courseid();
1293 events_trigger('quiz_attempt_submitted', $eventdata);
1294
987c2d49
TH
1295 // Tell any access rules that care that the attempt is over.
1296 $this->get_access_manager($timestamp)->current_attempt_finished();
36e413e3 1297 }
1298 }
78e7a3dd 1299
1300 /**
a1eb3a44
TH
1301 * Print the fields of the comment form for questions in this attempt.
1302 * @param $slot which question to output the fields for.
1303 * @param $prefix Prefix to add to all field names.
78e7a3dd 1304 */
a1eb3a44
TH
1305 public function question_print_comment_fields($slot, $prefix) {
1306 // Work out a nice title.
1307 $student = get_record('user', 'id', $this->get_userid());
1308 $a = new object();
1309 $a->fullname = fullname($student, true);
1310 $a->attempt = $this->get_attempt_number();
1311
1312 question_print_comment_fields($this->quba->get_question_attempt($slot),
1313 $prefix, $this->get_display_options(true)->markdp,
1314 get_string('gradingattempt', 'quiz_grading', $a));
78e7a3dd 1315 }
1316
a1eb3a44
TH
1317 // Private methods =====================================================================
1318
78e7a3dd 1319 /**
d18675a8 1320 * Get a URL for a particular question on a particular page of the quiz.
1321 * Used by {@link attempt_url()} and {@link review_url()}.
78e7a3dd 1322 *
d18675a8 1323 * @param string $script. Used in the URL like /mod/quiz/$script.php
25a03faa
TH
1324 * @param int $slot identifies the specific question on the page to jump to.
1325 * 0 to just use the $page parameter.
1326 * @param int $page -1 to look up the page number from the slot, otherwise
1327 * the page number to go to.
f7970e3c
TH
1328 * @param bool $showall if true, return a URL with showall=1, and not page number
1329 * @param int $thispage the page we are currently on. Links to questions on this
d18675a8 1330 * page will just be a fragment #q123. -1 to disable this.
1331 * @return The requested URL.
78e7a3dd 1332 */
a1eb3a44 1333 protected function page_and_question_url($script, $slot, $page, $showall, $thispage) {
d18675a8 1334 // Fix up $page
3c168fbb 1335 if ($page == -1) {
56e82d99 1336 if (!is_null($slot) && !$showall) {
a1eb3a44 1337 $page = $this->quba->get_question($slot)->_page;
78e7a3dd 1338 } else {
1339 $page = 0;
1340 }
1341 }
a1eb3a44 1342
78e7a3dd 1343 if ($showall) {
1344 $page = 0;
1345 }
d18675a8 1346
fb6dcdab 1347 // Add a fragment to scroll down to the question.
a1eb3a44 1348 $fragment = '';
56e82d99 1349 if (!is_null($slot)) {
a1eb3a44 1350 if ($slot == reset($this->pagelayout[$page])) {
d4a03c00 1351 // First question on page, go to top.
a1eb3a44 1352 $fragment = '#';
d4a03c00 1353 } else {
a1eb3a44 1354 $fragment = '#q' . $slot;
d4a03c00 1355 }
78e7a3dd 1356 }
d18675a8 1357
a1eb3a44
TH
1358 // Work out the correct start to the URL.
1359 if ($thispage == $page) {
1360 return new moodle_url($fragment);
36e413e3 1361
36e413e3 1362 } else {
a1eb3a44
TH
1363 $url = new moodle_url('/mod/quiz/' . $script . '.php' . $fragment,
1364 array('attempt' => $this->attempt->id));
1365 if ($showall) {
1366 $url->param('showall', 1);
1367 } else if ($page > 0) {
1368 $url->param('page', $page);
1369 }
1370 return $url;
36e413e3 1371 }
1372 }
36e413e3 1373}
3c168fbb 1374
f7970e3c 1375
b3782c71
TH
1376/**
1377 * Represents a single link in the navigation panel.
1378 *
1379 * @copyright 2011 The Open University
1380 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1381 * @since Moodle 2.1
1382 */
1383class quiz_nav_question_button implements renderable {
1384 public $id;
1385 public $number;
1386 public $stateclass;
1387 public $statestring;
1388 public $currentpage;
1389 public $flagged;
1390 public $url;
1391}
1392
1393
d18675a8 1394/**
1395 * Represents the navigation panel, and builds a {@link block_contents} to allow
1396 * it to be output.
1397 *
f7970e3c
TH
1398 * @copyright 2008 Tim Hunt
1399 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1400 * @since Moodle 2.0
d18675a8 1401 */
3c168fbb 1402abstract class quiz_nav_panel_base {
38c9edd0 1403 /** @var quiz_attempt */
3c168fbb 1404 protected $attemptobj;
38c9edd0 1405 /** @var question_display_options */
3c168fbb 1406 protected $options;
38c9edd0 1407 /** @var integer */
3c168fbb 1408 protected $page;
38c9edd0 1409 /** @var boolean */
d18675a8 1410 protected $showall;
3c168fbb 1411
a1eb3a44
TH
1412 public function __construct(quiz_attempt $attemptobj,
1413 question_display_options $options, $page, $showall) {
38c9edd0
TH
1414 $this->attemptobj = $attemptobj;
1415 $this->options = $options;
1416 $this->page = $page;
1417 $this->showall = $showall;
3c168fbb 1418 }
1419
b3782c71
TH
1420 public function get_question_buttons() {
1421 $buttons = array();
a1eb3a44
TH
1422 foreach ($this->attemptobj->get_slots() as $slot) {
1423 $qa = $this->attemptobj->get_question_attempt($slot);
1424 $showcorrectness = $this->options->correctness && $qa->has_marks();
a1eb3a44 1425
b3782c71
TH
1426 $button = new quiz_nav_question_button();
1427 $button->id = 'quiznavbutton' . $slot;
1428 $button->number = $qa->get_question()->_number;
97cdc1de 1429 $button->stateclass = $qa->get_state_class($showcorrectness);
33c8d37b 1430 $button->navmethod = $this->attemptobj->get_navigation_method();
b3782c71
TH
1431 if (!$showcorrectness && $button->stateclass == 'notanswered') {
1432 $button->stateclass = 'complete';
1433 }
79a46626 1434 $button->statestring = $this->get_state_string($qa, $showcorrectness);
b3782c71
TH
1435 $button->currentpage = $qa->get_question()->_page == $this->page;
1436 $button->flagged = $qa->is_flagged();
1437 $button->url = $this->get_question_url($slot);
1438 $buttons[] = $button;
a1eb3a44
TH
1439 }
1440
b3782c71 1441 return $buttons;
c752264f 1442 }
1443
79a46626 1444 protected function get_state_string(question_attempt $qa, $showcorrectness) {
0eafc988 1445 if ($qa->get_question()->length > 0) {
79a46626
TH
1446 return $qa->get_state_string($showcorrectness);
1447 }
1448
1449 // Special case handling for 'information' items.
1450 if ($qa->get_state() == question_state::$todo) {
1451 return get_string('notyetviewed', 'quiz');
1452 } else {
1453 return get_string('viewed', 'quiz');
1454 }
1455 }
1456
b3782c71 1457 public function render_before_button_bits(mod_quiz_renderer $output) {
d18675a8 1458 return '';
1459 }
3c168fbb 1460
0eb253f7
TH
1461 abstract public function render_end_bits(mod_quiz_renderer $output);
1462
1463 protected function render_restart_preview_link($output) {
b3782c71
TH
1464 if (!$this->attemptobj->is_own_preview()) {
1465 return '';
1466 }
1467 return $output->restart_preview_button(new moodle_url(
1468 $this->attemptobj->start_attempt_url(), array('forcenew' => true)));
1469 }
3c168fbb 1470
c7df5006 1471 protected abstract function get_question_url($slot);
d18675a8 1472
b3782c71
TH
1473 public function user_picture() {
1474 global $DB;
e28bbd89
TH
1475
1476 if (!$this->attemptobj->get_quiz()->showuserpicture) {
1477 return null;
1478 }
1479
a733c4b9 1480 $user = $DB->get_record('user', array('id' => $this->attemptobj->get_userid()));
b3782c71
TH
1481 $userpicture = new user_picture($user);
1482 $userpicture->courseid = $this->attemptobj->get_courseid();
1483 return $userpicture;
3c168fbb 1484 }
1485}
1486
f7970e3c 1487
d18675a8 1488/**
1489 * Specialisation of {@link quiz_nav_panel_base} for the attempt quiz page.
1490 *
f7970e3c
TH
1491 * @copyright 2008 Tim Hunt
1492 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1493 * @since Moodle 2.0
d18675a8 1494 */
3c168fbb 1495class quiz_attempt_nav_panel extends quiz_nav_panel_base {
b3782c71 1496 public function get_question_url($slot) {
33c8d37b
CF
1497 if ($this->attemptobj->can_navigate_to($slot)) {
1498 return $this->attemptobj->attempt_url($slot, -1, $this->page);
1499 } else {
1500 return null;
1501 }
3c168fbb 1502 }
1503
b3782c71
TH
1504 public function render_before_button_bits(mod_quiz_renderer $output) {
1505 return html_writer::tag('div', get_string('navnojswarning', 'quiz'),
1506 array('id' => 'quiznojswarning'));
3c168fbb 1507 }
1508
b3782c71
TH
1509 public function render_end_bits(mod_quiz_renderer $output) {
1510 return html_writer::link($this->attemptobj->summary_url(),
c9272e87 1511 get_string('endtest', 'quiz'), array('class' => 'endtestlink')) .
2b2b6458 1512 $output->countdown_timer($this->attemptobj, time()) .
0eb253f7 1513 $this->render_restart_preview_link($output);
3c168fbb 1514 }
1515}
1516
f7970e3c 1517
d18675a8 1518/**
1519 * Specialisation of {@link quiz_nav_panel_base} for the review quiz page.
1520 *
f7970e3c
TH
1521 * @copyright 2008 Tim Hunt
1522 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1523 * @since Moodle 2.0
d18675a8 1524 */
3c168fbb 1525class quiz_review_nav_panel extends quiz_nav_panel_base {
b3782c71 1526 public function get_question_url($slot) {
a1eb3a44 1527 return $this->attemptobj->review_url($slot, -1, $this->showall, $this->page);
3c168fbb 1528 }
1529
b3782c71 1530 public function render_end_bits(mod_quiz_renderer $output) {
d18675a8 1531 $html = '';
1532 if ($this->attemptobj->get_num_pages() > 1) {
1533 if ($this->showall) {
b3782c71
TH
1534 $html .= html_writer::link($this->attemptobj->review_url(null, 0, false),
1535 get_string('showeachpage', 'quiz'));
d18675a8 1536 } else {
b3782c71
TH
1537 $html .= html_writer::link($this->attemptobj->review_url(null, 0, true),
1538 get_string('showall', 'quiz'));
d18675a8 1539 }
1540 }
b3782c71 1541 $html .= $output->finish_review_link($this->attemptobj->view_url());
0eb253f7 1542 $html .= $this->render_restart_preview_link($output);
3c168fbb 1543 return $html;
1544 }
1545}