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 | |
11 | require_once("../../config.php"); |
12 | |
13 | /** |
14 | * Class for quiz exceptions. |
15 | * |
16 | */ |
17 | class 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 | |
26 | class 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 | |
136 | class 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 | |
319 | class 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 | ?> |