MDL-15452 - Put the OU quiz navigation improvements into the Moodle codebase
[moodle.git] / mod / quiz / attemptlib.php
CommitLineData
36e413e3 1<?php
2
3/**
4 * This class handles loading all the information about a quiz attempt into memory,
5 * and making it available for attemtp.php, summary.php and review.php.
6 * Initially, it only loads a minimal amout of information about each attempt - loading
7 * extra information only when necessary or when asked. The class tracks which questions
8 * are loaded.
9 */
10
11require_once("../../config.php");
12
13/**
14 * Class for quiz exceptions.
15 *
16 */
17class moodle_quiz_exception extends moodle_exception {
18 function __construct($quizobj, $errorcode, $a = NULL, $link = '', $debuginfo = null) {
19 if (!$link) {
20 $link = $quizobj->view_url();
21 }
22 parent::__construct($errorcode, 'quiz', $link, $a, $debuginfo);
23 }
24}
25
26class quiz {
27 // Fields initialised in the constructor.
28 protected $course;
29 protected $cm;
30 protected $quiz;
31 protected $context;
32
33 // Fields set later if that data is needed.
34 protected $accessmanager = null;
35 protected $reviewoptions = null;
36 protected $questions = array();
37 protected $questionsnumbered = false;
38
39 // Constructor =========================================================================
40 function __construct($quiz, $cm, $course) {
41 $this->quiz = $quiz;
42 $this->cm = $cm;
43 $this->course = $course;
44 $this->context = get_context_instance(CONTEXT_MODULE, $cm->id);
45 }
46
47 // Functions for loading more data =====================================================
48 public function load_questions_on_page($page) {
49 $this->load_questions(quiz_questions_on_page($this->quiz->layout, $page));
50 }
51
52 /**
53 * Load some or all of the queestions for this quiz.
54 *
55 * @param string $questionlist comma-separate list of question ids. Blank for all.
56 */
57 public function load_questions($questionlist = '') {
58 if (!$questionlist) {
59 $questionlist = quiz_questions_in_quiz($this->quiz->layout);
60 }
61 $newquestions = question_load_questions($questionlist, 'qqi.grade AS maxgrade, qqi.id AS instance',
62 '{quiz_question_instances} qqi ON qqi.quiz = ' . $this->quiz->id . ' AND q.id = qqi.question');
63 if (is_string($newquestions)) {
64 throw new moodle_quiz_exception($this, 'loadingquestionsfailed', $newquestions);
65 }
66 $this->questions = $this->questions + $newquestions;
67 $this->questionsnumbered = false;
68 }
69
70 // Simple getters ======================================================================
71 public function get_courseid() {
72 return $this->course->id;
73 }
74
75 public function get_quizid() {
76 return $this->quiz->id;
77 }
78
79 public function get_quiz() {
80 return $this->quiz;
81 }
82
83 public function get_quiz_name() {
84 return $this->quiz->name;
85 }
86
87 public function get_cmid() {
88 return $this->cm->id;
89 }
90
91 public function get_cm() {
92 return $this->cm;
93 }
94
95 public function get_question($id) {
96 $this->ensure_question_loaded($id);
97 return $this->questions[$id];
98 }
99
100 public function get_access_manager($timenow) {
101 if (is_null($this->accessmanager)) {
102 $this->accessmanager = new quiz_access_manager($this->quiz, $timenow,
103 has_capability('mod/quiz:ignoretimelimits', $this->context, NULL, false));
104 }
105 return $this->accessmanager;
106 }
107
108 // URLs related to this attempt ========================================================
109 public function view_url() {
110 global $CFG;
111 return $CFG->wwwroot . '/mod/quiz/view.php?id=' . $this->cm->id;
112 }
113
114 // Bits of content =====================================================================
115 public function update_module_button() {
116 if (has_capability('moodle/course:manageactivities',
117 get_context_instance(CONTEXT_COURSE, $this->course->id))) {
118 return update_module_button($this->cm->id, $this->course->id, get_string('modulename', 'quiz'));
119 } else {
120 return '';
121 }
122 }
123
124 public function navigation($title) {
125 return build_navigation($title, $this->cm);
126 }
127
128 // Private methods =====================================================================
129 private function ensure_question_loaded($id) {
130 if (!array_key_exists($id, $this->questions)) {
131 throw new moodle_quiz_exception($this, 'questionnotloaded', $id);
132 }
133 }
134}
135
136class quiz_attempt extends quiz {
137 // Fields initialised in the constructor.
138 protected $attempt;
139
140 // Fields set later if that data is needed.
141 protected $ispreview = null;
142 protected $states = array();
143
144 // Constructor =========================================================================
145 function __construct($attemptid) {
146 global $DB;
147 if (!$this->attempt = quiz_load_attempt($attemptid)) {
148 throw new moodle_exception('invalidattemptid', 'quiz');
149 }
150 if (!$quiz = $DB->get_record('quiz', array('id' => $this->attempt->quiz))) {
151 throw new moodle_exception('invalidquizid', 'quiz');
152 }
153 if (!$course = $DB->get_record('course', array('id' => $quiz->course))) {
154 throw new moodle_exception('invalidcoursemodule');
155 }
156 if (!$cm = get_coursemodule_from_instance('quiz', $quiz->id, $course->id)) {
157 throw new moodle_exception('invalidcoursemodule');
158 }
159 parent::__construct($quiz, $cm, $course);
160 }
161
162 // Functions for loading more data =====================================================
163 public function load_questions_on_page($page) {
164 $this->load_questions(quiz_questions_on_page($this->attempt->layout, $page));
165 }
166
167 /**
168 * Load some or all of the queestions for this quiz.
169 *
170 * @param string $questionlist comma-separate list of question ids. Blank for all.
171 */
172 public function load_questions($questionlist = '') {
173 if (!$questionlist) {
174 $questionlist = quiz_questions_in_quiz($this->attempt->layout);
175 }
176 parent::load_questions($questionlist);
177 }
178
179 public function load_question_states() {
180 $questionstodo = array_diff_key($this->questions, $this->states);
181 if (!$newstates = get_question_states($questionstodo, $this->quiz, $this->attempt)) {
182 throw new moodle_quiz_exception($this, 'cannotrestore');
183 }
184 $this->states = $this->states + $newstates;
185 }
186
187 /**
188 * Number the loaded quetsions.
189 *
190 * At the moment, this assumes for simplicity that the loaded questions are contiguous.
191 */
192 public function number_questions($page = 'all') {
193 if ($this->questionsnumbered) {
194 return;
195 }
196 if ($page != 'all') {
197 $pagelist = quiz_questions_in_page($this->attempt->layout, $page);
198 $number = quiz_first_questionnumber($this->attempt->layout, $pagelist);
199 } else {
200 $number = 1;
201 }
202 $questionids = $this->get_question_ids($page);
203 foreach ($questionids as $id) {
204 if ($this->questions[$id]->length > 0) {
205 $this->questions[$id]->number = $number;
206 $number += $this->questions[$id]->length;
207 } else {
208 $this->questions[$id]->number = get_string('infoshort', 'quiz');
209 }
210 }
211 }
212
213 // Simple getters ======================================================================
214 public function get_attemptid() {
215 return $this->attempt->id;
216 }
217
218 public function get_attempt() {
219 return $this->attempt;
220 }
221
222 public function get_userid() {
223 return $this->attempt->userid;
224 }
225
226 public function is_finished() {
227 return $this->attempt->timefinish != 0;
228 }
229
230 public function is_preview() {
231 if (is_null($this->ispreview)) {
232 $this->ispreview = has_capability('mod/quiz:preview', $this->context);
233 }
234 return $this->ispreview;
235 }
236
237 public function get_review_options() {
238 if (is_null($this->reviewoptions)) {
239 $this->reviewoptions = quiz_get_reviewoptions($this->quiz, $this->attempt, $this->context);
240 }
241 return $this->reviewoptions;
242 }
243
244 public function get_question_ids($page = 'all') {
245 if ($page == 'all') {
246 $questionlist = quiz_questions_in_quiz($this->attempt->layout);
247 } else {
248 $questionlist = quiz_questions_in_page($this->attempt->layout, $page);
249 }
250 return explode(',', $questionlist);
251 }
252
253 public function get_question_iterator($page = 'all') {
254 return new quiz_attempt_question_iterator($this, $page);
255 }
256
257 public function get_question_status($questionid) {
258 //TODO
259 return 'FROG';
260 }
261
262 /**
263 * Return the grade obtained on a particular question, if the user ispermitted to see it.
264 *
265 * @param integer $questionid
266 * @return string the formatted grade, to the number of decimal places specified by the quiz.
267 */
268 public function get_question_score($questionid) {
269 $this->ensure_state_loaded($questionid);
270 $options = quiz_get_renderoptions($this->quiz->review, $this->states[$questionid]);
271 if ($options->scores) {
272 return round($this->states[$questionid]->last_graded->grade, $this->quiz->decimalpoints);
273 } else {
274 return '';
275 }
276 }
277
278 // URLs related to this attempt ========================================================
279 public function attempt_url($page = 0, $question = false) {
280 global $CFG;
281 $fragment = '';
282 if ($question) {
283 $fragment = '#q' . $question;
284 }
285 return $CFG->wwwroot . '/mod/quiz/attempt.php?id=' .
286 $this->cm->id . '$amp;page=' . $page . $fragment;
287 }
288
289 public function summary_url() {
290 global $CFG;
291 return $CFG->wwwroot . '/mod/quiz/summary.php?attempt=' . $this->attempt->id;
292 }
293
294 public function review_url($page = 0, $question = false, $showall = false) {
295 global $CFG;
296 $fragment = '';
297 if ($question) {
298 $fragment = '#q' . $question;
299 }
300 $param = '';
301 if ($showall) {
302 $param = '$amp;showall=1';
303 } else if ($page) {
304 $param = '$amp;page=' . $page;
305 }
306 return $CFG->wwwroot . '/mod/quiz/review.php?attempt=' .
307 $this->attempt->id . $param . $fragment;
308 }
309
310
311 // Private methods =====================================================================
312 private function ensure_state_loaded($id) {
313 if (!array_key_exists($id, $this->states)) {
314 throw new moodle_quiz_exception($this, 'statenotloaded', $id);
315 }
316 }
317}
318
319class quiz_attempt_question_iterator implements Iterator {
320 private $attemptobj;
321 private $questionids;
322 public function __construct(quiz_attempt $attemptobj, $page = 'all') {
323 $this->attemptobj = $attemptobj;
324 $attemptobj->number_questions($page);
325 $this->questionids = $attemptobj->get_question_ids($page);
326 }
327
328 public function rewind() {
329 reset($this->questionids);
330 }
331
332 public function current() {
333 $id = current($this->questionids);
334 if ($id) {
335 return $this->attemptobj->get_question($id);
336 } else {
337 return false;
338 }
339 }
340
341 public function key() {
342 $id = current($this->questionids);
343 if ($id) {
344 return $this->attemptobj->get_question($id)->number;
345 } else {
346 return false;
347 }
348 return $this->attemptobj->get_question(current($this->questionids))->number;
349 }
350
351 public function next() {
352 $id = next($this->questionids);
353 if ($id) {
354 return $this->attemptobj->get_question($id);
355 } else {
356 return false;
357 }
358 }
359
360 public function valid() {
361 return $this->current() !== false;
362 }
363}
364?>