Commit | Line | Data |
---|---|---|
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 | * | |
34b7d838 TH |
23 | * @package mod_quiz |
24 | * @copyright 2008 onwards Tim Hunt | |
25 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
d18675a8 | 26 | */ |
36e413e3 | 27 | |
36e413e3 | 28 | |
a17b297d | 29 | defined('MOODLE_INTERNAL') || die(); |
a1eb3a44 TH |
30 | |
31 | ||
36e413e3 | 32 | /** |
b10c38a3 | 33 | * Class for quiz exceptions. Just saves a couple of arguments on the |
34 | * constructor for a moodle_exception. | |
d18675a8 | 35 | * |
34b7d838 TH |
36 | * @copyright 2008 Tim Hunt |
37 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
38 | * @since Moodle 2.0 | |
36e413e3 | 39 | */ |
40 | class moodle_quiz_exception extends moodle_exception { | |
25a03faa | 41 | public function __construct($quizobj, $errorcode, $a = null, $link = '', $debuginfo = null) { |
36e413e3 | 42 | if (!$link) { |
43 | $link = $quizobj->view_url(); | |
44 | } | |
45 | parent::__construct($errorcode, 'quiz', $link, $a, $debuginfo); | |
46 | } | |
47 | } | |
48 | ||
f7970e3c | 49 | |
b10c38a3 | 50 | /** |
d18675a8 | 51 | * A class encapsulating a quiz and the questions it contains, and making the |
52 | * information available to scripts like view.php. | |
53 | * | |
54 | * Initially, it only loads a minimal amout of information about each question - loading | |
55 | * extra information only when necessary or when asked. The class tracks which questions | |
56 | * are loaded. | |
57 | * | |
f7970e3c TH |
58 | * @copyright 2008 Tim Hunt |
59 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
60 | * @since Moodle 2.0 | |
b10c38a3 | 61 | */ |
36e413e3 | 62 | class quiz { |
63 | // Fields initialised in the constructor. | |
64 | protected $course; | |
65 | protected $cm; | |
66 | protected $quiz; | |
67 | protected $context; | |
7b6757b0 | 68 | |
36e413e3 | 69 | // Fields set later if that data is needed. |
78e7a3dd | 70 | protected $questions = null; |
36e413e3 | 71 | protected $accessmanager = null; |
b10c38a3 | 72 | protected $ispreviewuser = null; |
36e413e3 | 73 | |
9e83f3d1 | 74 | // Constructor ============================================================= |
b10c38a3 | 75 | /** |
76 | * Constructor, assuming we already have the necessary data loaded. | |
77 | * | |
78 | * @param object $quiz the row from the quiz table. | |
79 | * @param object $cm the course_module object for this quiz. | |
80 | * @param object $course the row from the course table for the course we belong to. | |
f7970e3c | 81 | * @param bool $getcontext intended for testing - stops the constructor getting the context. |
b10c38a3 | 82 | */ |
c7df5006 | 83 | public function __construct($quiz, $cm, $course, $getcontext = true) { |
36e413e3 | 84 | $this->quiz = $quiz; |
85 | $this->cm = $cm; | |
a18ba12c | 86 | $this->quiz->cmid = $this->cm->id; |
36e413e3 | 87 | $this->course = $course; |
739b0711 | 88 | if ($getcontext && !empty($cm->id)) { |
c492a78e | 89 | $this->context = context_module::instance($cm->id); |
739b0711 | 90 | } |
36e413e3 | 91 | } |
92 | ||
990650f9 TH |
93 | /** |
94 | * Static function to create a new quiz object for a specific user. | |
95 | * | |
f7970e3c TH |
96 | * @param int $quizid the the quiz id. |
97 | * @param int $userid the the userid. | |
a88ba570 | 98 | * @return quiz the new quiz object |
990650f9 | 99 | */ |
e1a2d0d9 | 100 | public static function create($quizid, $userid = null) { |
990650f9 TH |
101 | global $DB; |
102 | ||
dd70d561 | 103 | $quiz = quiz_access_manager::load_quiz_and_settings($quizid); |
74df2951 | 104 | $course = $DB->get_record('course', array('id' => $quiz->course), '*', MUST_EXIST); |
88f0eb15 | 105 | $cm = get_coursemodule_from_instance('quiz', $quiz->id, $course->id, false, MUST_EXIST); |
990650f9 | 106 | |
9e83f3d1 | 107 | // Update quiz with override information. |
e1a2d0d9 CC |
108 | if ($userid) { |
109 | $quiz = quiz_update_effective_access($quiz, $userid); | |
110 | } | |
990650f9 TH |
111 | |
112 | return new quiz($quiz, $cm, $course); | |
113 | } | |
114 | ||
34b7d838 TH |
115 | /** |
116 | * Create a {@link quiz_attempt} for an attempt at this quiz. | |
117 | * @param object $attemptdata row from the quiz_attempts table. | |
118 | * @return quiz_attempt the new quiz_attempt object. | |
119 | */ | |
120 | public function create_attempt_object($attemptdata) { | |
121 | return new quiz_attempt($attemptdata, $this->quiz, $this->cm, $this->course); | |
122 | } | |
123 | ||
9e83f3d1 | 124 | // Functions for loading more data ========================================= |
78e7a3dd | 125 | |
d18675a8 | 126 | /** |
127 | * Load just basic information about all the questions in this quiz. | |
128 | */ | |
78e7a3dd | 129 | public function preload_questions() { |
ccba5b88 TH |
130 | $this->questions = question_preload_questions(null, |
131 | 'slot.maxmark, slot.id AS slotid, slot.slot, slot.page', | |
132 | '{quiz_slots} slot ON slot.quizid = :quizid AND q.id = slot.questionid', | |
133 | array('quizid' => $this->quiz->id), 'slot.slot'); | |
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) { |
ccba5b88 TH |
143 | if ($this->questions === null) { |
144 | throw new coding_exception('You must call preload_questions before calling load_questions.'); | |
145 | } | |
78e7a3dd | 146 | if (is_null($questionids)) { |
ccba5b88 | 147 | $questionids = array_keys($this->questions); |
78e7a3dd | 148 | } |
149 | $questionstoprocess = array(); | |
150 | foreach ($questionids as $id) { | |
a1eb3a44 TH |
151 | if (array_key_exists($id, $this->questions)) { |
152 | $questionstoprocess[$id] = $this->questions[$id]; | |
153 | } | |
36e413e3 | 154 | } |
a8a8ec51 | 155 | get_question_options($questionstoprocess); |
36e413e3 | 156 | } |
157 | ||
e1a2d0d9 CC |
158 | /** |
159 | * Get an instance of the {@link \mod_quiz\structure} class for this quiz. | |
160 | * @return \mod_quiz\structure describes the questions in the quiz. | |
161 | */ | |
162 | public function get_structure() { | |
163 | return \mod_quiz\structure::create_for_quiz($this); | |
164 | } | |
165 | ||
9e83f3d1 | 166 | // Simple getters ========================================================== |
f7970e3c | 167 | /** @return int the course id. */ |
36e413e3 | 168 | public function get_courseid() { |
169 | return $this->course->id; | |
170 | } | |
171 | ||
78e7a3dd | 172 | /** @return object the row of the course table. */ |
173 | public function get_course() { | |
174 | return $this->course; | |
175 | } | |
176 | ||
f7970e3c | 177 | /** @return int the quiz id. */ |
36e413e3 | 178 | public function get_quizid() { |
179 | return $this->quiz->id; | |
180 | } | |
181 | ||
b10c38a3 | 182 | /** @return object the row of the quiz table. */ |
36e413e3 | 183 | public function get_quiz() { |
184 | return $this->quiz; | |
185 | } | |
186 | ||
b10c38a3 | 187 | /** @return string the name of this quiz. */ |
36e413e3 | 188 | public function get_quiz_name() { |
189 | return $this->quiz->name; | |
190 | } | |
191 | ||
33c8d37b CF |
192 | /** @return int the quiz navigation method. */ |
193 | public function get_navigation_method() { | |
194 | return $this->quiz->navmethod; | |
195 | } | |
196 | ||
f7970e3c | 197 | /** @return int the number of attempts allowed at this quiz (0 = infinite). */ |
b9b3aa94 | 198 | public function get_num_attempts_allowed() { |
199 | return $this->quiz->attempts; | |
200 | } | |
201 | ||
f7970e3c | 202 | /** @return int the course_module id. */ |
36e413e3 | 203 | public function get_cmid() { |
204 | return $this->cm->id; | |
205 | } | |
206 | ||
b10c38a3 | 207 | /** @return object the course_module object. */ |
36e413e3 | 208 | public function get_cm() { |
209 | return $this->cm; | |
210 | } | |
211 | ||
a1eb3a44 TH |
212 | /** @return object the module context for this quiz. */ |
213 | public function get_context() { | |
214 | return $this->context; | |
215 | } | |
216 | ||
b10c38a3 | 217 | /** |
f7970e3c | 218 | * @return bool wether the current user is someone who previews the quiz, |
b10c38a3 | 219 | * rather than attempting it. |
220 | */ | |
221 | public function is_preview_user() { | |
222 | if (is_null($this->ispreviewuser)) { | |
223 | $this->ispreviewuser = has_capability('mod/quiz:preview', $this->context); | |
224 | } | |
225 | return $this->ispreviewuser; | |
226 | } | |
227 | ||
78e7a3dd | 228 | /** |
a1eb3a44 | 229 | * @return whether any questions have been added to this quiz. |
78e7a3dd | 230 | */ |
a1eb3a44 | 231 | public function has_questions() { |
ccba5b88 TH |
232 | if ($this->questions === null) { |
233 | $this->preload_questions(); | |
234 | } | |
235 | return !empty($this->questions); | |
78e7a3dd | 236 | } |
237 | ||
b10c38a3 | 238 | /** |
f7970e3c | 239 | * @param int $id the question id. |
b10c38a3 | 240 | * @return object the question object with that id. |
241 | */ | |
36e413e3 | 242 | public function get_question($id) { |
36e413e3 | 243 | return $this->questions[$id]; |
244 | } | |
245 | ||
78e7a3dd | 246 | /** |
247 | * @param array $questionids question ids of the questions to load. null for all. | |
248 | */ | |
249 | public function get_questions($questionids = null) { | |
250 | if (is_null($questionids)) { | |
ccba5b88 | 251 | $questionids = array_keys($this->questions); |
78e7a3dd | 252 | } |
253 | $questions = array(); | |
254 | foreach ($questionids as $id) { | |
a1eb3a44 TH |
255 | if (!array_key_exists($id, $this->questions)) { |
256 | throw new moodle_exception('cannotstartmissingquestion', 'quiz', $this->view_url()); | |
257 | } | |
78e7a3dd | 258 | $questions[$id] = $this->questions[$id]; |
259 | $this->ensure_question_loaded($id); | |
260 | } | |
261 | return $questions; | |
262 | } | |
263 | ||
b10c38a3 | 264 | /** |
f7970e3c | 265 | * @param int $timenow the current time as a unix timestamp. |
25a03faa TH |
266 | * @return quiz_access_manager and instance of the quiz_access_manager class |
267 | * for this quiz at this time. | |
b10c38a3 | 268 | */ |
36e413e3 | 269 | public function get_access_manager($timenow) { |
270 | if (is_null($this->accessmanager)) { | |
78e7a3dd | 271 | $this->accessmanager = new quiz_access_manager($this, $timenow, |
25a03faa | 272 | has_capability('mod/quiz:ignoretimelimits', $this->context, null, false)); |
36e413e3 | 273 | } |
274 | return $this->accessmanager; | |
275 | } | |
276 | ||
78e7a3dd | 277 | /** |
278 | * Wrapper round the has_capability funciton that automatically passes in the quiz context. | |
279 | */ | |
25a03faa | 280 | public function has_capability($capability, $userid = null, $doanything = true) { |
78e7a3dd | 281 | return has_capability($capability, $this->context, $userid, $doanything); |
282 | } | |
283 | ||
284 | /** | |
285 | * Wrapper round the require_capability funciton that automatically passes in the quiz context. | |
286 | */ | |
25a03faa | 287 | public function require_capability($capability, $userid = null, $doanything = true) { |
78e7a3dd | 288 | return require_capability($capability, $this->context, $userid, $doanything); |
289 | } | |
290 | ||
9e83f3d1 | 291 | // URLs related to this attempt ============================================ |
b10c38a3 | 292 | /** |
293 | * @return string the URL of this quiz's view page. | |
294 | */ | |
36e413e3 | 295 | public function view_url() { |
296 | global $CFG; | |
297 | return $CFG->wwwroot . '/mod/quiz/view.php?id=' . $this->cm->id; | |
298 | } | |
299 | ||
78e7a3dd | 300 | /** |
301 | * @return string the URL of this quiz's edit page. | |
302 | */ | |
303 | public function edit_url() { | |
304 | global $CFG; | |
305 | return $CFG->wwwroot . '/mod/quiz/edit.php?cmid=' . $this->cm->id; | |
306 | } | |
307 | ||
308 | /** | |
f7970e3c | 309 | * @param int $attemptid the id of an attempt. |
da729916 | 310 | * @param int $page optional page number to go to in the attempt. |
78e7a3dd | 311 | * @return string the URL of that attempt. |
312 | */ | |
da729916 | 313 | public function attempt_url($attemptid, $page = 0) { |
78e7a3dd | 314 | global $CFG; |
da729916 TH |
315 | $url = $CFG->wwwroot . '/mod/quiz/attempt.php?attempt=' . $attemptid; |
316 | if ($page) { | |
317 | $url .= '&page=' . $page; | |
318 | } | |
319 | return $url; | |
78e7a3dd | 320 | } |
321 | ||
322 | /** | |
323 | * @return string the URL of this quiz's edit page. Needs to be POSTed to with a cmid parameter. | |
324 | */ | |
da729916 TH |
325 | public function start_attempt_url($page = 0) { |
326 | $params = array('cmid' => $this->cm->id, 'sesskey' => sesskey()); | |
327 | if ($page) { | |
328 | $params['page'] = $page; | |
329 | } | |
330 | return new moodle_url('/mod/quiz/startattempt.php', $params); | |
78e7a3dd | 331 | } |
332 | ||
333 | /** | |
f7970e3c | 334 | * @param int $attemptid the id of an attempt. |
78e7a3dd | 335 | * @return string the URL of the review of that attempt. |
336 | */ | |
337 | public function review_url($attemptid) { | |
ea9dbf24 | 338 | return new moodle_url('/mod/quiz/review.php', array('attempt' => $attemptid)); |
78e7a3dd | 339 | } |
340 | ||
34b7d838 TH |
341 | /** |
342 | * @param int $attemptid the id of an attempt. | |
343 | * @return string the URL of the review of that attempt. | |
344 | */ | |
345 | public function summary_url($attemptid) { | |
346 | return new moodle_url('/mod/quiz/summary.php', array('attempt' => $attemptid)); | |
347 | } | |
348 | ||
9e83f3d1 | 349 | // Bits of content ========================================================= |
36e413e3 | 350 | |
d755b0f5 TH |
351 | /** |
352 | * @param bool $unfinished whether there is currently an unfinished attempt active. | |
353 | * @return string if the quiz policies merit it, return a warning string to | |
354 | * be displayed in a javascript alert on the start attempt button. | |
355 | */ | |
356 | public function confirm_start_attempt_message($unfinished) { | |
357 | if ($unfinished) { | |
358 | return ''; | |
359 | } | |
360 | ||
361 | if ($this->quiz->timelimit && $this->quiz->attempts) { | |
362 | return get_string('confirmstartattempttimelimit', 'quiz', $this->quiz->attempts); | |
363 | } else if ($this->quiz->timelimit) { | |
364 | return get_string('confirmstarttimelimit', 'quiz'); | |
365 | } else if ($this->quiz->attempts) { | |
366 | return get_string('confirmstartattemptlimit', 'quiz', $this->quiz->attempts); | |
367 | } | |
368 | ||
369 | return ''; | |
370 | } | |
371 | ||
372 | /** | |
373 | * If $reviewoptions->attempt is false, meaning that students can't review this | |
374 | * attempt at the moment, return an appropriate string explaining why. | |
375 | * | |
376 | * @param int $when One of the mod_quiz_display_options::DURING, | |
377 | * IMMEDIATELY_AFTER, LATER_WHILE_OPEN or AFTER_CLOSE constants. | |
378 | * @param bool $short if true, return a shorter string. | |
379 | * @return string an appropraite message. | |
380 | */ | |
381 | public function cannot_review_message($when, $short = false) { | |
382 | ||
383 | if ($short) { | |
384 | $langstrsuffix = 'short'; | |
385 | $dateformat = get_string('strftimedatetimeshort', 'langconfig'); | |
386 | } else { | |
387 | $langstrsuffix = ''; | |
388 | $dateformat = ''; | |
389 | } | |
390 | ||
391 | if ($when == mod_quiz_display_options::DURING || | |
392 | $when == mod_quiz_display_options::IMMEDIATELY_AFTER) { | |
393 | return ''; | |
394 | } else if ($when == mod_quiz_display_options::LATER_WHILE_OPEN && $this->quiz->timeclose && | |
395 | $this->quiz->reviewattempt & mod_quiz_display_options::AFTER_CLOSE) { | |
396 | return get_string('noreviewuntil' . $langstrsuffix, 'quiz', | |
397 | userdate($this->quiz->timeclose, $dateformat)); | |
398 | } else { | |
399 | return get_string('noreview' . $langstrsuffix, 'quiz'); | |
400 | } | |
401 | } | |
402 | ||
b10c38a3 | 403 | /** |
404 | * @param string $title the name of this particular quiz page. | |
405 | * @return array the data that needs to be sent to print_header_simple as the $navigation | |
406 | * parameter. | |
407 | */ | |
36e413e3 | 408 | public function navigation($title) { |
2698e9c1 | 409 | global $PAGE; |
410 | $PAGE->navbar->add($title); | |
411 | return ''; | |
36e413e3 | 412 | } |
413 | ||
9e83f3d1 | 414 | // Private methods ========================================================= |
d18675a8 | 415 | /** |
25a03faa TH |
416 | * Check that the definition of a particular question is loaded, and if not throw an exception. |
417 | * @param $id a questionid. | |
d18675a8 | 418 | */ |
78e7a3dd | 419 | protected function ensure_question_loaded($id) { |
420 | if (isset($this->questions[$id]->_partiallyloaded)) { | |
36e413e3 | 421 | throw new moodle_quiz_exception($this, 'questionnotloaded', $id); |
422 | } | |
423 | } | |
424 | } | |
425 | ||
f7970e3c | 426 | |
b10c38a3 | 427 | /** |
428 | * This class extends the quiz class to hold data about the state of a particular attempt, | |
429 | * in addition to the data about the quiz. | |
d18675a8 | 430 | * |
f7970e3c TH |
431 | * @copyright 2008 Tim Hunt |
432 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
433 | * @since Moodle 2.0 | |
b10c38a3 | 434 | */ |
a1eb3a44 | 435 | class quiz_attempt { |
2de9be52 TH |
436 | |
437 | /** @var string to identify the in progress state. */ | |
438 | const IN_PROGRESS = 'inprogress'; | |
439 | /** @var string to identify the overdue state. */ | |
440 | const OVERDUE = 'overdue'; | |
441 | /** @var string to identify the finished state. */ | |
442 | const FINISHED = 'finished'; | |
443 | /** @var string to identify the abandoned state. */ | |
444 | const ABANDONED = 'abandoned'; | |
445 | ||
097dbfe1 TH |
446 | /** @var int maximum number of slots in the quiz for the review page to default to show all. */ |
447 | const MAX_SLOTS_FOR_DEFAULT_REVIEW_SHOW_ALL = 50; | |
448 | ||
9e83f3d1 | 449 | // Basic data. |
a1eb3a44 | 450 | protected $quizobj; |
36e413e3 | 451 | protected $attempt; |
452 | ||
32953f37 | 453 | /** @var question_usage_by_activity the question usage for this quiz attempt. */ |
2de9be52 | 454 | protected $quba; |
32953f37 TH |
455 | |
456 | /** @var array page no => array of slot numbers on the page in order. */ | |
457 | protected $pagelayout; | |
458 | ||
459 | /** @var array slot => displayed question number for this slot. (E.g. 1, 2, 3 or 'i'.) */ | |
460 | protected $questionnumbers; | |
461 | ||
462 | /** @var array slot => page number for this slot. */ | |
463 | protected $questionpages; | |
464 | ||
465 | /** @var mod_quiz_display_options cache for the appropriate review options. */ | |
4fc3d7e5 | 466 | protected $reviewoptions = null; |
36e413e3 | 467 | |
9e83f3d1 | 468 | // Constructor ============================================================= |
b10c38a3 | 469 | /** |
990650f9 TH |
470 | * Constructor assuming we already have the necessary data loaded. |
471 | * | |
472 | * @param object $attempt the row of the quiz_attempts table. | |
473 | * @param object $quiz the quiz object for this attempt and user. | |
474 | * @param object $cm the course_module object for this quiz. | |
475 | * @param object $course the row from the course table for the course we belong to. | |
08502b57 TH |
476 | * @param bool $loadquestions (optional) if true, the default, load all the details |
477 | * of the state of each question. Else just set up the basic details of the attempt. | |
990650f9 | 478 | */ |
08502b57 | 479 | public function __construct($attempt, $quiz, $cm, $course, $loadquestions = true) { |
990650f9 | 480 | $this->attempt = $attempt; |
a1eb3a44 | 481 | $this->quizobj = new quiz($quiz, $cm, $course); |
08502b57 TH |
482 | |
483 | if (!$loadquestions) { | |
484 | return; | |
485 | } | |
486 | ||
a1eb3a44 TH |
487 | $this->quba = question_engine::load_questions_usage_by_activity($this->attempt->uniqueid); |
488 | $this->determine_layout(); | |
489 | $this->number_questions(); | |
990650f9 TH |
490 | } |
491 | ||
492 | /** | |
56e82d99 TH |
493 | * Used by {create()} and {create_from_usage_id()}. |
494 | * @param array $conditions passed to $DB->get_record('quiz_attempts', $conditions). | |
b10c38a3 | 495 | */ |
c7df5006 | 496 | protected static function create_helper($conditions) { |
36e413e3 | 497 | global $DB; |
990650f9 | 498 | |
88f0eb15 | 499 | $attempt = $DB->get_record('quiz_attempts', $conditions, '*', MUST_EXIST); |
dd70d561 | 500 | $quiz = quiz_access_manager::load_quiz_and_settings($attempt->quiz); |
74df2951 | 501 | $course = $DB->get_record('course', array('id' => $quiz->course), '*', MUST_EXIST); |
88f0eb15 | 502 | $cm = get_coursemodule_from_instance('quiz', $quiz->id, $course->id, false, MUST_EXIST); |
a1eb3a44 | 503 | |
9e83f3d1 | 504 | // Update quiz with override information. |
990650f9 TH |
505 | $quiz = quiz_update_effective_access($quiz, $attempt->userid); |
506 | ||
507 | return new quiz_attempt($attempt, $quiz, $cm, $course); | |
36e413e3 | 508 | } |
509 | ||
56e82d99 TH |
510 | /** |
511 | * Static function to create a new quiz_attempt object given an attemptid. | |
512 | * | |
f7970e3c | 513 | * @param int $attemptid the attempt id. |
56e82d99 TH |
514 | * @return quiz_attempt the new quiz_attempt object |
515 | */ | |
c7df5006 | 516 | public static function create($attemptid) { |
56e82d99 TH |
517 | return self::create_helper(array('id' => $attemptid)); |
518 | } | |
519 | ||
520 | /** | |
521 | * Static function to create a new quiz_attempt object given a usage id. | |
522 | * | |
f7970e3c | 523 | * @param int $usageid the attempt usage id. |
56e82d99 TH |
524 | * @return quiz_attempt the new quiz_attempt object |
525 | */ | |
c7df5006 | 526 | public static function create_from_usage_id($usageid) { |
56e82d99 TH |
527 | return self::create_helper(array('uniqueid' => $usageid)); |
528 | } | |
529 | ||
b39b5d77 TH |
530 | /** |
531 | * @param string $state one of the state constants like IN_PROGRESS. | |
532 | * @return string the human-readable state name. | |
533 | */ | |
534 | public static function state_name($state) { | |
be18f589 | 535 | return quiz_attempt_state_name($state); |
b39b5d77 TH |
536 | } |
537 | ||
097dbfe1 TH |
538 | /** |
539 | * Parse attempt->layout to populate the other arrays the represent the layout. | |
540 | */ | |
541 | protected function determine_layout() { | |
a1eb3a44 TH |
542 | $this->pagelayout = array(); |
543 | ||
544 | // Break up the layout string into pages. | |
ccba5b88 | 545 | $pagelayouts = explode(',0', $this->attempt->layout); |
a1eb3a44 TH |
546 | |
547 | // Strip off any empty last page (normally there is one). | |
548 | if (end($pagelayouts) == '') { | |
549 | array_pop($pagelayouts); | |
36e413e3 | 550 | } |
a1eb3a44 TH |
551 | |
552 | // File the ids into the arrays. | |
553 | $this->pagelayout = array(); | |
554 | foreach ($pagelayouts as $page => $pagelayout) { | |
555 | $pagelayout = trim($pagelayout, ','); | |
556 | if ($pagelayout == '') { | |
557 | continue; | |
558 | } | |
559 | $this->pagelayout[$page] = explode(',', $pagelayout); | |
78e7a3dd | 560 | } |
a1eb3a44 TH |
561 | } |
562 | ||
097dbfe1 TH |
563 | /** |
564 | * Work out the number to display for each question/slot. | |
565 | */ | |
566 | protected function number_questions() { | |
a1eb3a44 TH |
567 | $number = 1; |
568 | foreach ($this->pagelayout as $page => $slots) { | |
569 | foreach ($slots as $slot) { | |
097dbfe1 | 570 | if ($length = $this->is_real_question($slot)) { |
32953f37 | 571 | $this->questionnumbers[$slot] = $number; |
097dbfe1 | 572 | $number += $length; |
a1eb3a44 | 573 | } else { |
32953f37 | 574 | $this->questionnumbers[$slot] = get_string('infoshort', 'quiz'); |
a1eb3a44 | 575 | } |
32953f37 | 576 | $this->questionpages[$slot] = $page; |
a1eb3a44 | 577 | } |
36e413e3 | 578 | } |
81d833ad | 579 | } |
580 | ||
ff2ec2cb TH |
581 | /** |
582 | * If the given page number is out of range (before the first page, or after | |
583 | * the last page, chnage it to be within range). | |
584 | * @param int $page the requested page number. | |
585 | * @return int a safe page number to use. | |
586 | */ | |
587 | public function force_page_number_into_range($page) { | |
588 | return min(max($page, 0), count($this->pagelayout) - 1); | |
589 | } | |
590 | ||
9e83f3d1 | 591 | // Simple getters ========================================================== |
a1eb3a44 TH |
592 | public function get_quiz() { |
593 | return $this->quizobj->get_quiz(); | |
594 | } | |
595 | ||
596 | public function get_quizobj() { | |
597 | return $this->quizobj; | |
598 | } | |
599 | ||
f7970e3c | 600 | /** @return int the course id. */ |
a1eb3a44 TH |
601 | public function get_courseid() { |
602 | return $this->quizobj->get_courseid(); | |
603 | } | |
604 | ||
f7970e3c | 605 | /** @return int the course id. */ |
a1eb3a44 TH |
606 | public function get_course() { |
607 | return $this->quizobj->get_course(); | |
608 | } | |
609 | ||
f7970e3c | 610 | /** @return int the quiz id. */ |
a1eb3a44 TH |
611 | public function get_quizid() { |
612 | return $this->quizobj->get_quizid(); | |
613 | } | |
614 | ||
615 | /** @return string the name of this quiz. */ | |
616 | public function get_quiz_name() { | |
617 | return $this->quizobj->get_quiz_name(); | |
618 | } | |
619 | ||
33c8d37b CF |
620 | /** @return int the quiz navigation method. */ |
621 | public function get_navigation_method() { | |
622 | return $this->quizobj->get_navigation_method(); | |
623 | } | |
624 | ||
a1eb3a44 TH |
625 | /** @return object the course_module object. */ |
626 | public function get_cm() { | |
627 | return $this->quizobj->get_cm(); | |
628 | } | |
629 | ||
630 | /** @return object the course_module object. */ | |
631 | public function get_cmid() { | |
632 | return $this->quizobj->get_cmid(); | |
633 | } | |
634 | ||
d18675a8 | 635 | /** |
f7970e3c | 636 | * @return bool wether the current user is someone who previews the quiz, |
a1eb3a44 | 637 | * rather than attempting it. |
d18675a8 | 638 | */ |
a1eb3a44 TH |
639 | public function is_preview_user() { |
640 | return $this->quizobj->is_preview_user(); | |
641 | } | |
642 | ||
f7970e3c | 643 | /** @return int the number of attempts allowed at this quiz (0 = infinite). */ |
a1eb3a44 TH |
644 | public function get_num_attempts_allowed() { |
645 | return $this->quizobj->get_num_attempts_allowed(); | |
646 | } | |
647 | ||
f7970e3c | 648 | /** @return int number fo pages in this quiz. */ |
a1eb3a44 TH |
649 | public function get_num_pages() { |
650 | return count($this->pagelayout); | |
36e413e3 | 651 | } |
652 | ||
d18675a8 | 653 | /** |
f7970e3c | 654 | * @param int $timenow the current time as a unix timestamp. |
25a03faa TH |
655 | * @return quiz_access_manager and instance of the quiz_access_manager class |
656 | * for this quiz at this time. | |
d18675a8 | 657 | */ |
a1eb3a44 TH |
658 | public function get_access_manager($timenow) { |
659 | return $this->quizobj->get_access_manager($timenow); | |
b55797b8 | 660 | } |
661 | ||
f7970e3c | 662 | /** @return int the attempt id. */ |
36e413e3 | 663 | public function get_attemptid() { |
664 | return $this->attempt->id; | |
665 | } | |
666 | ||
f7970e3c | 667 | /** @return int the attempt unique id. */ |
766df8f7 | 668 | public function get_uniqueid() { |
669 | return $this->attempt->uniqueid; | |
670 | } | |
671 | ||
b10c38a3 | 672 | /** @return object the row from the quiz_attempts table. */ |
36e413e3 | 673 | public function get_attempt() { |
674 | return $this->attempt; | |
675 | } | |
676 | ||
f7970e3c | 677 | /** @return int the number of this attemp (is it this user's first, second, ... attempt). */ |
78e7a3dd | 678 | public function get_attempt_number() { |
679 | return $this->attempt->attempt; | |
680 | } | |
681 | ||
96e5168c | 682 | /** @return string one of the quiz_attempt::IN_PROGRESS, FINISHED, OVERDUE or ABANDONED constants. */ |
2de9be52 TH |
683 | public function get_state() { |
684 | return $this->attempt->state; | |
685 | } | |
686 | ||
f7970e3c | 687 | /** @return int the id of the user this attempt belongs to. */ |
36e413e3 | 688 | public function get_userid() { |
689 | return $this->attempt->userid; | |
690 | } | |
c5fd1682 | 691 | |
5db82949 CF |
692 | /** @return int the current page of the attempt. */ |
693 | public function get_currentpage() { | |
694 | return $this->attempt->currentpage; | |
695 | } | |
36e413e3 | 696 | |
2de9be52 TH |
697 | public function get_sum_marks() { |
698 | return $this->attempt->sumgrades; | |
699 | } | |
700 | ||
25a03faa TH |
701 | /** |
702 | * @return bool whether this attempt has been finished (true) or is still | |
34b7d838 TH |
703 | * in progress (false). Be warned that this is not just state == self::FINISHED, |
704 | * it also includes self::ABANDONED. | |
25a03faa | 705 | */ |
36e413e3 | 706 | public function is_finished() { |
34b7d838 | 707 | return $this->attempt->state == self::FINISHED || $this->attempt->state == self::ABANDONED; |
36e413e3 | 708 | } |
709 | ||
f7970e3c | 710 | /** @return bool whether this attempt is a preview attempt. */ |
4fc3d7e5 | 711 | public function is_preview() { |
712 | return $this->attempt->preview; | |
713 | } | |
714 | ||
b55797b8 | 715 | /** |
716 | * Is this a student dealing with their own attempt/teacher previewing, | |
717 | * or someone with 'mod/quiz:viewreports' reviewing someone elses attempt. | |
7b6757b0 | 718 | * |
f7970e3c | 719 | * @return bool whether this situation should be treated as someone looking at their own |
b55797b8 | 720 | * attempt. The distinction normally only matters when an attempt is being reviewed. |
721 | */ | |
722 | public function is_own_attempt() { | |
723 | global $USER; | |
724 | return $this->attempt->userid == $USER->id && | |
725 | (!$this->is_preview_user() || $this->attempt->preview); | |
b55797b8 | 726 | } |
727 | ||
b3782c71 TH |
728 | /** |
729 | * @return bool whether this attempt is a preview belonging to the current user. | |
730 | */ | |
731 | public function is_own_preview() { | |
732 | global $USER; | |
733 | return $this->attempt->userid == $USER->id && | |
734 | $this->is_preview_user() && $this->attempt->preview; | |
735 | } | |
736 | ||
8032cd79 TH |
737 | /** |
738 | * Is the current user allowed to review this attempt. This applies when | |
739 | * {@link is_own_attempt()} returns false. | |
740 | * @return bool whether the review should be allowed. | |
741 | */ | |
742 | public function is_review_allowed() { | |
743 | if (!$this->has_capability('mod/quiz:viewreports')) { | |
744 | return false; | |
745 | } | |
746 | ||
747 | $cm = $this->get_cm(); | |
748 | if ($this->has_capability('moodle/site:accessallgroups') || | |
749 | groups_get_activity_groupmode($cm) != SEPARATEGROUPS) { | |
750 | return true; | |
751 | } | |
752 | ||
753 | // Check the users have at least one group in common. | |
754 | $teachersgroups = groups_get_activity_allowed_groups($cm); | |
25a03faa TH |
755 | $studentsgroups = groups_get_all_groups( |
756 | $cm->course, $this->attempt->userid, $cm->groupingid); | |
8032cd79 TH |
757 | return $teachersgroups && $studentsgroups && |
758 | array_intersect(array_keys($teachersgroups), array_keys($studentsgroups)); | |
759 | } | |
760 | ||
1c2e05c0 TH |
761 | /** |
762 | * Has the student, in this attempt, engaged with the quiz in a non-trivial way? | |
763 | * That is, is there any question worth a non-zero number of marks, where | |
764 | * the student has made some response that we have saved? | |
765 | * @return bool true if we have saved a response for at least one graded question. | |
766 | */ | |
767 | public function has_response_to_at_least_one_graded_question() { | |
768 | foreach ($this->quba->get_attempt_iterator() as $qa) { | |
769 | if ($qa->get_max_mark() == 0) { | |
770 | continue; | |
771 | } | |
772 | if ($qa->get_num_steps() > 1) { | |
773 | return true; | |
774 | } | |
775 | } | |
776 | return false; | |
777 | } | |
778 | ||
474aa125 TH |
779 | /** |
780 | * Get extra summary information about this attempt. | |
781 | * | |
782 | * Some behaviours may be able to provide interesting summary information | |
783 | * about the attempt as a whole, and this method provides access to that data. | |
784 | * To see how this works, try setting a quiz to one of the CBM behaviours, | |
785 | * and then look at the extra information displayed at the top of the quiz | |
786 | * review page once you have sumitted an attempt. | |
787 | * | |
788 | * In the return value, the array keys are identifiers of the form | |
789 | * qbehaviour_behaviourname_meaningfullkey. For qbehaviour_deferredcbm_highsummary. | |
790 | * The values are arrays with two items, title and content. Each of these | |
791 | * will be either a string, or a renderable. | |
792 | * | |
1c2e05c0 | 793 | * @param question_display_options $options the display options for this quiz attempt at this time. |
474aa125 TH |
794 | * @return array as described above. |
795 | */ | |
796 | public function get_additional_summary_data(question_display_options $options) { | |
797 | return $this->quba->get_summary_information($options); | |
798 | } | |
799 | ||
b2607ccc TH |
800 | /** |
801 | * Get the overall feedback corresponding to a particular mark. | |
802 | * @param $grade a particular grade. | |
803 | */ | |
804 | public function get_overall_feedback($grade) { | |
805 | return quiz_feedback_for_grade($grade, $this->get_quiz(), | |
2709ee45 | 806 | $this->quizobj->get_context()); |
b2607ccc TH |
807 | } |
808 | ||
a1eb3a44 TH |
809 | /** |
810 | * Wrapper round the has_capability funciton that automatically passes in the quiz context. | |
811 | */ | |
25a03faa | 812 | public function has_capability($capability, $userid = null, $doanything = true) { |
a1eb3a44 TH |
813 | return $this->quizobj->has_capability($capability, $userid, $doanything); |
814 | } | |
815 | ||
816 | /** | |
817 | * Wrapper round the require_capability funciton that automatically passes in the quiz context. | |
818 | */ | |
25a03faa | 819 | public function require_capability($capability, $userid = null, $doanything = true) { |
a1eb3a44 TH |
820 | return $this->quizobj->require_capability($capability, $userid, $doanything); |
821 | } | |
822 | ||
96c7d771 | 823 | /** |
824 | * Check the appropriate capability to see whether this user may review their own attempt. | |
825 | * If not, prints an error. | |
826 | */ | |
827 | public function check_review_capability() { | |
6a981b45 TH |
828 | if ($this->get_attempt_state() == mod_quiz_display_options::IMMEDIATELY_AFTER) { |
829 | $capability = 'mod/quiz:attempt'; | |
830 | } else { | |
831 | $capability = 'mod/quiz:reviewmyattempts'; | |
96c7d771 | 832 | } |
6a981b45 TH |
833 | |
834 | // These next tests are in a slighly funny order. The point is that the | |
835 | // common and most performance-critical case is students attempting a quiz | |
836 | // so we want to check that permisison first. | |
837 | ||
838 | if ($this->has_capability($capability)) { | |
839 | // User has the permission that lets you do the quiz as a student. Fine. | |
840 | return; | |
841 | } | |
842 | ||
843 | if ($this->has_capability('mod/quiz:viewreports') || | |
844 | $this->has_capability('mod/quiz:preview')) { | |
845 | // User has the permission that lets teachers review. Fine. | |
846 | return; | |
847 | } | |
848 | ||
849 | // They should not be here. Trigger the standard no-permission error | |
850 | // but using the name of the student capability. | |
851 | // We know this will fail. We just want the stadard exception thown. | |
852 | $this->require_capability($capability); | |
96c7d771 | 853 | } |
854 | ||
33c8d37b CF |
855 | /** |
856 | * Checks whether a user may navigate to a particular slot | |
857 | */ | |
858 | public function can_navigate_to($slot) { | |
859 | switch ($this->get_navigation_method()) { | |
860 | case QUIZ_NAVMETHOD_FREE: | |
861 | return true; | |
862 | break; | |
863 | case QUIZ_NAVMETHOD_SEQ: | |
864 | return false; | |
865 | break; | |
866 | } | |
867 | return true; | |
868 | } | |
869 | ||
d18675a8 | 870 | /** |
f7970e3c | 871 | * @return int one of the mod_quiz_display_options::DURING, |
a1eb3a44 | 872 | * IMMEDIATELY_AFTER, LATER_WHILE_OPEN or AFTER_CLOSE constants. |
d18675a8 | 873 | */ |
a1eb3a44 TH |
874 | public function get_attempt_state() { |
875 | return quiz_attempt_state($this->get_quiz(), $this->attempt); | |
78e7a3dd | 876 | } |
877 | ||
b10c38a3 | 878 | /** |
a1eb3a44 TH |
879 | * Wrapper that the correct mod_quiz_display_options for this quiz at the |
880 | * moment. | |
b10c38a3 | 881 | * |
a1eb3a44 | 882 | * @return question_display_options the render options for this user on this attempt. |
b10c38a3 | 883 | */ |
a1eb3a44 TH |
884 | public function get_display_options($reviewing) { |
885 | if ($reviewing) { | |
886 | if (is_null($this->reviewoptions)) { | |
7ee80cab | 887 | $this->reviewoptions = quiz_get_review_options($this->get_quiz(), |
a1eb3a44 TH |
888 | $this->attempt, $this->quizobj->get_context()); |
889 | } | |
890 | return $this->reviewoptions; | |
891 | ||
892 | } else { | |
893 | $options = mod_quiz_display_options::make_from_quiz($this->get_quiz(), | |
894 | mod_quiz_display_options::DURING); | |
895 | $options->flags = quiz_get_flag_option($this->attempt, $this->quizobj->get_context()); | |
896 | return $options; | |
36e413e3 | 897 | } |
36e413e3 | 898 | } |
899 | ||
da729916 TH |
900 | /** |
901 | * Wrapper that the correct mod_quiz_display_options for this quiz at the | |
902 | * moment. | |
903 | * | |
904 | * @param bool $reviewing true for review page, else attempt page. | |
905 | * @param int $slot which question is being displayed. | |
906 | * @param moodle_url $thispageurl to return to after the editing form is | |
907 | * submitted or cancelled. If null, no edit link will be generated. | |
908 | * | |
909 | * @return question_display_options the render options for this user on this | |
910 | * attempt, with extra info to generate an edit link, if applicable. | |
911 | */ | |
912 | public function get_display_options_with_edit_link($reviewing, $slot, $thispageurl) { | |
913 | $options = clone($this->get_display_options($reviewing)); | |
914 | ||
915 | if (!$thispageurl) { | |
916 | return $options; | |
917 | } | |
918 | ||
919 | if (!($reviewing || $this->is_preview())) { | |
920 | return $options; | |
921 | } | |
922 | ||
923 | $question = $this->quba->get_question($slot); | |
924 | if (!question_has_capability_on($question, 'edit', $question->category)) { | |
925 | return $options; | |
926 | } | |
927 | ||
928 | $options->editquestionparams['cmid'] = $this->get_cmid(); | |
929 | $options->editquestionparams['returnurl'] = $thispageurl; | |
930 | ||
931 | return $options; | |
932 | } | |
933 | ||
4fc3d7e5 | 934 | /** |
a1eb3a44 | 935 | * @param int $page page number |
f7970e3c | 936 | * @return bool true if this is the last page of the quiz. |
4fc3d7e5 | 937 | */ |
a1eb3a44 TH |
938 | public function is_last_page($page) { |
939 | return $page == count($this->pagelayout) - 1; | |
4fc3d7e5 | 940 | } |
941 | ||
b10c38a3 | 942 | /** |
a1eb3a44 TH |
943 | * Return the list of question ids for either a given page of the quiz, or for the |
944 | * whole quiz. | |
b10c38a3 | 945 | * |
a1eb3a44 TH |
946 | * @param mixed $page string 'all' or integer page number. |
947 | * @return array the reqested list of question ids. | |
b10c38a3 | 948 | */ |
a1eb3a44 | 949 | public function get_slots($page = 'all') { |
a1eb3a44 TH |
950 | if ($page === 'all') { |
951 | $numbers = array(); | |
952 | foreach ($this->pagelayout as $numbersonpage) { | |
953 | $numbers = array_merge($numbers, $numbersonpage); | |
954 | } | |
955 | return $numbers; | |
956 | } else { | |
957 | return $this->pagelayout[$page]; | |
958 | } | |
36e413e3 | 959 | } |
960 | ||
b10c38a3 | 961 | /** |
a1eb3a44 | 962 | * Get the question_attempt object for a particular question in this attempt. |
f7970e3c | 963 | * @param int $slot the number used to identify this question within this attempt. |
a1eb3a44 | 964 | * @return question_attempt |
b10c38a3 | 965 | */ |
a1eb3a44 TH |
966 | public function get_question_attempt($slot) { |
967 | return $this->quba->get_question_attempt($slot); | |
968 | } | |
b10c38a3 | 969 | |
a1eb3a44 TH |
970 | /** |
971 | * Is a particular question in this attempt a real question, or something like a description. | |
f7970e3c | 972 | * @param int $slot the number used to identify this question within this attempt. |
097dbfe1 TH |
973 | * @return int whether that question is a real question. Actually returns the |
974 | * question length, which could theoretically be greater than one. | |
a1eb3a44 TH |
975 | */ |
976 | public function is_real_question($slot) { | |
097dbfe1 | 977 | return $this->quba->get_question($slot)->length; |
36e413e3 | 978 | } |
979 | ||
62e76c67 | 980 | /** |
a1eb3a44 | 981 | * Is a particular question in this attempt a real question, or something like a description. |
f7970e3c TH |
982 | * @param int $slot the number used to identify this question within this attempt. |
983 | * @return bool whether that question is a real question. | |
62e76c67 | 984 | */ |
a1eb3a44 TH |
985 | public function is_question_flagged($slot) { |
986 | return $this->quba->get_question_attempt($slot)->is_flagged(); | |
62e76c67 | 987 | } |
988 | ||
36e413e3 | 989 | /** |
f7970e3c | 990 | * @param int $slot the number used to identify this question within this attempt. |
32953f37 TH |
991 | * @return string the displayed question number for the question in this slot. |
992 | * For example '1', '2', '3' or 'i'. | |
36e413e3 | 993 | */ |
a1eb3a44 | 994 | public function get_question_number($slot) { |
32953f37 TH |
995 | return $this->questionnumbers[$slot]; |
996 | } | |
997 | ||
998 | /** | |
999 | * @param int $slot the number used to identify this question within this attempt. | |
1000 | * @return int the page of the quiz this question appears on. | |
1001 | */ | |
1002 | public function get_question_page($slot) { | |
1003 | return $this->questionpages[$slot]; | |
a1eb3a44 TH |
1004 | } |
1005 | ||
1006 | /** | |
25a03faa TH |
1007 | * Return the grade obtained on a particular question, if the user is permitted |
1008 | * to see it. You must previously have called load_question_states to load the | |
1009 | * state data about this question. | |
a1eb3a44 | 1010 | * |
f7970e3c | 1011 | * @param int $slot the number used to identify this question within this attempt. |
25a03faa TH |
1012 | * @return string the formatted grade, to the number of decimal places specified |
1013 | * by the quiz. | |
a1eb3a44 TH |
1014 | */ |
1015 | public function get_question_name($slot) { | |
1016 | return $this->quba->get_question($slot)->name; | |
1017 | } | |
1018 | ||
1019 | /** | |
25a03faa TH |
1020 | * Return the grade obtained on a particular question, if the user is permitted |
1021 | * to see it. You must previously have called load_question_states to load the | |
1022 | * state data about this question. | |
a1eb3a44 | 1023 | * |
f7970e3c TH |
1024 | * @param int $slot the number used to identify this question within this attempt. |
1025 | * @param bool $showcorrectness Whether right/partial/wrong states should | |
a1eb3a44 | 1026 | * be distinguised. |
25a03faa TH |
1027 | * @return string the formatted grade, to the number of decimal places specified |
1028 | * by the quiz. | |
a1eb3a44 TH |
1029 | */ |
1030 | public function get_question_status($slot, $showcorrectness) { | |
1031 | return $this->quba->get_question_state_string($slot, $showcorrectness); | |
1032 | } | |
1033 | ||
97cdc1de TH |
1034 | /** |
1035 | * Return the grade obtained on a particular question, if the user is permitted | |
1036 | * to see it. You must previously have called load_question_states to load the | |
1037 | * state data about this question. | |
1038 | * | |
1039 | * @param int $slot the number used to identify this question within this attempt. | |
1040 | * @param bool $showcorrectness Whether right/partial/wrong states should | |
1041 | * be distinguised. | |
1042 | * @return string class name for this state. | |
1043 | */ | |
1044 | public function get_question_state_class($slot, $showcorrectness) { | |
1045 | return $this->quba->get_question_state_class($slot, $showcorrectness); | |
1046 | } | |
1047 | ||
a1eb3a44 TH |
1048 | /** |
1049 | * Return the grade obtained on a particular question. | |
1050 | * You must previously have called load_question_states to load the state | |
1051 | * data about this question. | |
1052 | * | |
f7970e3c | 1053 | * @param int $slot the number used to identify this question within this attempt. |
a1eb3a44 TH |
1054 | * @return string the formatted grade, to the number of decimal places specified by the quiz. |
1055 | */ | |
b2607ccc | 1056 | public function get_question_mark($slot) { |
a1eb3a44 TH |
1057 | return quiz_format_question_grade($this->get_quiz(), $this->quba->get_question_mark($slot)); |
1058 | } | |
1059 | ||
1060 | /** | |
1061 | * Get the time of the most recent action performed on a question. | |
f7970e3c TH |
1062 | * @param int $slot the number used to identify this question within this usage. |
1063 | * @return int timestamp. | |
a1eb3a44 TH |
1064 | */ |
1065 | public function get_question_action_time($slot) { | |
1066 | return $this->quba->get_question_action_time($slot); | |
36e413e3 | 1067 | } |
1068 | ||
2b2b6458 TH |
1069 | /** |
1070 | * Get the time remaining for an in-progress attempt, if the time is short | |
1071 | * enought that it would be worth showing a timer. | |
1072 | * @param int $timenow the time to consider as 'now'. | |
1073 | * @return int|false the number of seconds remaining for this attempt. | |
1074 | * False if there is no limit. | |
1075 | */ | |
8e771aed | 1076 | public function get_time_left_display($timenow) { |
2b2b6458 TH |
1077 | if ($this->attempt->state != self::IN_PROGRESS) { |
1078 | return false; | |
1079 | } | |
8e771aed | 1080 | return $this->get_access_manager($timenow)->get_time_left_display($this->attempt, $timenow); |
2b2b6458 | 1081 | } |
2de9be52 | 1082 | |
8e771aed | 1083 | |
2de9be52 TH |
1084 | /** |
1085 | * @return int the time when this attempt was submitted. 0 if it has not been | |
1086 | * submitted yet. | |
1087 | */ | |
1088 | public function get_submitted_date() { | |
1089 | return $this->attempt->timefinish; | |
1090 | } | |
1091 | ||
1092 | /** | |
1093 | * If the attempt is in an applicable state, work out the time by which the | |
1094 | * student should next do something. | |
1095 | * @return int timestamp by which the student needs to do something. | |
1096 | */ | |
9e83f3d1 | 1097 | public function get_due_date() { |
2b2b6458 TH |
1098 | $deadlines = array(); |
1099 | if ($this->quizobj->get_quiz()->timelimit) { | |
1100 | $deadlines[] = $this->attempt->timestart + $this->quizobj->get_quiz()->timelimit; | |
1101 | } | |
1102 | if ($this->quizobj->get_quiz()->timeclose) { | |
1103 | $deadlines[] = $this->quizobj->get_quiz()->timeclose; | |
1104 | } | |
1105 | if ($deadlines) { | |
1106 | $duedate = min($deadlines); | |
1107 | } else { | |
1108 | return false; | |
1109 | } | |
2de9be52 | 1110 | |
2b2b6458 | 1111 | switch ($this->attempt->state) { |
2de9be52 | 1112 | case self::IN_PROGRESS: |
2b2b6458 | 1113 | return $duedate; |
2de9be52 TH |
1114 | |
1115 | case self::OVERDUE: | |
2b2b6458 | 1116 | return $duedate + $this->quizobj->get_quiz()->graceperiod; |
2de9be52 TH |
1117 | |
1118 | default: | |
2b2b6458 | 1119 | throw new coding_exception('Unexpected state: ' . $this->attempt->state); |
2de9be52 TH |
1120 | } |
1121 | } | |
1122 | ||
9e83f3d1 | 1123 | // URLs related to this attempt ============================================ |
b10c38a3 | 1124 | /** |
a1eb3a44 TH |
1125 | * @return string quiz view url. |
1126 | */ | |
1127 | public function view_url() { | |
1128 | return $this->quizobj->view_url(); | |
1129 | } | |
1130 | ||
1131 | /** | |
1132 | * @return string the URL of this quiz's edit page. Needs to be POSTed to with a cmid parameter. | |
1133 | */ | |
da729916 TH |
1134 | public function start_attempt_url($slot = null, $page = -1) { |
1135 | if ($page == -1 && !is_null($slot)) { | |
32953f37 | 1136 | $page = $this->get_question_page($slot); |
da729916 TH |
1137 | } else { |
1138 | $page = 0; | |
1139 | } | |
1140 | return $this->quizobj->start_attempt_url($page); | |
a1eb3a44 TH |
1141 | } |
1142 | ||
1143 | /** | |
f7970e3c TH |
1144 | * @param int $slot if speified, the slot number of a specific question to link to. |
1145 | * @param int $page if specified, a particular page to link to. If not givem deduced | |
a1eb3a44 | 1146 | * from $slot, or goes to the first page. |
f7970e3c | 1147 | * @param int $questionid a question id. If set, will add a fragment to the URL |
b10c38a3 | 1148 | * to jump to a particuar question on the page. |
f7970e3c | 1149 | * @param int $thispage if not -1, the current page. Will cause links to other things on |
d18675a8 | 1150 | * this page to be output as only a fragment. |
b10c38a3 | 1151 | * @return string the URL to continue this attempt. |
1152 | */ | |
56e82d99 | 1153 | public function attempt_url($slot = null, $page = -1, $thispage = -1) { |
a1eb3a44 | 1154 | return $this->page_and_question_url('attempt', $slot, $page, false, $thispage); |
36e413e3 | 1155 | } |
1156 | ||
b10c38a3 | 1157 | /** |
1158 | * @return string the URL of this quiz's summary page. | |
1159 | */ | |
36e413e3 | 1160 | public function summary_url() { |
a1eb3a44 | 1161 | return new moodle_url('/mod/quiz/summary.php', array('attempt' => $this->attempt->id)); |
36e413e3 | 1162 | } |
1163 | ||
9f9eec1e | 1164 | /** |
1165 | * @return string the URL of this quiz's summary page. | |
1166 | */ | |
1167 | public function processattempt_url() { | |
a1eb3a44 | 1168 | return new moodle_url('/mod/quiz/processattempt.php'); |
9f9eec1e | 1169 | } |
1170 | ||
b10c38a3 | 1171 | /** |
f7970e3c TH |
1172 | * @param int $slot indicates which question to link to. |
1173 | * @param int $page if specified, the URL of this particular page of the attempt, otherwise | |
a1eb3a44 | 1174 | * the URL will go to the first page. If -1, deduce $page from $slot. |
097dbfe1 TH |
1175 | * @param bool|null $showall if true, the URL will be to review the entire attempt on one page, |
1176 | * and $page will be ignored. If null, a sensible default will be chosen. | |
f7970e3c | 1177 | * @param int $thispage if not -1, the current page. Will cause links to other things on |
d18675a8 | 1178 | * this page to be output as only a fragment. |
b10c38a3 | 1179 | * @return string the URL to review this attempt. |
1180 | */ | |
097dbfe1 | 1181 | public function review_url($slot = null, $page = -1, $showall = null, $thispage = -1) { |
a1eb3a44 | 1182 | return $this->page_and_question_url('review', $slot, $page, $showall, $thispage); |
78e7a3dd | 1183 | } |
1184 | ||
097dbfe1 TH |
1185 | /** |
1186 | * By default, should this script show all questions on one page for this attempt? | |
1187 | * @param string $script the script name, e.g. 'attempt', 'summary', 'review'. | |
1188 | * @return whether show all on one page should be on by default. | |
1189 | */ | |
1190 | public function get_default_show_all($script) { | |
1191 | return $script == 'review' && count($this->questionpages) < self::MAX_SLOTS_FOR_DEFAULT_REVIEW_SHOW_ALL; | |
1192 | } | |
1193 | ||
9e83f3d1 | 1194 | // Bits of content ========================================================= |
a1eb3a44 | 1195 | |
d755b0f5 TH |
1196 | /** |
1197 | * If $reviewoptions->attempt is false, meaning that students can't review this | |
1198 | * attempt at the moment, return an appropriate string explaining why. | |
1199 | * | |
1200 | * @param bool $short if true, return a shorter string. | |
1201 | * @return string an appropraite message. | |
1202 | */ | |
1203 | public function cannot_review_message($short = false) { | |
1204 | return $this->quizobj->cannot_review_message( | |
1205 | $this->get_attempt_state(), $short); | |
1206 | } | |
1207 | ||
d18675a8 | 1208 | /** |
ff2ec2cb | 1209 | * Initialise the JS etc. required all the questions on a page. |
d18675a8 | 1210 | * @param mixed $page a page number, or 'all'. |
1211 | */ | |
a1eb3a44 TH |
1212 | public function get_html_head_contributions($page = 'all', $showall = false) { |
1213 | if ($showall) { | |
1214 | $page = 'all'; | |
1215 | } | |
1216 | $result = ''; | |
1217 | foreach ($this->get_slots($page) as $slot) { | |
1218 | $result .= $this->quba->render_question_head_html($slot); | |
1219 | } | |
1220 | $result .= question_engine::initialise_js(); | |
1221 | return $result; | |
78e7a3dd | 1222 | } |
1223 | ||
d18675a8 | 1224 | /** |
1225 | * Initialise the JS etc. required by one question. | |
f7970e3c | 1226 | * @param int $questionid the question id. |
d18675a8 | 1227 | */ |
a1eb3a44 TH |
1228 | public function get_question_html_head_contributions($slot) { |
1229 | return $this->quba->render_question_head_html($slot) . | |
1230 | question_engine::initialise_js(); | |
b826bcef | 1231 | } |
1232 | ||
d18675a8 | 1233 | /** |
3c6185e9 TH |
1234 | * Print the HTML for the start new preview button, if the current user |
1235 | * is allowed to see one. | |
1236 | */ | |
1237 | public function restart_preview_button() { | |
1238 | global $OUTPUT; | |
1239 | if ($this->is_preview() && $this->is_preview_user()) { | |
1240 | return $OUTPUT->single_button(new moodle_url( | |
1241 | $this->start_attempt_url(), array('forcenew' => true)), | |
1242 | get_string('startnewpreview', 'quiz')); | |
1243 | } else { | |
1244 | return ''; | |
1245 | } | |
36e413e3 | 1246 | } |
1247 | ||
aafdb447 | 1248 | /** |
a1eb3a44 TH |
1249 | * Generate the HTML that displayes the question in its current state, with |
1250 | * the appropriate display options. | |
aafdb447 | 1251 | * |
f7970e3c TH |
1252 | * @param int $id the id of a question in this quiz attempt. |
1253 | * @param bool $reviewing is the being printed on an attempt or a review page. | |
da729916 | 1254 | * @param moodle_url $thispageurl the URL of the page this question is being printed on. |
a1eb3a44 | 1255 | * @return string HTML for the question in its current state. |
aafdb447 | 1256 | */ |
da729916 | 1257 | public function render_question($slot, $reviewing, $thispageurl = null) { |
a1eb3a44 | 1258 | return $this->quba->render_question($slot, |
da729916 | 1259 | $this->get_display_options_with_edit_link($reviewing, $slot, $thispageurl), |
32953f37 | 1260 | $this->get_question_number($slot)); |
a1eb3a44 | 1261 | } |
fb6dcdab | 1262 | |
a1eb3a44 TH |
1263 | /** |
1264 | * Like {@link render_question()} but displays the question at the past step | |
1265 | * indicated by $seq, rather than showing the latest step. | |
1266 | * | |
f7970e3c TH |
1267 | * @param int $id the id of a question in this quiz attempt. |
1268 | * @param int $seq the seq number of the past state to display. | |
1269 | * @param bool $reviewing is the being printed on an attempt or a review page. | |
a1eb3a44 TH |
1270 | * @param string $thispageurl the URL of the page this question is being printed on. |
1271 | * @return string HTML for the question in its current state. | |
1272 | */ | |
1273 | public function render_question_at_step($slot, $seq, $reviewing, $thispageurl = '') { | |
1274 | return $this->quba->render_question_at_step($slot, $seq, | |
2515be0e | 1275 | $this->get_display_options_with_edit_link($reviewing, $slot, $thispageurl), |
32953f37 | 1276 | $this->get_question_number($slot)); |
a1eb3a44 TH |
1277 | } |
1278 | ||
1279 | /** | |
1280 | * Wrapper round print_question from lib/questionlib.php. | |
1281 | * | |
f7970e3c | 1282 | * @param int $id the id of a question in this quiz attempt. |
a1eb3a44 TH |
1283 | */ |
1284 | public function render_question_for_commenting($slot) { | |
1285 | $options = $this->get_display_options(true); | |
1286 | $options->hide_all_feedback(); | |
1287 | $options->manualcomment = question_display_options::EDITABLE; | |
25a03faa | 1288 | return $this->quba->render_question($slot, $options, |
32953f37 | 1289 | $this->get_question_number($slot)); |
78e7a3dd | 1290 | } |
1291 | ||
a1eb3a44 TH |
1292 | /** |
1293 | * Check wheter access should be allowed to a particular file. | |
1294 | * | |
f7970e3c TH |
1295 | * @param int $id the id of a question in this quiz attempt. |
1296 | * @param bool $reviewing is the being printed on an attempt or a review page. | |
a1eb3a44 TH |
1297 | * @param string $thispageurl the URL of the page this question is being printed on. |
1298 | * @return string HTML for the question in its current state. | |
1299 | */ | |
56e82d99 | 1300 | public function check_file_access($slot, $reviewing, $contextid, $component, |
fe6ce234 | 1301 | $filearea, $args, $forcedownload) { |
56e82d99 TH |
1302 | return $this->quba->check_file_access($slot, $this->get_display_options($reviewing), |
1303 | $component, $filearea, $args, $forcedownload); | |
fe6ce234 DC |
1304 | } |
1305 | ||
d18675a8 | 1306 | /** |
1307 | * Get the navigation panel object for this attempt. | |
1308 | * | |
1309 | * @param $panelclass The type of panel, quiz_attempt_nav_panel or quiz_review_nav_panel | |
1310 | * @param $page the current page number. | |
1311 | * @param $showall whether we are showing the whole quiz on one page. (Used by review.php) | |
1312 | * @return quiz_nav_panel_base the requested object. | |
1313 | */ | |
b3782c71 TH |
1314 | public function get_navigation_panel(mod_quiz_renderer $output, |
1315 | $panelclass, $page, $showall = false) { | |
a1eb3a44 | 1316 | $panel = new $panelclass($this, $this->get_display_options(true), $page, $showall); |
b3782c71 TH |
1317 | |
1318 | $bc = new block_contents(); | |
83f93ec4 | 1319 | $bc->attributes['id'] = 'mod_quiz_navblock'; |
b3782c71 TH |
1320 | $bc->title = get_string('quiznavigation', 'quiz'); |
1321 | $bc->content = $output->navigation_panel($panel); | |
1322 | return $bc; | |
3c168fbb | 1323 | } |
36e413e3 | 1324 | |
d18675a8 | 1325 | /** |
83192608 | 1326 | * Given a URL containing attempt={this attempt id}, return an array of variant URLs |
b3782c71 | 1327 | * @param moodle_url $url a URL. |
d18675a8 | 1328 | * @return string HTML fragment. Comma-separated list of links to the other |
1329 | * attempts with the attempt number as the link text. The curent attempt is | |
1330 | * included but is not a link. | |
1331 | */ | |
b3782c71 | 1332 | public function links_to_other_attempts(moodle_url $url) { |
a1eb3a44 | 1333 | $attempts = quiz_get_user_attempts($this->get_quiz()->id, $this->attempt->userid, 'all'); |
f88fb62c | 1334 | if (count($attempts) <= 1) { |
1335 | return false; | |
1336 | } | |
b3782c71 TH |
1337 | |
1338 | $links = new mod_quiz_links_to_other_attempts(); | |
b55797b8 | 1339 | foreach ($attempts as $at) { |
1340 | if ($at->id == $this->attempt->id) { | |
b3782c71 | 1341 | $links->links[$at->attempt] = null; |
b55797b8 | 1342 | } else { |
b3782c71 | 1343 | $links->links[$at->attempt] = new moodle_url($url, array('attempt' => $at->id)); |
b55797b8 | 1344 | } |
1345 | } | |
b3782c71 | 1346 | return $links; |
b55797b8 | 1347 | } |
1348 | ||
a1eb3a44 TH |
1349 | // Methods for processing ================================================== |
1350 | ||
34b7d838 TH |
1351 | /** |
1352 | * Check this attempt, to see if there are any state transitions that should | |
8e771aed | 1353 | * happen automatically. This function will update the attempt checkstatetime. |
34b7d838 | 1354 | * @param int $timestamp the timestamp that should be stored as the modifed |
a403bce0 | 1355 | * @param bool $studentisonline is the student currently interacting with Moodle? |
34b7d838 | 1356 | */ |
a403bce0 | 1357 | public function handle_if_time_expired($timestamp, $studentisonline) { |
34b7d838 TH |
1358 | global $DB; |
1359 | ||
8e771aed | 1360 | $timeclose = $this->get_access_manager($timestamp)->get_end_time($this->attempt); |
a403bce0 | 1361 | |
8e771aed MP |
1362 | if ($timeclose === false || $this->is_preview()) { |
1363 | $this->update_timecheckstate(null); | |
1364 | return; // No time limit | |
1365 | } | |
1366 | if ($timestamp < $timeclose) { | |
1367 | $this->update_timecheckstate($timeclose); | |
34b7d838 TH |
1368 | return; // Time has not yet expired. |
1369 | } | |
1370 | ||
1371 | // If the attempt is already overdue, look to see if it should be abandoned ... | |
9e83f3d1 | 1372 | if ($this->attempt->state == self::OVERDUE) { |
8e771aed MP |
1373 | $timeoverdue = $timestamp - $timeclose; |
1374 | $graceperiod = $this->quizobj->get_quiz()->graceperiod; | |
1375 | if ($timeoverdue >= $graceperiod) { | |
a403bce0 | 1376 | $this->process_abandon($timestamp, $studentisonline); |
8e771aed MP |
1377 | } else { |
1378 | // Overdue time has not yet expired | |
1379 | $this->update_timecheckstate($timeclose + $graceperiod); | |
34b7d838 | 1380 | } |
34b7d838 TH |
1381 | return; // ... and we are done. |
1382 | } | |
1383 | ||
9e83f3d1 | 1384 | if ($this->attempt->state != self::IN_PROGRESS) { |
8e771aed | 1385 | $this->update_timecheckstate(null); |
34b7d838 TH |
1386 | return; // Attempt is already in a final state. |
1387 | } | |
1388 | ||
1389 | // Otherwise, we were in quiz_attempt::IN_PROGRESS, and time has now expired. | |
1390 | // Transition to the appropriate state. | |
1391 | switch ($this->quizobj->get_quiz()->overduehandling) { | |
1392 | case 'autosubmit': | |
1393 | $this->process_finish($timestamp, false); | |
1394 | return; | |
1395 | ||
1396 | case 'graceperiod': | |
a403bce0 | 1397 | $this->process_going_overdue($timestamp, $studentisonline); |
34b7d838 TH |
1398 | return; |
1399 | ||
1400 | case 'autoabandon': | |
a403bce0 | 1401 | $this->process_abandon($timestamp, $studentisonline); |
34b7d838 TH |
1402 | return; |
1403 | } | |
8e771aed MP |
1404 | |
1405 | // This is an overdue attempt with no overdue handling defined, so just abandon. | |
1406 | $this->process_abandon($timestamp, $studentisonline); | |
1407 | return; | |
34b7d838 TH |
1408 | } |
1409 | ||
d18675a8 | 1410 | /** |
a1eb3a44 TH |
1411 | * Process all the actions that were submitted as part of the current request. |
1412 | * | |
388f0473 JP |
1413 | * @param int $timestamp the timestamp that should be stored as the modifed |
1414 | * time in the database for these actions. If null, will use the current time. | |
1415 | * @param bool $becomingoverdue | |
1416 | * @param array|null $simulatedresponses If not null, then we are testing, and this is an array of simulated data, keys are slot | |
1417 | * nos and values are arrays representing student responses which will be passed to | |
1418 | * question_definition::prepare_simulated_post_data method and then have the | |
1419 | * appropriate prefix added. | |
1420 | */ | |
1421 | public function process_submitted_actions($timestamp, $becomingoverdue = false, $simulatedresponses = null) { | |
8f37f7fb | 1422 | global $DB; |
34b7d838 TH |
1423 | |
1424 | $transaction = $DB->start_delegated_transaction(); | |
1425 | ||
388f0473 JP |
1426 | if ($simulatedresponses !== null) { |
1427 | $simulatedpostdata = $this->quba->prepare_simulated_post_data($simulatedresponses); | |
1428 | } else { | |
1429 | $simulatedpostdata = null; | |
1430 | } | |
1431 | ||
1432 | $this->quba->process_all_actions($timestamp, $simulatedpostdata); | |
a1eb3a44 TH |
1433 | question_engine::save_questions_usage_by_activity($this->quba); |
1434 | ||
1435 | $this->attempt->timemodified = $timestamp; | |
34b7d838 | 1436 | if ($this->attempt->state == self::FINISHED) { |
a1eb3a44 TH |
1437 | $this->attempt->sumgrades = $this->quba->get_total_mark(); |
1438 | } | |
34b7d838 | 1439 | if ($becomingoverdue) { |
a403bce0 TH |
1440 | $this->process_going_overdue($timestamp, true); |
1441 | } else { | |
1442 | $DB->update_record('quiz_attempts', $this->attempt); | |
34b7d838 | 1443 | } |
88f0eb15 | 1444 | |
9e83f3d1 | 1445 | if (!$this->is_preview() && $this->attempt->state == self::FINISHED) { |
a1eb3a44 | 1446 | quiz_save_best_grade($this->get_quiz(), $this->get_userid()); |
766df8f7 | 1447 | } |
34b7d838 TH |
1448 | |
1449 | $transaction->allow_commit(); | |
766df8f7 | 1450 | } |
1451 | ||
59570888 TH |
1452 | /** |
1453 | * Process all the autosaved data that was part of the current request. | |
1454 | * | |
1455 | * @param int $timestamp the timestamp that should be stored as the modifed | |
1456 | * time in the database for these actions. If null, will use the current time. | |
1457 | */ | |
1458 | public function process_auto_save($timestamp) { | |
1459 | global $DB; | |
1460 | ||
1461 | $transaction = $DB->start_delegated_transaction(); | |
1462 | ||
1463 | $this->quba->process_all_autosaves($timestamp); | |
1464 | question_engine::save_questions_usage_by_activity($this->quba); | |
1465 | ||
1466 | $transaction->allow_commit(); | |
1467 | } | |
1468 | ||
d18675a8 | 1469 | /** |
a1eb3a44 TH |
1470 | * Update the flagged state for all question_attempts in this usage, if their |
1471 | * flagged state was changed in the request. | |
d18675a8 | 1472 | */ |
a1eb3a44 | 1473 | public function save_question_flags() { |
4be550c5 TH |
1474 | global $DB; |
1475 | ||
34b7d838 | 1476 | $transaction = $DB->start_delegated_transaction(); |
a1eb3a44 TH |
1477 | $this->quba->update_question_flags(); |
1478 | question_engine::save_questions_usage_by_activity($this->quba); | |
34b7d838 | 1479 | $transaction->allow_commit(); |
a1eb3a44 | 1480 | } |
e8f99abc | 1481 | |
34b7d838 | 1482 | public function process_finish($timestamp, $processsubmitted) { |
a403bce0 | 1483 | global $DB; |
34b7d838 TH |
1484 | |
1485 | $transaction = $DB->start_delegated_transaction(); | |
1486 | ||
1487 | if ($processsubmitted) { | |
1488 | $this->quba->process_all_actions($timestamp); | |
1489 | } | |
a1eb3a44 | 1490 | $this->quba->finish_all_questions($timestamp); |
e8f99abc | 1491 | |
a1eb3a44 | 1492 | question_engine::save_questions_usage_by_activity($this->quba); |
e8f99abc | 1493 | |
a1eb3a44 TH |
1494 | $this->attempt->timemodified = $timestamp; |
1495 | $this->attempt->timefinish = $timestamp; | |
1496 | $this->attempt->sumgrades = $this->quba->get_total_mark(); | |
a403bce0 | 1497 | $this->attempt->state = self::FINISHED; |
8e771aed | 1498 | $this->attempt->timecheckstate = null; |
88f0eb15 | 1499 | $DB->update_record('quiz_attempts', $this->attempt); |
766df8f7 | 1500 | |
a1eb3a44 | 1501 | if (!$this->is_preview()) { |
704f062b | 1502 | quiz_save_best_grade($this->get_quiz(), $this->attempt->userid); |
cbb8b55c | 1503 | |
9e83f3d1 | 1504 | // Trigger event. |
d900f1dc | 1505 | $this->fire_state_transition_event('\mod_quiz\event\attempt_submitted', $timestamp); |
cbb8b55c | 1506 | |
987c2d49 TH |
1507 | // Tell any access rules that care that the attempt is over. |
1508 | $this->get_access_manager($timestamp)->current_attempt_finished(); | |
36e413e3 | 1509 | } |
34b7d838 TH |
1510 | |
1511 | $transaction->allow_commit(); | |
1512 | } | |
1513 | ||
8e771aed MP |
1514 | /** |
1515 | * Update this attempt timecheckstate if necessary. | |
1516 | * @param int|null the timecheckstate | |
1517 | */ | |
1518 | public function update_timecheckstate($time) { | |
1519 | global $DB; | |
1520 | if ($this->attempt->timecheckstate !== $time) { | |
1521 | $this->attempt->timecheckstate = $time; | |
1522 | $DB->set_field('quiz_attempts', 'timecheckstate', $time, array('id'=>$this->attempt->id)); | |
1523 | } | |
1524 | } | |
1525 | ||
34b7d838 TH |
1526 | /** |
1527 | * Mark this attempt as now overdue. | |
1528 | * @param int $timestamp the time to deem as now. | |
a403bce0 | 1529 | * @param bool $studentisonline is the student currently interacting with Moodle? |
34b7d838 | 1530 | */ |
a403bce0 TH |
1531 | public function process_going_overdue($timestamp, $studentisonline) { |
1532 | global $DB; | |
1533 | ||
1534 | $transaction = $DB->start_delegated_transaction(); | |
1535 | $this->attempt->timemodified = $timestamp; | |
1536 | $this->attempt->state = self::OVERDUE; | |
8e771aed MP |
1537 | // If we knew the attempt close time, we could compute when the graceperiod ends. |
1538 | // Instead we'll just fix it up through cron. | |
1539 | $this->attempt->timecheckstate = $timestamp; | |
a403bce0 TH |
1540 | $DB->update_record('quiz_attempts', $this->attempt); |
1541 | ||
0a8b091d | 1542 | $this->fire_state_transition_event('\mod_quiz\event\attempt_becameoverdue', $timestamp); |
a403bce0 TH |
1543 | |
1544 | $transaction->allow_commit(); | |
1c2e05c0 TH |
1545 | |
1546 | quiz_send_overdue_message($this); | |
34b7d838 TH |
1547 | } |
1548 | ||
1549 | /** | |
1550 | * Mark this attempt as abandoned. | |
1551 | * @param int $timestamp the time to deem as now. | |
a403bce0 | 1552 | * @param bool $studentisonline is the student currently interacting with Moodle? |
34b7d838 | 1553 | */ |
a403bce0 TH |
1554 | public function process_abandon($timestamp, $studentisonline) { |
1555 | global $DB; | |
1556 | ||
1557 | $transaction = $DB->start_delegated_transaction(); | |
1558 | $this->attempt->timemodified = $timestamp; | |
1559 | $this->attempt->state = self::ABANDONED; | |
8e771aed | 1560 | $this->attempt->timecheckstate = null; |
a403bce0 TH |
1561 | $DB->update_record('quiz_attempts', $this->attempt); |
1562 | ||
d900f1dc | 1563 | $this->fire_state_transition_event('\mod_quiz\event\attempt_abandoned', $timestamp); |
a403bce0 TH |
1564 | |
1565 | $transaction->allow_commit(); | |
1566 | } | |
1567 | ||
1568 | /** | |
1569 | * Fire a state transition event. | |
d900f1dc FM |
1570 | * the same event information. |
1571 | * @param string $eventclass the event class name. | |
a403bce0 | 1572 | * @param int $timestamp the timestamp to include in the event. |
d900f1dc | 1573 | * @return void |
a403bce0 | 1574 | */ |
d900f1dc | 1575 | protected function fire_state_transition_event($eventclass, $timestamp) { |
a403bce0 | 1576 | global $USER; |
fdc729ea | 1577 | $quizrecord = $this->get_quiz(); |
d900f1dc FM |
1578 | $params = array( |
1579 | 'context' => $this->get_quizobj()->get_context(), | |
1580 | 'courseid' => $this->get_courseid(), | |
1581 | 'objectid' => $this->attempt->id, | |
1582 | 'relateduserid' => $this->attempt->userid, | |
1583 | 'other' => array( | |
fdc729ea RT |
1584 | 'submitterid' => CLI_SCRIPT ? null : $USER->id, |
1585 | 'quizid' => $quizrecord->id | |
d900f1dc FM |
1586 | ) |
1587 | ); | |
1588 | ||
1589 | $event = $eventclass::create($params); | |
1590 | $event->add_record_snapshot('quiz', $this->get_quiz()); | |
1591 | $event->add_record_snapshot('quiz_attempts', $this->get_attempt()); | |
1592 | $event->trigger(); | |
36e413e3 | 1593 | } |
78e7a3dd | 1594 | |
1595 | /** | |
a1eb3a44 TH |
1596 | * Print the fields of the comment form for questions in this attempt. |
1597 | * @param $slot which question to output the fields for. | |
1598 | * @param $prefix Prefix to add to all field names. | |
78e7a3dd | 1599 | */ |
a1eb3a44 TH |
1600 | public function question_print_comment_fields($slot, $prefix) { |
1601 | // Work out a nice title. | |
1602 | $student = get_record('user', 'id', $this->get_userid()); | |
1603 | $a = new object(); | |
1604 | $a->fullname = fullname($student, true); | |
1605 | $a->attempt = $this->get_attempt_number(); | |
1606 | ||
1607 | question_print_comment_fields($this->quba->get_question_attempt($slot), | |
1608 | $prefix, $this->get_display_options(true)->markdp, | |
1609 | get_string('gradingattempt', 'quiz_grading', $a)); | |
78e7a3dd | 1610 | } |
1611 | ||
9e83f3d1 | 1612 | // Private methods ========================================================= |
a1eb3a44 | 1613 | |
78e7a3dd | 1614 | /** |
d18675a8 | 1615 | * Get a URL for a particular question on a particular page of the quiz. |
1616 | * Used by {@link attempt_url()} and {@link review_url()}. | |
78e7a3dd | 1617 | * |
d18675a8 | 1618 | * @param string $script. Used in the URL like /mod/quiz/$script.php |
25a03faa TH |
1619 | * @param int $slot identifies the specific question on the page to jump to. |
1620 | * 0 to just use the $page parameter. | |
1621 | * @param int $page -1 to look up the page number from the slot, otherwise | |
1622 | * the page number to go to. | |
097dbfe1 TH |
1623 | * @param bool|null $showall if true, return a URL with showall=1, and not page number. |
1624 | * if null, then an intelligent default will be chosen. | |
f7970e3c | 1625 | * @param int $thispage the page we are currently on. Links to questions on this |
d18675a8 | 1626 | * page will just be a fragment #q123. -1 to disable this. |
1627 | * @return The requested URL. | |
78e7a3dd | 1628 | */ |
a1eb3a44 | 1629 | protected function page_and_question_url($script, $slot, $page, $showall, $thispage) { |
097dbfe1 TH |
1630 | |
1631 | $defaultshowall = $this->get_default_show_all($script); | |
1632 | if ($showall === null && ($page == 0 || $page == -1)) { | |
1633 | $showall = $defaultshowall; | |
1634 | } | |
1635 | ||
9e83f3d1 | 1636 | // Fix up $page. |
3c168fbb | 1637 | if ($page == -1) { |
097dbfe1 | 1638 | if ($slot !== null && !$showall) { |
32953f37 | 1639 | $page = $this->get_question_page($slot); |
78e7a3dd | 1640 | } else { |
1641 | $page = 0; | |
1642 | } | |
1643 | } | |
a1eb3a44 | 1644 | |
78e7a3dd | 1645 | if ($showall) { |
1646 | $page = 0; | |
1647 | } | |
d18675a8 | 1648 | |
fb6dcdab | 1649 | // Add a fragment to scroll down to the question. |
a1eb3a44 | 1650 | $fragment = ''; |
097dbfe1 | 1651 | if ($slot !== null) { |
a1eb3a44 | 1652 | if ($slot == reset($this->pagelayout[$page])) { |
d4a03c00 | 1653 | // First question on page, go to top. |
a1eb3a44 | 1654 | $fragment = '#'; |
d4a03c00 | 1655 | } else { |
a1eb3a44 | 1656 | $fragment = '#q' . $slot; |
d4a03c00 | 1657 | } |
78e7a3dd | 1658 | } |
d18675a8 | 1659 | |
a1eb3a44 TH |
1660 | // Work out the correct start to the URL. |
1661 | if ($thispage == $page) { | |
1662 | return new moodle_url($fragment); | |
36e413e3 | 1663 | |
36e413e3 | 1664 | } else { |
a1eb3a44 TH |
1665 | $url = new moodle_url('/mod/quiz/' . $script . '.php' . $fragment, |
1666 | array('attempt' => $this->attempt->id)); | |
097dbfe1 TH |
1667 | if ($page == 0 && $showall != $defaultshowall) { |
1668 | $url->param('showall', (int) $showall); | |
a1eb3a44 TH |
1669 | } else if ($page > 0) { |
1670 | $url->param('page', $page); | |
1671 | } | |
1672 | return $url; | |
36e413e3 | 1673 | } |
1674 | } | |
36e413e3 | 1675 | } |
3c168fbb | 1676 | |
f7970e3c | 1677 | |
b3782c71 TH |
1678 | /** |
1679 | * Represents a single link in the navigation panel. | |
1680 | * | |
1681 | * @copyright 2011 The Open University | |
1682 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
1683 | * @since Moodle 2.1 | |
1684 | */ | |
1685 | class quiz_nav_question_button implements renderable { | |
1686 | public $id; | |
1687 | public $number; | |
1688 | public $stateclass; | |
1689 | public $statestring; | |
1690 | public $currentpage; | |
1691 | public $flagged; | |
1692 | public $url; | |
1693 | } | |
1694 | ||
1695 | ||
d18675a8 | 1696 | /** |
1697 | * Represents the navigation panel, and builds a {@link block_contents} to allow | |
1698 | * it to be output. | |
1699 | * | |
f7970e3c TH |
1700 | * @copyright 2008 Tim Hunt |
1701 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
1702 | * @since Moodle 2.0 | |
d18675a8 | 1703 | */ |
3c168fbb | 1704 | abstract class quiz_nav_panel_base { |
38c9edd0 | 1705 | /** @var quiz_attempt */ |
3c168fbb | 1706 | protected $attemptobj; |
38c9edd0 | 1707 | /** @var question_display_options */ |
3c168fbb | 1708 | protected $options; |
38c9edd0 | 1709 | /** @var integer */ |
3c168fbb | 1710 | protected $page; |
38c9edd0 | 1711 | /** @var boolean */ |
d18675a8 | 1712 | protected $showall; |
3c168fbb | 1713 | |
a1eb3a44 TH |
1714 | public function __construct(quiz_attempt $attemptobj, |
1715 | question_display_options $options, $page, $showall) { | |
38c9edd0 TH |
1716 | $this->attemptobj = $attemptobj; |
1717 | $this->options = $options; | |
1718 | $this->page = $page; | |
1719 | $this->showall = $showall; | |
3c168fbb | 1720 | } |
1721 | ||
b3782c71 TH |
1722 | public function get_question_buttons() { |
1723 | $buttons = array(); | |
a1eb3a44 TH |
1724 | foreach ($this->attemptobj->get_slots() as $slot) { |
1725 | $qa = $this->attemptobj->get_question_attempt($slot); | |
1726 | $showcorrectness = $this->options->correctness && $qa->has_marks(); | |
a1eb3a44 | 1727 | |
b3782c71 TH |
1728 | $button = new quiz_nav_question_button(); |
1729 | $button->id = 'quiznavbutton' . $slot; | |
32953f37 | 1730 | $button->number = $this->attemptobj->get_question_number($slot); |
97cdc1de | 1731 | $button->stateclass = $qa->get_state_class($showcorrectness); |
33c8d37b | 1732 | $button->navmethod = $this->attemptobj->get_navigation_method(); |
b3782c71 TH |
1733 | if (!$showcorrectness && $button->stateclass == 'notanswered') { |
1734 | $button->stateclass = 'complete'; | |
1735 | } | |
79a46626 | 1736 | $button->statestring = $this->get_state_string($qa, $showcorrectness); |
0328341f | 1737 | $button->currentpage = $this->showall || $this->attemptobj->get_question_page($slot) == $this->page; |
b3782c71 TH |
1738 | $button->flagged = $qa->is_flagged(); |
1739 | $button->url = $this->get_question_url($slot); | |
1740 | $buttons[] = $button; | |
a1eb3a44 TH |
1741 | } |
1742 | ||
b3782c71 | 1743 | return $buttons; |
c752264f | 1744 | } |
1745 | ||
79a46626 | 1746 | protected function get_state_string(question_attempt $qa, $showcorrectness) { |
0eafc988 | 1747 | if ($qa->get_question()->length > 0) { |
79a46626 TH |
1748 | return $qa->get_state_string($showcorrectness); |
1749 | } | |
1750 | ||
1751 | // Special case handling for 'information' items. | |
1752 | if ($qa->get_state() == question_state::$todo) { | |
1753 | return get_string('notyetviewed', 'quiz'); | |
1754 | } else { | |
1755 | return get_string('viewed', 'quiz'); | |
1756 | } | |
1757 | } | |
1758 | ||
b3782c71 | 1759 | public function render_before_button_bits(mod_quiz_renderer $output) { |
d18675a8 | 1760 | return ''; |
1761 | } | |
3c168fbb | 1762 | |
0eb253f7 TH |
1763 | abstract public function render_end_bits(mod_quiz_renderer $output); |
1764 | ||
1765 | protected function render_restart_preview_link($output) { | |
b3782c71 TH |
1766 | if (!$this->attemptobj->is_own_preview()) { |
1767 | return ''; | |
1768 | } | |
1769 | return $output->restart_preview_button(new moodle_url( | |
1770 | $this->attemptobj->start_attempt_url(), array('forcenew' => true))); | |
1771 | } | |
3c168fbb | 1772 | |
c7df5006 | 1773 | protected abstract function get_question_url($slot); |
d18675a8 | 1774 | |
b3782c71 TH |
1775 | public function user_picture() { |
1776 | global $DB; | |
1ecd3c30 | 1777 | if ($this->attemptobj->get_quiz()->showuserpicture == QUIZ_SHOWIMAGE_NONE) { |
e28bbd89 TH |
1778 | return null; |
1779 | } | |
a733c4b9 | 1780 | $user = $DB->get_record('user', array('id' => $this->attemptobj->get_userid())); |
b3782c71 TH |
1781 | $userpicture = new user_picture($user); |
1782 | $userpicture->courseid = $this->attemptobj->get_courseid(); | |
1ecd3c30 JA |
1783 | if ($this->attemptobj->get_quiz()->showuserpicture == QUIZ_SHOWIMAGE_LARGE) { |
1784 | $userpicture->size = true; | |
1785 | } | |
b3782c71 | 1786 | return $userpicture; |
3c168fbb | 1787 | } |
0328341f K |
1788 | |
1789 | /** | |
1790 | * Return 'allquestionsononepage' as CSS class name when $showall is set, | |
1791 | * otherwise, return 'multipages' as CSS class name. | |
1792 | * @return string, CSS class name | |
1793 | */ | |
1794 | public function get_button_container_class() { | |
1795 | // Quiz navigation is set on 'Show all questions on one page'. | |
1796 | if ($this->showall) { | |
1797 | return 'allquestionsononepage'; | |
1798 | } | |
1799 | // Quiz navigation is set on 'Show one page at a time'. | |
1800 | return 'multipages'; | |
1801 | } | |
3c168fbb | 1802 | } |
1803 | ||
f7970e3c | 1804 | |
d18675a8 | 1805 | /** |
1806 | * Specialisation of {@link quiz_nav_panel_base} for the attempt quiz page. | |
1807 | * | |
f7970e3c TH |
1808 | * @copyright 2008 Tim Hunt |
1809 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
1810 | * @since Moodle 2.0 | |
d18675a8 | 1811 | */ |
3c168fbb | 1812 | class quiz_attempt_nav_panel extends quiz_nav_panel_base { |
b3782c71 | 1813 | public function get_question_url($slot) { |
33c8d37b CF |
1814 | if ($this->attemptobj->can_navigate_to($slot)) { |
1815 | return $this->attemptobj->attempt_url($slot, -1, $this->page); | |
1816 | } else { | |
1817 | return null; | |
1818 | } | |
3c168fbb | 1819 | } |
1820 | ||
b3782c71 TH |
1821 | public function render_before_button_bits(mod_quiz_renderer $output) { |
1822 | return html_writer::tag('div', get_string('navnojswarning', 'quiz'), | |
1823 | array('id' => 'quiznojswarning')); | |
3c168fbb | 1824 | } |
1825 | ||
b3782c71 TH |
1826 | public function render_end_bits(mod_quiz_renderer $output) { |
1827 | return html_writer::link($this->attemptobj->summary_url(), | |
c9272e87 | 1828 | get_string('endtest', 'quiz'), array('class' => 'endtestlink')) . |
2b2b6458 | 1829 | $output->countdown_timer($this->attemptobj, time()) . |
0eb253f7 | 1830 | $this->render_restart_preview_link($output); |
3c168fbb | 1831 | } |
1832 | } | |
1833 | ||
f7970e3c | 1834 | |
d18675a8 | 1835 | /** |
1836 | * Specialisation of {@link quiz_nav_panel_base} for the review quiz page. | |
1837 | * | |
f7970e3c TH |
1838 | * @copyright 2008 Tim Hunt |
1839 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
1840 | * @since Moodle 2.0 | |
d18675a8 | 1841 | */ |
3c168fbb | 1842 | class quiz_review_nav_panel extends quiz_nav_panel_base { |
b3782c71 | 1843 | public function get_question_url($slot) { |
a1eb3a44 | 1844 | return $this->attemptobj->review_url($slot, -1, $this->showall, $this->page); |
3c168fbb | 1845 | } |
1846 | ||
b3782c71 | 1847 | public function render_end_bits(mod_quiz_renderer $output) { |
d18675a8 | 1848 | $html = ''; |
1849 | if ($this->attemptobj->get_num_pages() > 1) { | |
1850 | if ($this->showall) { | |
b3782c71 TH |
1851 | $html .= html_writer::link($this->attemptobj->review_url(null, 0, false), |
1852 | get_string('showeachpage', 'quiz')); | |
d18675a8 | 1853 | } else { |
b3782c71 TH |
1854 | $html .= html_writer::link($this->attemptobj->review_url(null, 0, true), |
1855 | get_string('showall', 'quiz')); | |
d18675a8 | 1856 | } |
1857 | } | |
d869de66 | 1858 | $html .= $output->finish_review_link($this->attemptobj); |
0eb253f7 | 1859 | $html .= $this->render_restart_preview_link($output); |
3c168fbb | 1860 | return $html; |
1861 | } | |
1862 | } |