MDL-27408 Database upgrade for adaptive mode.
[moodle.git] / question / engine / questionattemptstep.php
CommitLineData
dcd03928
TH
1<?php
2
3// This file is part of Moodle - http://moodle.org/
4//
5// Moodle is free software: you can redistribute it and/or modify
6// it under the terms of the GNU General Public License as published by
7// the Free Software Foundation, either version 3 of the License, or
8// (at your option) any later version.
9//
10// Moodle is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13// GNU General Public License for more details.
14//
15// You should have received a copy of the GNU General Public License
16// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17
18/**
19 * This file defines the question attempt step class, and a few related classes.
20 *
21 * @package moodlecore
22 * @subpackage questionengine
23 * @copyright 2009 The Open University
24 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25 */
26
27
28defined('MOODLE_INTERNAL') || die();
29
30
31/**
32 * Stores one step in a {@link question_attempt}.
33 *
34 * The most important attributes of a step are the state, which is one of the
35 * {@link question_state} constants, the fraction, which may be null, or a
36 * number bewteen the attempt's minfraction and 1.0, and the array of submitted
37 * data, about which more later.
38 *
39 * A step also tracks the time it was created, and the user responsible for
40 * creating it.
41 *
42 * The submitted data is basically just an array of name => value pairs, with
43 * certain conventions about the to divide the variables into four = two times two
44 * categories.
45 *
46 * Variables may either belong to the behaviour, in which case the
47 * name starts with a '-', or they may belong to the question type in which case
48 * they name does not start with a '-'.
49 *
50 * Second, variables may either be ones that came form the original request, in
51 * which case the name does not start with an _, or they are cached values that
52 * were created during processing, in which case the name does start with an _.
53 *
54 * That is, each name will start with one of '', '_'. '-' or '-_'. The remainder
55 * of the name should match the regex [a-z][a-z0-9]*.
56 *
57 * These variables can be accessed with {@link get_behaviour_var()} and {@link get_qt_var()},
58 * - to be clear, ->get_behaviour_var('x') gets the variable with name '-x' -
59 * and values whose names start with '_' can be set using {@link set_behaviour_var()}
60 * and {@link set_qt_var()}. There are some other methods like {@link has_behaviour_var()}
61 * to check wether a varaible with a particular name is set, and {@link get_behaviour_data()}
62 * to get all the behaviour data as an associative array.
63 *
64 * @copyright 2009 The Open University
65 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
66 */
67class question_attempt_step {
68 /** @var integer if this attempts is stored in the question_attempts table, the id of that row. */
69 private $id = null;
70
71 /** @var question_state one of the {@link question_state} constants. The state after this step. */
72 private $state;
73
74 /** @var null|number the fraction (grade on a scale of minfraction .. 1.0) or null. */
75 private $fraction = null;
76
77 /** @var integer the timestamp when this step was created. */
78 private $timecreated;
79
80 /** @var integer the id of the user resonsible for creating this step. */
81 private $userid;
82
83 /** @var array name => value pairs. The submitted data. */
84 private $data;
85
86 /** @var array name => array of {@link stored_file}s. Caches the contents of file areas. */
87 private $files = array();
88
89 /**
90 * You should not need to call this constructor in your own code. Steps are
91 * normally created by {@link question_attempt} methods like
92 * {@link question_attempt::process_action()}.
93 * @param array $data the submitted data that defines this step.
94 * @param int $timestamp the time to record for the action. (If not given, use now.)
95 * @param int $userid the user to attribute the aciton to. (If not given, use the current user.)
96 */
97 public function __construct($data = array(), $timecreated = null, $userid = null) {
98 global $USER;
99 $this->state = question_state::$unprocessed;
100 $this->data = $data;
101 if (is_null($timecreated)) {
102 $this->timecreated = time();
103 } else {
104 $this->timecreated = $timecreated;
105 }
106 if (is_null($userid)) {
107 $this->userid = $USER->id;
108 } else {
109 $this->userid = $userid;
110 }
111 }
112
113 /** @return question_state The state after this step. */
114 public function get_state() {
115 return $this->state;
116 }
117
118 /**
119 * Set the state. Normally only called by behaviours.
120 * @param question_state $state one of the {@link question_state} constants.
121 */
122 public function set_state($state) {
123 $this->state = $state;
124 }
125
126 /**
127 * @return null|number the fraction (grade on a scale of minfraction .. 1.0)
128 * or null if this step has not been marked.
129 */
130 public function get_fraction() {
131 return $this->fraction;
132 }
133
134 /**
135 * Set the fraction. Normally only called by behaviours.
136 * @param null|number $fraction the fraction to set.
137 */
138 public function set_fraction($fraction) {
139 $this->fraction = $fraction;
140 }
141
142 /** @return int the id of the user resonsible for creating this step. */
143 public function get_user_id() {
144 return $this->userid;
145 }
146
147 /** @return int the timestamp when this step was created. */
148 public function get_timecreated() {
149 return $this->timecreated;
150 }
151
152 /**
153 * @param string $name the name of a question type variable to look for in the submitted data.
154 * @return bool whether a variable with this name exists in the question type data.
155 */
156 public function has_qt_var($name) {
157 return array_key_exists($name, $this->data);
158 }
159
160 /**
161 * @param string $name the name of a question type variable to look for in the submitted data.
162 * @return string the requested variable, or null if the variable is not set.
163 */
164 public function get_qt_var($name) {
165 if (!$this->has_qt_var($name)) {
166 return null;
167 }
168 return $this->data[$name];
169 }
170
171 /**
172 * Set a cached question type variable.
173 * @param string $name the name of the variable to set. Must match _[a-z][a-z0-9]*.
174 * @param string $value the value to set.
175 */
176 public function set_qt_var($name, $value) {
177 if ($name[0] != '_') {
178 throw new coding_exception('Cannot set question type data ' . $name . ' on an attempt step. You can only set variables with names begining with _.');
179 }
180 $this->data[$name] = $value;
181 }
182
183 /**
184 * Get the latest set of files for a particular question type variable of
185 * type question_attempt::PARAM_FILES.
186 *
187 * @param string $name the name of the associated variable.
188 * @return array of {@link stored_files}.
189 */
190 public function get_qt_files($name, $contextid) {
191 if (array_key_exists($name, $this->files)) {
192 return $this->files[$name];
193 }
194
195 if (!$this->has_qt_var($name)) {
196 $this->files[$name] = array();
197 return array();
198 }
199
200 $fs = get_file_storage();
201 $this->files[$name] = $fs->get_area_files($contextid, 'question',
202 'response_' . $name, $this->id, 'sortorder', false);
203
204 return $this->files[$name];
205 }
206
207 /**
208 * Prepare a draft file are for the files belonging the a response variable
209 * of this step.
210 *
211 * @param string $name the variable name the files belong to.
212 * @param int $contextid the id of the context the quba belongs to.
213 * @return int the draft itemid.
214 */
215 public function prepare_response_files_draft_itemid($name, $contextid) {
216 list($draftid, $notused) = $this->prepare_response_files_draft_itemid_with_text(
217 $name, $contextid, null);
218 return $draftid;
219 }
220
221 /**
222 * Prepare a draft file are for the files belonging the a response variable
223 * of this step, while rewriting the URLs in some text.
224 *
225 * @param string $name the variable name the files belong to.
226 * @param int $contextid the id of the context the quba belongs to.
227 * @param string $text the text to update the URLs in.
228 * @return array(int, string) the draft itemid and the text with URLs rewritten.
229 */
230 public function prepare_response_files_draft_itemid_with_text($name, $contextid, $text) {
231 $draftid = 0; // Will be filled in by file_prepare_draft_area.
232 $newtext = file_prepare_draft_area($draftid, $contextid, 'question',
233 'response_' . $name, $this->id, null, $text);
234 return array($draftid, $newtext);
235 }
236
237 /**
238 * Rewrite the @@PLUGINFILE@@ tokens in a response variable from this step
239 * that contains links to file. Normally you should probably call
240 * {@link question_attempt::rewrite_response_pluginfile_urls()} instead of
241 * calling this method directly.
242 *
243 * @param string $text the text to update the URLs in.
244 * @param int $contextid the id of the context the quba belongs to.
245 * @param string $name the variable name the files belong to.
246 * @param array $extra extra file path components.
247 * @return string the rewritten text.
248 */
249 public function rewrite_response_pluginfile_urls($text, $contextid, $name, $extras) {
250 return question_rewrite_question_urls($text, 'pluginfile.php', $contextid,
251 'question', 'response_' . $name, $extras, $this->id);
252 }
253
254 /**
255 * Get all the question type variables.
256 * @param array name => value pairs.
257 */
258 public function get_qt_data() {
259 $result = array();
260 foreach ($this->data as $name => $value) {
261 if ($name[0] != '-' && $name[0] != ':') {
262 $result[$name] = $value;
263 }
264 }
265 return $result;
266 }
267
268 /**
269 * @param string $name the name of an behaviour variable to look for in the submitted data.
270 * @return bool whether a variable with this name exists in the question type data.
271 */
272 public function has_behaviour_var($name) {
273 return array_key_exists('-' . $name, $this->data);
274 }
275
276 /**
277 * @param string $name the name of an behaviour variable to look for in the submitted data.
278 * @return string the requested variable, or null if the variable is not set.
279 */
280 public function get_behaviour_var($name) {
281 if (!$this->has_behaviour_var($name)) {
282 return null;
283 }
284 return $this->data['-' . $name];
285 }
286
287 /**
288 * Set a cached behaviour variable.
289 * @param string $name the name of the variable to set. Must match _[a-z][a-z0-9]*.
290 * @param string $value the value to set.
291 */
292 public function set_behaviour_var($name, $value) {
293 if ($name[0] != '_') {
294 throw new coding_exception('Cannot set question type data ' . $name . ' on an attempt step. You can only set variables with names begining with _.');
295 }
296 return $this->data['-' . $name] = $value;
297 }
298
299 /**
300 * Get all the behaviour variables.
301 * @param array name => value pairs.
302 */
303 public function get_behaviour_data() {
304 $result = array();
305 foreach ($this->data as $name => $value) {
306 if ($name[0] == '-') {
307 $result[substr($name, 1)] = $value;
308 }
309 }
310 return $result;
311 }
312
313 /**
314 * Get all the submitted data, but not the cached data. behaviour
315 * variables have the - at the start of their name. This is only really
316 * intended for use by {@link question_attempt::regrade()}, it should not
317 * be considered part of the public API.
318 * @param array name => value pairs.
319 */
320 public function get_submitted_data() {
321 $result = array();
322 foreach ($this->data as $name => $value) {
323 if ($name[0] == '_' || ($name[0] == '-' && $name[1] == '_')) {
324 continue;
325 }
326 $result[$name] = $value;
327 }
328 return $result;
329 }
330
331 /**
332 * Get all the data. behaviour variables have the - at the start of
333 * their name. This is only intended for internal use, for example by
334 * {@link question_engine_data_mapper::insert_question_attempt_step()},
335 * however, it can ocasionally be useful in test code. It should not be
336 * considered part of the public API of this class.
337 * @param array name => value pairs.
338 */
339 public function get_all_data() {
340 return $this->data;
341 }
342
343 /**
344 * Create a question_attempt_step from records loaded from the database.
345 * @param array $records Raw records loaded from the database.
346 * @param int $stepid The id of the records to extract.
347 * @return question_attempt_step The newly constructed question_attempt_step.
348 */
349 public static function load_from_records(&$records, $attemptstepid) {
350 $currentrec = current($records);
351 while ($currentrec->attemptstepid != $attemptstepid) {
352 $currentrec = next($records);
353 if (!$currentrec) {
354 throw new coding_exception("Question attempt step $attemptstepid not found in the database.");
355 }
356 }
357
358 $record = $currentrec;
359 $data = array();
360 while ($currentrec && $currentrec->attemptstepid == $attemptstepid) {
361 if ($currentrec->name) {
362 $data[$currentrec->name] = $currentrec->value;
363 }
364 $currentrec = next($records);
365 }
366
367 $step = new question_attempt_step_read_only($data, $record->timecreated, $record->userid);
368 $step->state = question_state::get($record->state);
369 $step->id = $record->attemptstepid;
370 if (!is_null($record->fraction)) {
371 $step->fraction = $record->fraction + 0;
372 }
373 return $step;
374 }
375}
376
377
378/**
379 * A subclass with a bit of additional funcitonality, for pending steps.
380 *
381 * @copyright 2010 The Open University
382 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
383 */
384class question_attempt_pending_step extends question_attempt_step {
385 /** @var string . */
386 protected $newresponsesummary = null;
387
388 /**
389 * If as a result of processing this step, the response summary for the
390 * question attempt should changed, you should call this method to set the
391 * new summary.
392 * @param string $responsesummary the new response summary.
393 */
394 public function set_new_response_summary($responsesummary) {
395 $this->newresponsesummary = $responsesummary;
396 }
397
398 /** @return string the new response summary, if any. */
399 public function get_new_response_summary() {
400 return $this->newresponsesummary;
401 }
402
403 /** @return string whether this step changes the response summary. */
404 public function response_summary_changed() {
405 return !is_null($this->newresponsesummary);
406 }
407}
408
409
410/**
411 * A subclass of {@link question_attempt_step} that cannot be modified.
412 *
413 * @copyright 2009 The Open University
414 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
415 */
416class question_attempt_step_read_only extends question_attempt_step {
417 public function set_state($state) {
418 throw new coding_exception('Cannot modify a question_attempt_step_read_only.');
419 }
420 public function set_fraction($fraction) {
421 throw new coding_exception('Cannot modify a question_attempt_step_read_only.');
422 }
423 public function set_qt_var($name, $value) {
424 throw new coding_exception('Cannot modify a question_attempt_step_read_only.');
425 }
426 public function set_behaviour_var($name, $value) {
427 throw new coding_exception('Cannot modify a question_attempt_step_read_only.');
428 }
429}
430
431
432/**
433 * A null {@link question_attempt_step} returned from
434 * {@link question_attempt::get_last_step()} etc. when a an attempt has just been
435 * created and there is no acutal step.
436 *
437 * @copyright 2009 The Open University
438 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
439 */
440class question_null_step {
441 public function get_state() {
442 return question_state::$notstarted;
443 }
444
445 public function set_state($state) {
446 throw new coding_exception('This question has not been started.');
447 }
448
449 public function get_fraction() {
450 return null;
451 }
452}