MDL-20636 Reveiw all throw statements.
[moodle.git] / question / engine / lib.php
1 <?php
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/>.
18 /**
19  * This defines the core classes of the Moodle question engine.
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  */
28 defined('MOODLE_INTERNAL') || die();
30 require_once(dirname(__FILE__) . '/states.php');
31 require_once(dirname(__FILE__) . '/datalib.php');
32 require_once(dirname(__FILE__) . '/renderer.php');
33 require_once(dirname(__FILE__) . '/bank.php');
34 require_once(dirname(__FILE__) . '/../type/questiontype.php');
35 require_once(dirname(__FILE__) . '/../type/questionbase.php');
36 require_once(dirname(__FILE__) . '/../type/rendererbase.php');
37 require_once(dirname(__FILE__) . '/../behaviour/behaviourbase.php');
38 require_once(dirname(__FILE__) . '/../behaviour/rendererbase.php');
39 require_once($CFG->libdir . '/questionlib.php');
42 /**
43  * This static class provides access to the other question engine classes.
44  *
45  * It provides functions for managing question behaviours), and for
46  * creating, loading, saving and deleting {@link question_usage_by_activity}s,
47  * which is the main class that is used by other code that wants to use questions.
48  *
49  * @copyright  2009 The Open University
50  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
51  */
52 abstract class question_engine {
53     /** @var array behaviour name => 1. Records which behaviours have been loaded. */
54     private static $loadedbehaviours = array();
56     /**
57      * Create a new {@link question_usage_by_activity}. The usage is
58      * created in memory. If you want it to persist, you will need to call
59      * {@link save_questions_usage_by_activity()}.
60      *
61      * @param string $component the plugin creating this attempt. For example mod_quiz.
62      * @param object $context the context this usage belongs to.
63      * @return question_usage_by_activity the newly created object.
64      */
65     public static function make_questions_usage_by_activity($component, $context) {
66         return new question_usage_by_activity($component, $context);
67     }
69     /**
70      * Load a {@link question_usage_by_activity} from the database, based on its id.
71      * @param int $qubaid the id of the usage to load.
72      * @return question_usage_by_activity loaded from the database.
73      */
74     public static function load_questions_usage_by_activity($qubaid) {
75         $dm = new question_engine_data_mapper();
76         return $dm->load_questions_usage_by_activity($qubaid);
77     }
79     /**
80      * Save a {@link question_usage_by_activity} to the database. This works either
81      * if the usage was newly created by {@link make_questions_usage_by_activity()}
82      * or loaded from the database using {@link load_questions_usage_by_activity()}
83      * @param question_usage_by_activity the usage to save.
84      */
85     public static function save_questions_usage_by_activity(question_usage_by_activity $quba) {
86         $dm = new question_engine_data_mapper();
87         $observer = $quba->get_observer();
88         if ($observer instanceof question_engine_unit_of_work) {
89             $observer->save($dm);
90         } else {
91             $dm->insert_questions_usage_by_activity($quba);
92         }
93     }
95     /**
96      * Delete a {@link question_usage_by_activity} from the database, based on its id.
97      * @param int $qubaid the id of the usage to delete.
98      */
99     public static function delete_questions_usage_by_activity($qubaid) {
100         global $CFG;
101         self::delete_questions_usage_by_activities('{question_usages}.id = :qubaid', array('qubaid' => $qubaid));
102     }
104     /**
105      * Delete a {@link question_usage_by_activity} from the database, based on its id.
106      * @param int $qubaid the id of the usage to delete.
107      */
108     public static function delete_questions_usage_by_activities($where, $params) {
109         $dm = new question_engine_data_mapper();
110         $dm->delete_questions_usage_by_activities($where, $params);
111     }
113     /**
114      * Change the maxmark for the question_attempt with number in usage $slot
115      * for all the specified question_attempts.
116      * @param qubaid_condition $qubaids Selects which usages are updated.
117      * @param int $slot the number is usage to affect.
118      * @param number $newmaxmark the new max mark to set.
119      */
120     public static function set_max_mark_in_attempts(qubaid_condition $qubaids,
121             $slot, $newmaxmark) {
122         $dm = new question_engine_data_mapper();
123         $dm->set_max_mark_in_attempts($qubaids, $slot, $newmaxmark);
124     }
126     /**
127      * @param array $questionids of question ids.
128      * @return bool whether any of these questions are being used by the question engine.
129      */
130     public static function questions_in_use(array $questionids) {
131         $dm = new question_engine_data_mapper();
132         return $dm->questions_in_use($questionids);
133     }
135     /**
136      * Create an archetypal behaviour for a particular question attempt.
137      * Used by {@link question_definition::make_behaviour()}.
138      *
139      * @param string $preferredbehaviour the type of model required.
140      * @param question_attempt $qa the question attempt the model will process.
141      * @return question_behaviour an instance of appropriate behaviour class.
142      */
143     public static function make_archetypal_behaviour($preferredbehaviour, question_attempt $qa) {
144         question_engine::load_behaviour_class($preferredbehaviour);
145         $class = 'qbehaviour_' . $preferredbehaviour;
146         if (!constant($class . '::IS_ARCHETYPAL')) {
147             throw new coding_exception('The requested behaviour is not actually an archetypal one.');
148         }
149         return new $class($qa, $preferredbehaviour);
150     }
152     /**
153      * @param string $behaviour the name of a behaviour.
154      * @return array of {@link question_display_options} field names, that are
155      * not relevant to this behaviour before a 'finish' action.
156      */
157     public static function get_behaviour_unused_display_options($behaviour) {
158         self::load_behaviour_class($behaviour);
159         $class = 'qbehaviour_' . $behaviour;
160         if (!method_exists($class, 'get_unused_display_options')) {
161             return question_behaviour::get_unused_display_options();
162         }
163         return call_user_func(array($class, 'get_unused_display_options'));
164     }
166     /**
167      * Create an behaviour for a particular type. If that type cannot be
168      * found, return an instance of qbehaviour_missing.
169      *
170      * Normally you should use {@link make_archetypal_behaviour()}, or
171      * call the constructor of a particular model class directly. This method
172      * is only intended for use by {@link question_attempt::load_from_records()}.
173      *
174      * @param string $behaviour the type of model to create.
175      * @param question_attempt $qa the question attempt the model will process.
176      * @param string $preferredbehaviour the preferred behaviour for the containing usage.
177      * @return question_behaviour an instance of appropriate behaviour class.
178      */
179     public static function make_behaviour($behaviour, question_attempt $qa, $preferredbehaviour) {
180         try {
181             self::load_behaviour_class($behaviour);
182         } catch (Exception $e) {
183             question_engine::load_behaviour_class('missing');
184             return new qbehaviour_missing($qa, $preferredbehaviour);
185         }
186         $class = 'qbehaviour_' . $behaviour;
187         return new $class($qa, $preferredbehaviour);
188     }
190     /**
191      * Load the behaviour class(es) belonging to a particular model. That is,
192      * include_once('/question/behaviour/' . $behaviour . '/behaviour.php'), with a bit
193      * of checking.
194      * @param string $qtypename the question type name. For example 'multichoice' or 'shortanswer'.
195      */
196     public static function load_behaviour_class($behaviour) {
197         global $CFG;
198         if (isset(self::$loadedbehaviours[$behaviour])) {
199             return;
200         }
201         $file = $CFG->dirroot . '/question/behaviour/' . $behaviour . '/behaviour.php';
202         if (!is_readable($file)) {
203             throw new coding_exception('Unknown question behaviour ' . $behaviour);
204         }
205         include_once($file);
206         self::$loadedbehaviours[$behaviour] = 1;
207     }
209     /**
210      * Return an array where the keys are the internal names of the archetypal
211      * behaviours, and the values are a human-readable name. An
212      * archetypal behaviour is one that is suitable to pass the name of to
213      * {@link question_usage_by_activity::set_preferred_behaviour()}.
214      *
215      * @return array model name => lang string for this behaviour name.
216      */
217     public static function get_archetypal_behaviours() {
218         $archetypes = array();
219         $behaviours = get_list_of_plugins('question/behaviour');
220         foreach ($behaviours as $path) {
221             $behaviour = basename($path);
222             self::load_behaviour_class($behaviour);
223             $plugin = 'qbehaviour_' . $behaviour;
224             if (constant($plugin . '::IS_ARCHETYPAL')) {
225                 $archetypes[$behaviour] = self::get_behaviour_name($behaviour);
226             }
227         }
228         asort($archetypes, SORT_LOCALE_STRING);
229         return $archetypes;
230     }
232     /**
233      * Return an array where the keys are the internal names of the behaviours
234      * in preferred order and the values are a human-readable name.
235      *
236      * @param array $archetypes, array of behaviours
237      * @param string $questionbehavioursorder, a comma separated list of behaviour names
238      * @param string $questionbehavioursdisabled, a comma separated list of behaviour names
239      * @param string $currentbahaviour, current behaviour name
240      * @return array model name => lang string for this behaviour name.
241      */
242     public static function sort_behaviours($archetypes, $questionbehavioursorder,
243             $questionbehavioursdisabled, $currentbahaviour) {
244         $behaviourorder = array();
245         $behaviourdisabled = array();
247         // Get disabled behaviours
248         if ($questionbehavioursdisabled) {
249             $behaviourdisabledtemp = preg_split('/[\s,;]+/', $questionbehavioursdisabled);
250         } else {
251             $behaviourdisabledtemp = array();
252         }
254         if ($questionbehavioursorder) {
255             $behaviourordertemp = preg_split('/[\s,;]+/', $questionbehavioursorder);
256         } else {
257             $behaviourordertemp = array();
258         }
260         foreach ($behaviourdisabledtemp as $key) {
261             if (array_key_exists($key, $archetypes)) {
262                 // Do not disable the current behaviour
263                 if ($key != $currentbahaviour) {
264                     $behaviourdisabled[$key] = $archetypes[$key];
265                 }
266             }
267         }
269         // Get behaviours in preferred order
270         foreach ($behaviourordertemp as $key) {
271             if (array_key_exists($key, $archetypes)) {
272                 $behaviourorder[$key] = $archetypes[$key];
273             }
274         }
275         // Get the rest of behaviours and sort them alphabetically
276         $leftover = array_diff_key($archetypes, $behaviourdisabled, $behaviourorder);
277         asort($leftover, SORT_LOCALE_STRING);
279         // Set up the final order to be displayed
280         $finalorder = $behaviourorder + $leftover;
281         return $finalorder;
282     }
284     /**
285      * Return an array where the keys are the internal names of the behaviours
286      * in preferred order and the values are a human-readable name.
287      *
288      * @param string $currentbahaviour
289      * @return array model name => lang string for this behaviour name.
290      */
291     public static function get_behaviour_options($currentbahaviour) {
292         global $CFG;
293         $archetypes = self::get_archetypal_behaviours();
295         // If no admin setting return all behavious
296         if (empty($CFG->questionbehavioursdisabled) && empty($CFG->questionbehavioursorder)) {
297             return $archetypes;
298         }
300         return self::sort_behaviours($archetypes, $CFG->questionbehavioursorder,
301                 $CFG->questionbehavioursdisabled, $currentbahaviour);
302     }
304     /**
305      * Get the translated name of an behaviour, for display in the UI.
306      * @param string $behaviour the internal name of the model.
307      * @return string name from the current language pack.
308      */
309     public static function get_behaviour_name($behaviour) {
310         return get_string('pluginname', 'qbehaviour_' . $behaviour);
311     }
313     /**
314      * Returns the valid choices for the number of decimal places for showing
315      * question marks. For use in the user interface.
316      * @return array suitable for passing to {@link choose_from_menu()} or similar.
317      */
318     public static function get_dp_options() {
319         return question_display_options::get_dp_options();
320     }
322     public static function initialise_js() {
323         return question_flags::initialise_js();
324     }
328 /**
329  * This class contains all the options that controls how a question is displayed.
330  *
331  * Normally, what will happen is that the calling code will set up some display
332  * options to indicate what sort of question display it wants, and then before the
333  * question is rendered, the behaviour will be given a chance to modify the
334  * display options, so that, for example, A question that is finished will only
335  * be shown read-only, and a question that has not been submitted will not have
336  * any sort of feedback displayed.
337  *
338  * @copyright  2009 The Open University
339  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
340  */
341 class question_display_options {
342     /**#@+ @var integer named constants for the values that most of the options take. */
343     const HIDDEN = 0;
344     const VISIBLE = 1;
345     const EDITABLE = 2;
346     /**#@-*/
348     /**#@+ @var integer named constants for the {@link $marks} option. */
349     const MAX_ONLY = 1;
350     const MARK_AND_MAX = 2;
351     /**#@-*/
353     /**
354      * @var integer maximum value for the {@link $markpd} option. This is
355      * effectively set by the database structure, which uses NUMBER(12,7) columns
356      * for question marks/fractions.
357      */
358     const MAX_DP = 7;
360     /**
361      * @var boolean whether the question should be displayed as a read-only review,
362      * or in an active state where you can change the answer.
363      */
364     public $readonly = false;
366     /**
367      * @var boolean whether the question type should output hidden form fields
368      * to reset any incorrect parts of the resonse to blank.
369      */
370     public $clearwrong = false;
372     /**
373      * Should the student have what they got right and wrong clearly indicated.
374      * This includes the green/red hilighting of the bits of their response,
375      * whether the one-line summary of the current state of the question says
376      * correct/incorrect or just answered.
377      * @var integer {@link question_display_options::HIDDEN} or
378      * {@link question_display_options::VISIBLE}
379      */
380     public $correctness = self::VISIBLE;
382     /**
383      * The the mark and/or the maximum available mark for this question be visible?
384      * @var integer {@link question_display_options::HIDDEN},
385      * {@link question_display_options::MAX_ONLY} or {@link question_display_options::MARK_AND_MAX}
386      */
387     public $marks = self::MARK_AND_MAX;
389     /** @var number of decimal places to use when formatting marks for output. */
390     public $markdp = 2;
392     /**
393      * Should the flag this question UI element be visible, and if so, should the
394      * flag state be changable?
395      * @var integer {@link question_display_options::HIDDEN},
396      * {@link question_display_options::VISIBLE} or {@link question_display_options::EDITABLE}
397      */
398     public $flags = self::VISIBLE;
400     /**
401      * Should the specific feedback be visible.
402      * @var integer {@link question_display_options::HIDDEN} or
403      * {@link question_display_options::VISIBLE}
404      */
405     public $feedback = self::VISIBLE;
407     /**
408      * For questions with a number of sub-parts (like matching, or
409      * multiple-choice, multiple-reponse) display the number of sub-parts that
410      * were correct.
411      * @var integer {@link question_display_options::HIDDEN} or
412      * {@link question_display_options::VISIBLE}
413      */
414     public $numpartscorrect = self::VISIBLE;
416     /**
417      * Should the general feedback be visible?
418      * @var integer {@link question_display_options::HIDDEN} or
419      * {@link question_display_options::VISIBLE}
420      */
421     public $generalfeedback = self::VISIBLE;
423     /**
424      * Should the automatically generated display of what the correct answer is
425      * be visible?
426      * @var integer {@link question_display_options::HIDDEN} or
427      * {@link question_display_options::VISIBLE}
428      */
429     public $rightanswer = self::VISIBLE;
431     /**
432      * Should the manually added marker's comment be visible. Should the link for
433      * adding/editing the comment be there.
434      * @var integer {@link question_display_options::HIDDEN},
435      * {@link question_display_options::VISIBLE}, or {@link question_display_options::EDITABLE}.
436      * Editable means that form fields are displayed inline.
437      */
438     public $manualcomment = self::VISIBLE;
440     /**
441      * Should we show a 'Make comment or override grade' link?
442      * @var string base URL for the edit comment script, which will be shown if
443      * $manualcomment = self::VISIBLE.
444      */
445     public $manualcommentlink = null;
447     /**
448      * Used in places like the question history table, to show a link to review
449      * this question in a certain state. If blank, a link is not shown.
450      * @var string base URL for a review question script.
451      */
452     public $questionreviewlink = null;
454     /**
455      * Should the history of previous question states table be visible?
456      * @var integer {@link question_display_options::HIDDEN} or
457      * {@link question_display_options::VISIBLE}
458      */
459     public $history = self::HIDDEN;
461     /**
462      * Set all the feedback-related fields {@link $feedback}, {@link generalfeedback},
463      * {@link rightanswer} and {@link manualcomment} to
464      * {@link question_display_options::HIDDEN}.
465      */
466     public function hide_all_feedback() {
467         $this->feedback = self::HIDDEN;
468         $this->numpartscorrect = self::HIDDEN;
469         $this->generalfeedback = self::HIDDEN;
470         $this->rightanswer = self::HIDDEN;
471         $this->manualcomment = self::HIDDEN;
472         $this->correctness = self::HIDDEN;
473     }
475     /**
476      * Returns the valid choices for the number of decimal places for showing
477      * question marks. For use in the user interface.
478      *
479      * Calling code should probably use {@link question_engine::get_dp_options()}
480      * rather than calling this method directly.
481      *
482      * @return array suitable for passing to {@link choose_from_menu()} or similar.
483      */
484     public static function get_dp_options() {
485         $options = array();
486         for ($i = 0; $i <= self::MAX_DP; $i += 1) {
487             $options[$i] = $i;
488         }
489         return $options;
490     }
494 /**
495  * Contains the logic for handling question flags.
496  *
497  * @copyright  2010 The Open University
498  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
499  */
500 abstract class question_flags {
501     /**
502      * Get the checksum that validates that a toggle request is valid.
503      * @param int $qubaid the question usage id.
504      * @param int $questionid the question id.
505      * @param int $sessionid the question_attempt id.
506      * @param object $user the user. If null, defaults to $USER.
507      * @return string that needs to be sent to question/toggleflag.php for it to work.
508      */
509     protected static function get_toggle_checksum($qubaid, $questionid, $qaid, $slot, $user = null) {
510         if (is_null($user)) {
511             global $USER;
512             $user = $USER;
513         }
514         return md5($qubaid . "_" . $user->secret . "_" . $questionid . "_" . $qaid . "_" . $slot);
515     }
517     /**
518      * Get the postdata that needs to be sent to question/toggleflag.php to change the flag state.
519      * You need to append &newstate=0/1 to this.
520      * @return the post data to send.
521      */
522     public static function get_postdata(question_attempt $qa) {
523         $qaid = $qa->get_database_id();
524         $qubaid = $qa->get_usage_id();
525         $qid = $qa->get_question()->id;
526         $slot = $qa->get_slot();
527         $checksum = self::get_toggle_checksum($qubaid, $qid, $qaid, $slot);
528         return "qaid=$qaid&qubaid=$qubaid&qid=$qid&slot=$slot&checksum=$checksum&sesskey=" . sesskey();
529     }
531     /**
532      * If the request seems valid, update the flag state of a question attempt.
533      * Throws exceptions if this is not a valid update request.
534      * @param int $qubaid the question usage id.
535      * @param int $questionid the question id.
536      * @param int $sessionid the question_attempt id.
537      * @param string $checksum checksum, as computed by {@link get_toggle_checksum()}
538      *      corresponding to the last three arguments.
539      * @param bool $newstate the new state of the flag. true = flagged.
540      */
541     public static function update_flag($qubaid, $questionid, $qaid, $slot, $checksum, $newstate) {
542         // Check the checksum - it is very hard to know who a question session belongs
543         // to, so we require that checksum parameter is matches an md5 hash of the
544         // three ids and the users username. Since we are only updating a flag, that
545         // probably makes it sufficiently difficult for malicious users to toggle
546         // other users flags.
547         if ($checksum != question_flags::get_toggle_checksum($qubaid, $questionid, $qaid, $slot)) {
548             throw new moodle_exception('errorsavingflags', 'question');
549         }
551         $dm = new question_engine_data_mapper();
552         $dm->update_question_attempt_flag($qubaid, $questionid, $qaid, $slot, $newstate);
553     }
555     public static function initialise_js() {
556         global $CFG, $PAGE, $OUTPUT;
557         static $done = false;
558         if ($done) {
559             return;
560         }
561         $module = array(
562             'name' => 'core_question_flags',
563             'fullpath' => '/question/flags.js',
564             'requires' => array('base', 'dom', 'event-delegate', 'io-base'),
565         );
566         $actionurl = $CFG->wwwroot . '/question/toggleflag.php';
567         $flagattributes = array(
568             0 => array(
569                 'src' => $OUTPUT->pix_url('i/unflagged') . '',
570                 'title' => get_string('clicktoflag', 'question'),
571                 'alt' => get_string('notflagged', 'question'),
572             ),
573             1 => array(
574                 'src' => $OUTPUT->pix_url('i/flagged') . '',
575                 'title' => get_string('clicktounflag', 'question'),
576                 'alt' => get_string('flagged', 'question'),
577             ),
578         );
579         $PAGE->requires->js_init_call('M.core_question_flags.init',
580                 array($actionurl, $flagattributes), false, $module);
581         $done = true;
582     }
586 /**
587  * Exception thrown when the system detects that a student has done something
588  * out-of-order to a question. This can happen, for example, if they click
589  * the back button in a quiz, then try to submit a different response.
590  *
591  * @copyright  2010 The Open University
592  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
593  */
594 class question_out_of_sequence_exception extends moodle_exception {
595     function __construct($qubaid, $slot, $postdata) {
596         if ($postdata == null) {
597             $postdata = data_submitted();
598         }
599         parent::__construct('submissionoutofsequence', 'question', '', null,
600                 "QUBAid: $qubaid, slot: $slot, post data: " . print_r($postdata, true));
601     }
605 /**
606  * This class keeps track of a group of questions that are being attempted,
607  * and which state, and so on, each one is currently in.
608  *
609  * A quiz attempt or a lesson attempt could use an instance of this class to
610  * keep track of all the questions in the attempt and process student submissions.
611  * It is basically a collection of {@question_attempt} objects.
612  *
613  * The questions being attempted as part of this usage are identified by an integer
614  * that is passed into many of the methods as $slot. ($question->id is not
615  * used so that the same question can be used more than once in an attempt.)
616  *
617  * Normally, calling code should be able to do everything it needs to be calling
618  * methods of this class. You should not normally need to get individual
619  * {@question_attempt} objects and play around with their inner workind, in code
620  * that it outside the quetsion engine.
621  *
622  * Instances of this class correspond to rows in the question_usages table.
623  *
624  * @copyright  2009 The Open University
625  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
626  */
627 class question_usage_by_activity {
628     /**
629      * @var integer|string the id for this usage. If this usage was loaded from
630      * the database, then this is the database id. Otherwise a unique random
631      * string is used.
632      */
633     protected $id = null;
635     /**
636      * @var string name of an archetypal behaviour, that should be used
637      * by questions in this usage if possible.
638      */
639     protected $preferredbehaviour = null;
641     /** @var object the context this usage belongs to. */
642     protected $context;
644     /** @var string plugin name of the plugin this usage belongs to. */
645     protected $owningcomponent;
647     /** @var array {@link question_attempt}s that make up this usage. */
648     protected $questionattempts = array();
650     /** @var question_usage_observer that tracks changes to this usage. */
651     protected $observer;
653     /**
654      * Create a new instance. Normally, calling code should use
655      * {@link question_engine::make_questions_usage_by_activity()} or
656      * {@link question_engine::load_questions_usage_by_activity()} rather than
657      * calling this constructor directly.
658      *
659      * @param string $component the plugin creating this attempt. For example mod_quiz.
660      * @param object $context the context this usage belongs to.
661      */
662     public function __construct($component, $context) {
663         $this->owningcomponent = $component;
664         $this->context = $context;
665         $this->observer = new question_usage_null_observer();
666     }
668     /**
669      * @param string $behaviour the name of an archetypal behaviour, that should
670      * be used by questions in this usage if possible.
671      */
672     public function set_preferred_behaviour($behaviour) {
673         $this->preferredbehaviour = $behaviour;
674         $this->observer->notify_modified();
675     }
677     /** @return string the name of the preferred behaviour. */
678     public function get_preferred_behaviour() {
679         return $this->preferredbehaviour;
680     }
682     /** @return object the context this usage belongs to. */
683     public function get_owning_context() {
684         return $this->context;
685     }
687     /** @return string the name of the plugin that owns this attempt. */
688     public function get_owning_component() {
689         return $this->owningcomponent;
690     }
692     /** @return int|string If this usage came from the database, then the id
693      * from the question_usages table is returned. Otherwise a random string is
694      * returned. */
695     public function get_id() {
696         if (is_null($this->id)) {
697             $this->id = random_string(10);
698         }
699         return $this->id;
700     }
702     /** @return question_usage_observer that is tracking changes made to this usage. */
703     public function get_observer() {
704         return $this->observer;
705     }
707     /**
708      * For internal use only. Used by {@link question_engine_data_mapper} to set
709      * the id when a usage is saved to the database.
710      * @param int $id the newly determined id for this usage.
711      */
712     public function set_id_from_database($id) {
713         $this->id = $id;
714         foreach ($this->questionattempts as $qa) {
715             $qa->set_usage_id($id);
716         }
717     }
719     /**
720      * Add another question to this usage.
721      *
722      * The added question is not started until you call {@link start_question()}
723      * on it.
724      *
725      * @param question_definition $question the question to add.
726      * @param number $maxmark the maximum this question will be marked out of in
727      *      this attempt (optional). If not given, $question->defaultmark is used.
728      * @return int the number used to identify this question within this usage.
729      */
730     public function add_question(question_definition $question, $maxmark = null) {
731         $qa = new question_attempt($question, $this->get_id(),
732                 $this->context->id, $this->observer, $maxmark);
733         if (count($this->questionattempts) == 0) {
734             $this->questionattempts[1] = $qa;
735         } else {
736             $this->questionattempts[] = $qa;
737         }
738         $qa->set_number_in_usage(end(array_keys($this->questionattempts)));
739         $this->observer->notify_attempt_added($qa);
740         return $qa->get_slot();
741     }
743     /**
744      * Get the question_definition for a question in this attempt.
745      * @param int $slot the number used to identify this question within this usage.
746      * @return question_definition the requested question object.
747      */
748     public function get_question($slot) {
749         return $this->get_question_attempt($slot)->get_question();
750     }
752     /** @return array all the identifying numbers of all the questions in this usage. */
753     public function get_slots() {
754         return array_keys($this->questionattempts);
755     }
757     /** @return int the identifying number of the first question that was added to this usage. */
758     public function get_first_question_number() {
759         reset($this->questionattempts);
760         return key($this->questionattempts);
761     }
763     /** @return int the number of questions that are currently in this usage. */
764     public function question_count() {
765         return count($this->questionattempts);
766     }
768     /**
769      * Note the part of the {@link question_usage_by_activity} comment that explains
770      * that {@link question_attempt} objects should be considered part of the inner
771      * workings of the question engine, and should not, if possible, be accessed directly.
772      *
773      * @return question_attempt_iterator for iterating over all the questions being
774      * attempted. as part of this usage.
775      */
776     public function get_attempt_iterator() {
777         return new question_attempt_iterator($this);
778     }
780     /**
781      * Check whether $number actually corresponds to a question attempt that is
782      * part of this usage. Throws an exception if not.
783      *
784      * @param int $slot a number allegedly identifying a question within this usage.
785      */
786     protected function check_slot($slot) {
787         if (!array_key_exists($slot, $this->questionattempts)) {
788             throw new coding_exception("There is no question_attempt number $slot in this attempt.");
789         }
790     }
792     /**
793      * Note the part of the {@link question_usage_by_activity} comment that explains
794      * that {@link question_attempt} objects should be considered part of the inner
795      * workings of the question engine, and should not, if possible, be accessed directly.
796      *
797      * @param int $slot the number used to identify this question within this usage.
798      * @return question_attempt the corresponding {@link question_attempt} object.
799      */
800     public function get_question_attempt($slot) {
801         $this->check_slot($slot);
802         return $this->questionattempts[$slot];
803     }
805     /**
806      * Get the current state of the attempt at a question.
807      * @param int $slot the number used to identify this question within this usage.
808      * @return question_state.
809      */
810     public function get_question_state($slot) {
811         return $this->get_question_attempt($slot)->get_state();
812     }
814     /**
815      * @param int $slot the number used to identify this question within this usage.
816      * @param bool $showcorrectness Whether right/partial/wrong states should
817      * be distinguised.
818      * @return string A brief textual description of the current state.
819      */
820     public function get_question_state_string($slot, $showcorrectness) {
821         return $this->get_question_attempt($slot)->get_state_string($showcorrectness);
822     }
824     /**
825      * Get the time of the most recent action performed on a question.
826      * @param int $slot the number used to identify this question within this usage.
827      * @return int timestamp.
828      */
829     public function get_question_action_time($slot) {
830         return $this->get_question_attempt($slot)->get_last_action_time();
831     }
833     /**
834      * Get the current fraction awarded for the attempt at a question.
835      * @param int $slot the number used to identify this question within this usage.
836      * @return number|null The current fraction for this question, or null if one has
837      * not been assigned yet.
838      */
839     public function get_question_fraction($slot) {
840         return $this->get_question_attempt($slot)->get_fraction();
841     }
843     /**
844      * Get the current mark awarded for the attempt at a question.
845      * @param int $slot the number used to identify this question within this usage.
846      * @return number|null The current mark for this question, or null if one has
847      * not been assigned yet.
848      */
849     public function get_question_mark($slot) {
850         return $this->get_question_attempt($slot)->get_mark();
851     }
853     /**
854      * Get the maximum mark possible for the attempt at a question.
855      * @param int $slot the number used to identify this question within this usage.
856      * @return number the available marks for this question.
857      */
858     public function get_question_max_mark($slot) {
859         return $this->get_question_attempt($slot)->get_max_mark();
860     }
862     /**
863      * Get the current mark awarded for the attempt at a question.
864      * @param int $slot the number used to identify this question within this usage.
865      * @return number|null The current mark for this question, or null if one has
866      * not been assigned yet.
867      */
868     public function get_total_mark() {
869         $mark = 0;
870         foreach ($this->questionattempts as $qa) {
871             if ($qa->get_max_mark() > 0 && $qa->get_state() == question_state::$needsgrading) {
872                 return null;
873             }
874             $mark += $qa->get_mark();
875         }
876         return $mark;
877     }
879     /**
880      * @return string a simple textual summary of the question that was asked.
881      */
882     public function get_question_summary($slot) {
883         return $this->get_question_attempt($slot)->get_question_summary();
884     }
886     /**
887      * @return string a simple textual summary of response given.
888      */
889     public function get_response_summary($slot) {
890         return $this->get_question_attempt($slot)->get_response_summary();
891     }
893     /**
894      * @return string a simple textual summary of the correct resonse.
895      */
896     public function get_right_answer_summary($slot) {
897         return $this->get_question_attempt($slot)->get_right_answer_summary();
898     }
900     /**
901      * Get the {@link core_question_renderer}, in collaboration with appropriate
902      * {@link qbehaviour_renderer} and {@link qtype_renderer} subclasses, to generate the
903      * HTML to display this question.
904      * @param int $slot the number used to identify this question within this usage.
905      * @param question_display_options $options controls how the question is rendered.
906      * @param string|null $number The question number to display. 'i' is a special
907      *      value that gets displayed as Information. Null means no number is displayed.
908      * @return string HTML fragment representing the question.
909      */
910     public function render_question($slot, $options, $number = null) {
911         return $this->get_question_attempt($slot)->render($options, $number);
912     }
914     /**
915      * Generate any bits of HTML that needs to go in the <head> tag when this question
916      * is displayed in the body.
917      * @param int $slot the number used to identify this question within this usage.
918      * @return string HTML fragment.
919      */
920     public function render_question_head_html($slot) {
921         return $this->get_question_attempt($slot)->render_head_html();
922     }
924     /**
925      * Like {@link render_question()} but displays the question at the past step
926      * indicated by $seq, rather than showing the latest step.
927      *
928      * @param int $slot the number used to identify this question within this usage.
929      * @param int $seq the seq number of the past state to display.
930      * @param question_display_options $options controls how the question is rendered.
931      * @param string|null $number The question number to display. 'i' is a special
932      *      value that gets displayed as Information. Null means no number is displayed.
933      * @return string HTML fragment representing the question.
934      */
935     public function render_question_at_step($slot, $seq, $options, $number = null) {
936         return $this->get_question_attempt($slot)->render_at_step($seq, $options, $number, $this->preferredbehaviour);
937     }
939     /**
940      * Checks whether the users is allow to be served a particular file.
941      * @param int $slot the number used to identify this question within this usage.
942      * @param question_display_options $options the options that control display of the question.
943      * @param string $component the name of the component we are serving files for.
944      * @param string $filearea the name of the file area.
945      * @param array $args the remaining bits of the file path.
946      * @param bool $forcedownload whether the user must be forced to download the file.
947      * @return bool true if the user can access this file.
948      */
949     public function check_file_access($slot, $options, $component, $filearea, $args, $forcedownload) {
950         return $this->get_question_attempt($slot)->check_file_access($options, $component, $filearea, $args, $forcedownload);
951     }
953     /**
954      * Replace a particular question_attempt with a different one.
955      *
956      * For internal use only. Used when reloading the state of a question from the
957      * database.
958      *
959      * @param array $records Raw records loaded from the database.
960      * @param int $questionattemptid The id of the question_attempt to extract.
961      * @return question_attempt The newly constructed question_attempt_step.
962      */
963     public function replace_loaded_question_attempt_info($slot, $qa) {
964         $this->check_slot($slot);
965         $this->questionattempts[$slot] = $qa;
966     }
968     /**
969      * You should probably not use this method in code outside the question engine.
970      * The main reason for exposing it was for the benefit of unit tests.
971      * @param int $slot the number used to identify this question within this usage.
972      * @return string return the prefix that is pre-pended to field names in the HTML
973      * that is output.
974      */
975     public function get_field_prefix($slot) {
976         return $this->get_question_attempt($slot)->get_field_prefix();
977     }
979     /**
980      * Start the attempt at a question that has been added to this usage.
981      * @param int $slot the number used to identify this question within this usage.
982      */
983     public function start_question($slot) {
984         $qa = $this->get_question_attempt($slot);
985         $qa->start($this->preferredbehaviour);
986         $this->observer->notify_attempt_modified($qa);
987     }
989     /**
990      * Start the attempt at all questions that has been added to this usage.
991      */
992     public function start_all_questions() {
993         foreach ($this->questionattempts as $qa) {
994             $qa->start($this->preferredbehaviour);
995             $this->observer->notify_attempt_modified($qa);
996         }
997     }
999     /**
1000      * Start the attempt at a question, starting from the point where the previous
1001      * question_attempt $oldqa had reached. This is used by the quiz 'Each attempt
1002      * builds on last' mode.
1003      * @param int $slot the number used to identify this question within this usage.
1004      * @param question_attempt $oldqa a previous attempt at this quetsion that
1005      *      defines the starting point.
1006      */
1007     public function start_question_based_on($slot, question_attempt $oldqa) {
1008         $qa = $this->get_question_attempt($slot);
1009         $qa->start_based_on($oldqa);
1010         $this->observer->notify_attempt_modified($qa);
1011     }
1013     /**
1014      * Process all the question actions in the current request.
1015      *
1016      * If there is a parameter slots included in the post data, then only
1017      * those question numbers will be processed, otherwise all questions in this
1018      * useage will be.
1019      *
1020      * This function also does {@link update_question_flags()}.
1021      *
1022      * @param int $timestamp optional, use this timestamp as 'now'.
1023      * @param array $postdata optional, only intended for testing. Use this data
1024      * instead of the data from $_POST.
1025      */
1026     public function process_all_actions($timestamp = null, $postdata = null) {
1027         $slots = question_attempt::get_submitted_var('slots', PARAM_SEQUENCE, $postdata);
1028         if (is_null($slots)) {
1029             $slots = $this->get_slots();
1030         } else if (!$slots) {
1031             $slots = array();
1032         } else {
1033             $slots = explode(',', $slots);
1034         }
1035         foreach ($slots as $slot) {
1036             $this->validate_sequence_number($slot, $postdata);
1037             $submitteddata = $this->extract_responses($slot, $postdata);
1038             $this->process_action($slot, $submitteddata, $timestamp);
1039         }
1040         $this->update_question_flags($postdata);
1041     }
1043     /**
1044      * Get the submitted data from the current request that belongs to this
1045      * particular question.
1046      *
1047      * @param int $slot the number used to identify this question within this usage.
1048      * @param $postdata optional, only intended for testing. Use this data
1049      * instead of the data from $_POST.
1050      * @return array submitted data specific to this question.
1051      */
1052     public function extract_responses($slot, $postdata = null) {
1053         return $this->get_question_attempt($slot)->get_submitted_data($postdata);
1054     }
1056     /**
1057      * Process a specific action on a specific question.
1058      * @param int $slot the number used to identify this question within this usage.
1059      * @param $submitteddata the submitted data that constitutes the action.
1060      */
1061     public function process_action($slot, $submitteddata, $timestamp = null) {
1062         $qa = $this->get_question_attempt($slot);
1063         $qa->process_action($submitteddata, $timestamp);
1064         $this->observer->notify_attempt_modified($qa);
1065     }
1067     /**
1068      * Check that the sequence number, that detects weird things like the student
1069      * clicking back, is OK.
1070      * @param int $slot the number used to identify this question within this usage.
1071      * @param array $submitteddata the submitted data that constitutes the action.
1072      */
1073     public function validate_sequence_number($slot, $postdata = null) {
1074         $qa = $this->get_question_attempt($slot);
1075         $sequencecheck = question_attempt::get_submitted_var(
1076                 $qa->get_control_field_name('sequencecheck'), PARAM_INT, $postdata);
1077         if (!is_null($sequencecheck) && $sequencecheck != $qa->get_num_steps()) {
1078             throw new question_out_of_sequence_exception($this->id, $slot, $postdata);
1079         }
1080     }
1081     /**
1082      * Update the flagged state for all question_attempts in this usage, if their
1083      * flagged state was changed in the request.
1084      *
1085      * @param $postdata optional, only intended for testing. Use this data
1086      * instead of the data from $_POST.
1087      */
1088     public function update_question_flags($postdata = null) {
1089         foreach ($this->questionattempts as $qa) {
1090             $flagged = question_attempt::get_submitted_var(
1091                     $qa->get_flag_field_name(), PARAM_BOOL, $postdata);
1092             if (!is_null($flagged) && $flagged != $qa->is_flagged()) {
1093                 $qa->set_flagged($flagged);
1094             }
1095         }
1096     }
1098     /**
1099      * Get the correct response to a particular question. Passing the results of
1100      * this method to {@link process_action()} will probably result in full marks.
1101      * @param int $slot the number used to identify this question within this usage.
1102      * @return array that constitutes a correct response to this question.
1103      */
1104     public function get_correct_response($slot) {
1105         return $this->get_question_attempt($slot)->get_correct_response();
1106     }
1108     /**
1109      * Finish the active phase of an attempt at a question.
1110      *
1111      * This is an external act of finishing the attempt. Think, for example, of
1112      * the 'Submit all and finish' button in the quiz. Some behaviours,
1113      * (for example, immediatefeedback) give a way of finishing the active phase
1114      * of a question attempt as part of a {@link process_action()} call.
1115      *
1116      * After the active phase is over, the only changes possible are things like
1117      * manual grading, or changing the flag state.
1118      *
1119      * @param int $slot the number used to identify this question within this usage.
1120      */
1121     public function finish_question($slot, $timestamp = null) {
1122         $qa = $this->get_question_attempt($slot);
1123         $qa->finish($timestamp);
1124         $this->observer->notify_attempt_modified($qa);
1125     }
1127     /**
1128      * Finish the active phase of an attempt at a question. See {@link finish_question()}
1129      * for a fuller description of what 'finish' means.
1130      */
1131     public function finish_all_questions($timestamp = null) {
1132         foreach ($this->questionattempts as $qa) {
1133             $qa->finish($timestamp);
1134             $this->observer->notify_attempt_modified($qa);
1135         }
1136     }
1138     /**
1139      * Perform a manual grading action on a question attempt.
1140      * @param int $slot the number used to identify this question within this usage.
1141      * @param string $comment the comment being added to the question attempt.
1142      * @param number $mark the mark that is being assigned. Can be null to just
1143      * add a comment.
1144      */
1145     public function manual_grade($slot, $comment, $mark) {
1146         $qa = $this->get_question_attempt($slot);
1147         $qa->manual_grade($comment, $mark);
1148         $this->observer->notify_attempt_modified($qa);
1149     }
1151     /**
1152      * Regrade a question in this usage. This replays the sequence of submitted
1153      * actions to recompute the outcomes.
1154      * @param int $slot the number used to identify this question within this usage.
1155      * @param bool $finished whether the question attempt should be forced to be finished
1156      *      after the regrade, or whether it may still be in progress (default false).
1157      * @param number $newmaxmark (optional) if given, will change the max mark while regrading.
1158      */
1159     public function regrade_question($slot, $finished = false, $newmaxmark = null) {
1160         $oldqa = $this->get_question_attempt($slot);
1161         if (is_null($newmaxmark)) {
1162             $newmaxmark = $oldqa->get_max_mark();
1163         }
1165         $this->observer->notify_delete_attempt_steps($oldqa);
1167         $newqa = new question_attempt($oldqa->get_question(), $oldqa->get_usage_id(),
1168                 $this->context->id, $this->observer, $newmaxmark);
1169         $newqa->set_database_id($oldqa->get_database_id());
1170         $newqa->regrade($oldqa, $finished);
1172         $this->questionattempts[$slot] = $newqa;
1173         $this->observer->notify_attempt_modified($newqa);
1174     }
1176     /**
1177      * Regrade all the questions in this usage (without changing their max mark).
1178      * @param bool $finished whether each question should be forced to be finished
1179      *      after the regrade, or whether it may still be in progress (default false).
1180      */
1181     public function regrade_all_questions($finished = false) {
1182         foreach ($this->questionattempts as $slot => $notused) {
1183             $this->regrade_question($slot, $finished);
1184         }
1185     }
1187     /**
1188      * Create a question_usage_by_activity from records loaded from the database.
1189      *
1190      * For internal use only.
1191      *
1192      * @param array $records Raw records loaded from the database.
1193      * @param int $questionattemptid The id of the question_attempt to extract.
1194      * @return question_attempt The newly constructed question_attempt_step.
1195      */
1196     public static function load_from_records(&$records, $qubaid) {
1197         $record = current($records);
1198         while ($record->qubaid != $qubaid) {
1199             $record = next($records);
1200             if (!$record) {
1201                 throw new coding_exception("Question usage $qubaid not found in the database.");
1202             }
1203         }
1205         $quba = new question_usage_by_activity($record->component,
1206             get_context_instance_by_id($record->contextid));
1207         $quba->set_id_from_database($record->qubaid);
1208         $quba->set_preferred_behaviour($record->preferredbehaviour);
1210         $quba->observer = new question_engine_unit_of_work($quba);
1212         while ($record && $record->qubaid == $qubaid && !is_null($record->slot)) {
1213             $quba->questionattempts[$record->slot] =
1214                     question_attempt::load_from_records($records,
1215                     $record->questionattemptid, $quba->observer,
1216                     $quba->get_preferred_behaviour());
1217             $record = current($records);
1218         }
1220         return $quba;
1221     }
1225 /**
1226  * A class abstracting access to the
1227  * {@link question_usage_by_activity::$questionattempts} array.
1228  *
1229  * This class snapshots the list of {@link question_attempts} to iterate over
1230  * when it is created. If a question is added to the usage mid-iteration, it
1231  * will now show up.
1232  *
1233  * To create an instance of this class, use
1234  * {@link question_usage_by_activity::get_attempt_iterator()}
1235  *
1236  * @copyright  2009 The Open University
1237  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1238  */
1239 class question_attempt_iterator implements Iterator, ArrayAccess {
1240     /** @var question_usage_by_activity that we are iterating over. */
1241     protected $quba;
1242     /** @var array of question numbers. */
1243     protected $slots;
1245     /**
1246      * To create an instance of this class, use {@link question_usage_by_activity::get_attempt_iterator()}.
1247      * @param $quba the usage to iterate over.
1248      */
1249     public function __construct(question_usage_by_activity $quba) {
1250         $this->quba = $quba;
1251         $this->slots = $quba->get_slots();
1252         $this->rewind();
1253     }
1255     /** @return question_attempt_step */
1256     public function current() {
1257         return $this->offsetGet(current($this->slots));
1258     }
1259     /** @return int */
1260     public function key() {
1261         return current($this->slots);
1262     }
1263     public function next() {
1264         next($this->slots);
1265     }
1266     public function rewind() {
1267         reset($this->slots);
1268     }
1269     /** @return bool */
1270     public function valid() {
1271         return current($this->slots) !== false;
1272     }
1274     /** @return bool */
1275     public function offsetExists($slot) {
1276         return in_array($slot, $this->slots);
1277     }
1278     /** @return question_attempt_step */
1279     public function offsetGet($slot) {
1280         return $this->quba->get_question_attempt($slot);
1281     }
1282     public function offsetSet($slot, $value) {
1283         throw new coding_exception('You are only allowed read-only access to question_attempt::states through a question_attempt_step_iterator. Cannot set.');
1284     }
1285     public function offsetUnset($slot) {
1286         throw new coding_exception('You are only allowed read-only access to question_attempt::states through a question_attempt_step_iterator. Cannot unset.');
1287     }
1291 /**
1292  * Tracks an attempt at one particular question in a {@link question_usage_by_activity}.
1293  *
1294  * Most calling code should need to access objects of this class. They should be
1295  * able to do everything through the usage interface. This class is an internal
1296  * implementation detail of the question engine.
1297  *
1298  * Instances of this class correspond to rows in the question_attempts table, and
1299  * a collection of {@link question_attempt_steps}. Question inteaction models and
1300  * question types do work with question_attempt objects.
1301  *
1302  * @copyright  2009 The Open University
1303  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1304  */
1305 class question_attempt {
1306     /**
1307      * @var string this is a magic value that question types can return from
1308      * {@link question_definition::get_expected_data()}.
1309      */
1310     const USE_RAW_DATA = 'use raw data';
1312     /**
1313      * @var string special value used by manual grading because {@link PARAM_NUMBER}
1314      * converts '' to 0.
1315      */
1316     const PARAM_MARK = 'parammark';
1318     /** @var integer if this attempts is stored in the question_attempts table, the id of that row. */
1319     protected $id = null;
1321     /** @var integer|string the id of the question_usage_by_activity we belong to. */
1322     protected $usageid;
1324     /** @var integer the id of the context this question_attempt belongs to. */
1325     // TODO I don't think this is actually needed.
1326     protected $owningcontextid = null;
1328     /** @var integer the number used to identify this question_attempt within the usage. */
1329     protected $slot = null;
1331     /**
1332      * @var question_behaviour the behaviour controlling this attempt.
1333      * null until {@link start()} is called.
1334      */
1335     protected $behaviour = null;
1337     /** @var question_definition the question this is an attempt at. */
1338     protected $question;
1340     /** @var number the maximum mark that can be scored at this question. */
1341     protected $maxmark;
1343     /**
1344      * @var number the minimum fraction that can be scored at this question, so
1345      * the minimum mark is $this->minfraction * $this->maxmark.
1346      */
1347     protected $minfraction = null;
1349     /**
1350      * @var string plain text summary of the variant of the question the
1351      * student saw. Intended for reporting purposes.
1352      */
1353     protected $questionsummary = null;
1355     /**
1356      * @var string plain text summary of the response the student gave.
1357      * Intended for reporting purposes.
1358      */
1359     protected $responsesummary = null;
1361     /**
1362      * @var string plain text summary of the correct response to this question
1363      * variant the student saw. The format should be similar to responsesummary.
1364      * Intended for reporting purposes.
1365      */
1366     protected $rightanswer = null;
1368     /** @var array of {@link question_attempt_step}s. The steps in this attempt. */
1369     protected $steps = array();
1371     /** @var boolean whether the user has flagged this attempt within the usage. */
1372     protected $flagged = false;
1374     /** @var question_usage_observer tracks changes to the useage this attempt is part of.*/
1375     protected $observer;
1377     /**#@+
1378      * Constants used by the intereaction models to indicate whether the current
1379      * pending step should be kept or discarded.
1380      */
1381     const KEEP = true;
1382     const DISCARD = false;
1383     /**#@-*/
1385     /**
1386      * Create a new {@link question_attempt}. Normally you should create question_attempts
1387      * indirectly, by calling {@link question_usage_by_activity::add_question()}.
1388      *
1389      * @param question_definition $question the question this is an attempt at.
1390      * @param int|string $usageid The id of the
1391      *      {@link question_usage_by_activity} we belong to. Used by {@link get_field_prefix()}.
1392      * @param question_usage_observer $observer tracks changes to the useage this
1393      *      attempt is part of. (Optional, a {@link question_usage_null_observer} is
1394      *      used if one is not passed.
1395      * @param number $maxmark the maximum grade for this question_attempt. If not
1396      * passed, $question->defaultmark is used.
1397      */
1398     public function __construct(question_definition $question, $usageid, $owningcontextid,
1399             question_usage_observer $observer = null, $maxmark = null) {
1400         $this->question = $question;
1401         $this->usageid = $usageid;
1402         $this->owningcontextid = $owningcontextid;
1403         if (is_null($observer)) {
1404             $observer = new question_usage_null_observer();
1405         }
1406         $this->observer = $observer;
1407         if (!is_null($maxmark)) {
1408             $this->maxmark = $maxmark;
1409         } else {
1410             $this->maxmark = $question->defaultmark;
1411         }
1412     }
1414     /** @return question_definition the question this is an attempt at. */
1415     public function get_question() {
1416         return $this->question;
1417     }
1419     /**
1420      * Set the number used to identify this question_attempt within the usage.
1421      * For internal use only.
1422      * @param int $slot
1423      */
1424     public function set_number_in_usage($slot) {
1425         $this->slot = $slot;
1426     }
1428     /** @return int the number used to identify this question_attempt within the usage. */
1429     public function get_slot() {
1430         return $this->slot;
1431     }
1433     /**
1434      * @return int the id of row for this question_attempt, if it is stored in the
1435      * database. null if not.
1436      */
1437     public function get_database_id() {
1438         return $this->id;
1439     }
1441     /**
1442      * For internal use only. Set the id of the corresponding database row.
1443      * @param int $id the id of row for this question_attempt, if it is
1444      * stored in the database.
1445      */
1446     public function set_database_id($id) {
1447         $this->id = $id;
1448     }
1450     /** @return int|string the id of the {@link question_usage_by_activity} we belong to. */
1451     public function get_usage_id() {
1452         return $this->usageid;
1453     }
1455     /**
1456      * Set the id of the {@link question_usage_by_activity} we belong to.
1457      * For internal use only.
1458      * @param int|string the new id.
1459      */
1460     public function set_usage_id($usageid) {
1461         $this->usageid = $usageid;
1462     }
1464     /** @return string the name of the behaviour that is controlling this attempt. */
1465     public function get_behaviour_name() {
1466         return $this->behaviour->get_name();
1467     }
1469     /**
1470      * For internal use only.
1471      * @return question_behaviour the behaviour that is controlling this attempt.
1472      */
1473     public function get_behaviour() {
1474         return $this->behaviour;
1475     }
1477     /**
1478      * Set the flagged state of this question.
1479      * @param bool $flagged the new state.
1480      */
1481     public function set_flagged($flagged) {
1482         $this->flagged = $flagged;
1483         $this->observer->notify_attempt_modified($this);
1484     }
1486     /** @return bool whether this question is currently flagged. */
1487     public function is_flagged() {
1488         return $this->flagged;
1489     }
1491     /**
1492      * Get the name (in the sense a HTML name="" attribute, or a $_POST variable
1493      * name) to use for the field that indicates whether this question is flagged.
1494      *
1495      * @return string  The field name to use.
1496      */
1497     public function get_flag_field_name() {
1498         return $this->get_control_field_name('flagged');
1499     }
1501     /**
1502      * Get the name (in the sense a HTML name="" attribute, or a $_POST variable
1503      * name) to use for a question_type variable belonging to this question_attempt.
1504      *
1505      * See the comment on {@link question_attempt_step} for an explanation of
1506      * question type and behaviour variables.
1507      *
1508      * @param $varname The short form of the variable name.
1509      * @return string  The field name to use.
1510      */
1511     public function get_qt_field_name($varname) {
1512         return $this->get_field_prefix() . $varname;
1513     }
1515     /**
1516      * Get the name (in the sense a HTML name="" attribute, or a $_POST variable
1517      * name) to use for a question_type variable belonging to this question_attempt.
1518      *
1519      * See the comment on {@link question_attempt_step} for an explanation of
1520      * question type and behaviour variables.
1521      *
1522      * @param $varname The short form of the variable name.
1523      * @return string  The field name to use.
1524      */
1525     public function get_behaviour_field_name($varname) {
1526         return $this->get_field_prefix() . '-' . $varname;
1527     }
1529     /**
1530      * Get the name (in the sense a HTML name="" attribute, or a $_POST variable
1531      * name) to use for a control variables belonging to this question_attempt.
1532      *
1533      * Examples are :sequencecheck and :flagged
1534      *
1535      * @param $varname The short form of the variable name.
1536      * @return string  The field name to use.
1537      */
1538     public function get_control_field_name($varname) {
1539         return $this->get_field_prefix() . ':' . $varname;
1540     }
1542     /**
1543      * Get the prefix added to variable names to give field names for this
1544      * question attempt.
1545      *
1546      * You should not use this method directly. This is an implementation detail
1547      * anyway, but if you must access it, use {@link question_usage_by_activity::get_field_prefix()}.
1548      *
1549      * @param $varname The short form of the variable name.
1550      * @return string  The field name to use.
1551      */
1552     public function get_field_prefix() {
1553         return 'q' . $this->usageid . ':' . $this->slot . '_';
1554     }
1556     /**
1557      * Get one of the steps in this attempt.
1558      * For internal/test code use only.
1559      * @param int $i the step number.
1560      * @return question_attempt_step
1561      */
1562     public function get_step($i) {
1563         if ($i < 0 || $i >= count($this->steps)) {
1564             throw new coding_exception('Index out of bounds in question_attempt::get_step.');
1565         }
1566         return $this->steps[$i];
1567     }
1569     /**
1570      * Get the number of steps in this attempt.
1571      * For internal/test code use only.
1572      * @return int the number of steps we currently have.
1573      */
1574     public function get_num_steps() {
1575         return count($this->steps);
1576     }
1578     /**
1579      * Return the latest step in this question_attempt.
1580      * For internal/test code use only.
1581      * @return question_attempt_step
1582      */
1583     public function get_last_step() {
1584         if (count($this->steps) == 0) {
1585             return new question_null_step();
1586         }
1587         return end($this->steps);
1588     }
1590     /**
1591      * @return question_attempt_step_iterator for iterating over the steps in
1592      * this attempt, in order.
1593      */
1594     public function get_step_iterator() {
1595         return new question_attempt_step_iterator($this);
1596     }
1598     /**
1599      * The same as {@link get_step_iterator()}. However, for a
1600      * {@link question_attempt_with_restricted_history} this returns the full
1601      * list of steps, while {@link get_step_iterator()} returns only the
1602      * limited history.
1603      * @return question_attempt_step_iterator for iterating over the steps in
1604      * this attempt, in order.
1605      */
1606     public function get_full_step_iterator() {
1607         return $this->get_step_iterator();
1608     }
1610     /**
1611      * @return question_attempt_reverse_step_iterator for iterating over the steps in
1612      * this attempt, in reverse order.
1613      */
1614         public function get_reverse_step_iterator() {
1615         return new question_attempt_reverse_step_iterator($this);
1616     }
1618     /**
1619      * Get the qt data from the latest step that has any qt data. Return $default
1620      * array if it is no step has qt data.
1621      *
1622      * @param string $name the name of the variable to get.
1623      * @param mixed default the value to return no step has qt data.
1624      *      (Optional, defaults to an empty array.)
1625      * @return array|mixed the data, or $default if there is not any.
1626      */
1627     public function get_last_qt_data($default = array()) {
1628         foreach ($this->get_reverse_step_iterator() as $step) {
1629             $response = $step->get_qt_data();
1630             if (!empty($response)) {
1631                 return $response;
1632             }
1633         }
1634         return $default;
1635     }
1637     /**
1638      * Get the latest value of a particular question type variable. That is, get
1639      * the value from the latest step that has it set. Return null if it is not
1640      * set in any step.
1641      *
1642      * @param string $name the name of the variable to get.
1643      * @param mixed default the value to return in the variable has never been set.
1644      *      (Optional, defaults to null.)
1645      * @return mixed string value, or $default if it has never been set.
1646      */
1647     public function get_last_qt_var($name, $default = null) {
1648         foreach ($this->get_reverse_step_iterator() as $step) {
1649             if ($step->has_qt_var($name)) {
1650                 return $step->get_qt_var($name);
1651             }
1652         }
1653         return $default;
1654     }
1656     /**
1657      * Get the latest value of a particular behaviour variable. That is,
1658      * get the value from the latest step that has it set. Return null if it is
1659      * not set in any step.
1660      *
1661      * @param string $name the name of the variable to get.
1662      * @param mixed default the value to return in the variable has never been set.
1663      *      (Optional, defaults to null.)
1664      * @return mixed string value, or $default if it has never been set.
1665      */
1666     public function get_last_behaviour_var($name, $default = null) {
1667         foreach ($this->get_reverse_step_iterator() as $step) {
1668             if ($step->has_behaviour_var($name)) {
1669                 return $step->get_behaviour_var($name);
1670             }
1671         }
1672         return $default;
1673     }
1675     /**
1676      * Get the current state of this question attempt. That is, the state of the
1677      * latest step.
1678      * @return question_state
1679      */
1680     public function get_state() {
1681         return $this->get_last_step()->get_state();
1682     }
1684     /**
1685      * @param bool $showcorrectness Whether right/partial/wrong states should
1686      * be distinguised.
1687      * @return string A brief textual description of the current state.
1688      */
1689     public function get_state_string($showcorrectness) {
1690         return $this->behaviour->get_state_string($showcorrectness);
1691     }
1693     /**
1694      * @return int the timestamp of the most recent step in this question attempt.
1695      */
1696     public function get_last_action_time() {
1697         return $this->get_last_step()->get_timecreated();
1698     }
1700     /**
1701      * Get the current fraction of this question attempt. That is, the fraction
1702      * of the latest step, or null if this question has not yet been graded.
1703      * @return number the current fraction.
1704      */
1705     public function get_fraction() {
1706         return $this->get_last_step()->get_fraction();
1707     }
1709     /** @return bool whether this question attempt has a non-zero maximum mark. */
1710     public function has_marks() {
1711         // Since grades are stored in the database as NUMBER(12,7).
1712         return $this->maxmark >= 0.00000005;
1713     }
1715     /**
1716      * @return number the current mark for this question.
1717      * {@link get_fraction()} * {@link get_max_mark()}.
1718      */
1719     public function get_mark() {
1720         return $this->fraction_to_mark($this->get_fraction());
1721     }
1723     /**
1724      * This is used by the manual grading code, particularly in association with
1725      * validation. If there is a mark submitted in the request, then use that,
1726      * otherwise use the latest mark for this question.
1727      * @return number the current mark for this question.
1728      * {@link get_fraction()} * {@link get_max_mark()}.
1729      */
1730     public function get_current_manual_mark() {
1731         $mark = self::get_submitted_var($this->get_behaviour_field_name('mark'), question_attempt::PARAM_MARK);
1732         if (is_null($mark)) {
1733             return $this->get_mark();
1734         } else {
1735             return $mark;
1736         }
1737     }
1739     /**
1740      * @param number|null $fraction a fraction.
1741      * @return number|null the corresponding mark.
1742      */
1743     public function fraction_to_mark($fraction) {
1744         if (is_null($fraction)) {
1745             return null;
1746         }
1747         return $fraction * $this->maxmark;
1748     }
1750     /** @return number the maximum mark possible for this question attempt. */
1751     public function get_max_mark() {
1752         return $this->maxmark;
1753     }
1755     /** @return number the maximum mark possible for this question attempt. */
1756     public function get_min_fraction() {
1757         if (is_null($this->minfraction)) {
1758             throw new coding_exception('This question_attempt has not been started yet, the min fraction is not yet konwn.');
1759         }
1760         return $this->minfraction;
1761     }
1763     /**
1764      * The current mark, formatted to the stated number of decimal places. Uses
1765      * {@link format_float()} to format floats according to the current locale.
1766      * @param int $dp number of decimal places.
1767      * @return string formatted mark.
1768      */
1769     public function format_mark($dp) {
1770         return $this->format_fraction_as_mark($this->get_fraction(), $dp);
1771     }
1773     /**
1774      * The current mark, formatted to the stated number of decimal places. Uses
1775      * {@link format_float()} to format floats according to the current locale.
1776      * @param int $dp number of decimal places.
1777      * @return string formatted mark.
1778      */
1779     public function format_fraction_as_mark($fraction, $dp) {
1780         return format_float($this->fraction_to_mark($fraction), $dp);
1781     }
1783     /**
1784      * The maximum mark for this question attempt, formatted to the stated number
1785      * of decimal places. Uses {@link format_float()} to format floats according
1786      * to the current locale.
1787      * @param int $dp number of decimal places.
1788      * @return string formatted maximum mark.
1789      */
1790     public function format_max_mark($dp) {
1791         return format_float($this->maxmark, $dp);
1792     }
1794     /**
1795      * Return the hint that applies to the question in its current state, or null.
1796      * @return question_hint|null
1797      */
1798     public function get_applicable_hint() {
1799         return $this->behaviour->get_applicable_hint();
1800     }
1802     /**
1803      * Produce a plain-text summary of what the user did during a step.
1804      * @param question_attempt_step $step the step in quetsion.
1805      * @return string a summary of what was done during that step.
1806      */
1807     public function summarise_action(question_attempt_step $step) {
1808         return $this->behaviour->summarise_action($step);
1809     }
1811     /**
1812      * Calls {@link question_rewrite_question_urls()} with appropriate parameters
1813      * for content belonging to this question.
1814      * @param string $text the content to output.
1815      * @param string $component the component name (normally 'question' or 'qtype_...')
1816      * @param string $filearea the name of the file area.
1817      * @param int $itemid the item id.
1818      */
1819     public function rewrite_pluginfile_urls($text, $component, $filearea, $itemid) {
1820         return question_rewrite_question_urls($text,
1821                 'pluginfile.php', $this->question->contextid, $component, $filearea,
1822                 array($this->get_usage_id(), $this->get_slot()), $itemid);
1823     }
1825     /**
1826      * Get the {@link core_question_renderer}, in collaboration with appropriate
1827      * {@link qbehaviour_renderer} and {@link qtype_renderer} subclasses, to generate the
1828      * HTML to display this question attempt in its current state.
1829      * @param question_display_options $options controls how the question is rendered.
1830      * @param string|null $number The question number to display.
1831      * @return string HTML fragment representing the question.
1832      */
1833     public function render($options, $number) {
1834         global $PAGE;
1835         $qoutput = $PAGE->get_renderer('core', 'question');
1836         $qtoutput = $this->question->get_renderer();
1837         return $this->behaviour->render($options, $number, $qoutput, $qtoutput);
1838     }
1840     /**
1841      * Generate any bits of HTML that needs to go in the <head> tag when this question
1842      * attempt is displayed in the body.
1843      * @return string HTML fragment.
1844      */
1845     public function render_head_html() {
1846         return $this->question->get_renderer()->head_code($this) .
1847                 $this->behaviour->get_renderer()->head_code($this);
1848     }
1850     /**
1851      * Like {@link render_question()} but displays the question at the past step
1852      * indicated by $seq, rather than showing the latest step.
1853      *
1854      * @param int $seq the seq number of the past state to display.
1855      * @param question_display_options $options controls how the question is rendered.
1856      * @param string|null $number The question number to display. 'i' is a special
1857      *      value that gets displayed as Information. Null means no number is displayed.
1858      * @return string HTML fragment representing the question.
1859      */
1860     public function render_at_step($seq, $options, $number, $preferredbehaviour) {
1861         $restrictedqa = new question_attempt_with_restricted_history($this, $seq, $preferredbehaviour);
1862         return $restrictedqa->render($options, $number);
1863     }
1865     /**
1866      * Checks whether the users is allow to be served a particular file.
1867      * @param question_display_options $options the options that control display of the question.
1868      * @param string $component the name of the component we are serving files for.
1869      * @param string $filearea the name of the file area.
1870      * @param array $args the remaining bits of the file path.
1871      * @param bool $forcedownload whether the user must be forced to download the file.
1872      * @return bool true if the user can access this file.
1873      */
1874     public function check_file_access($options, $component, $filearea, $args, $forcedownload) {
1875         return $this->behaviour->check_file_access($options, $component, $filearea, $args, $forcedownload);
1876     }
1878     /**
1879      * Add a step to this question attempt.
1880      * @param question_attempt_step $step the new step.
1881      */
1882     protected function add_step(question_attempt_step $step) {
1883         $this->steps[] = $step;
1884         end($this->steps);
1885         $this->observer->notify_step_added($step, $this, key($this->steps));
1886     }
1888     /**
1889      * Start (or re-start) this question attempt.
1890      *
1891      * You should not call this method directly. Call
1892      * {@link question_usage_by_activity::start_question()} instead.
1893      *
1894      * @param string|question_behaviour $preferredbehaviour the name of the
1895      *      desired archetypal behaviour, or an actual model instance.
1896      * @param $submitteddata optional, used when re-starting to keep the same initial state.
1897      * @param $timestamp optional, the timstamp to record for this action. Defaults to now.
1898      * @param $userid optional, the user to attribute this action to. Defaults to the current user.
1899      */
1900     public function start($preferredbehaviour, $submitteddata = array(), $timestamp = null, $userid = null) {
1901         // Initialise the behaviour.
1902         if (is_string($preferredbehaviour)) {
1903             $this->behaviour =
1904                     $this->question->make_behaviour($this, $preferredbehaviour);
1905         } else {
1906             $class = get_class($preferredbehaviour);
1907             $this->behaviour = new $class($this, $preferredbehaviour);
1908         }
1910         // Record the minimum fraction.
1911         $this->minfraction = $this->behaviour->get_min_fraction();
1913         // Initialise the first step.
1914         $firststep = new question_attempt_step($submitteddata, $timestamp, $userid);
1915         $firststep->set_state(question_state::$todo);
1916         $this->behaviour->init_first_step($firststep);
1917         $this->add_step($firststep);
1919         // Record questionline and correct answer.
1920         // TODO we should only really do this for new attempts, not when called
1921         // via load_from_records.
1922         $this->questionsummary = $this->behaviour->get_question_summary();
1923         $this->rightanswer = $this->behaviour->get_right_answer_summary();
1924     }
1926     /**
1927      * Start this question attempt, starting from the point that the previous
1928      * attempt $oldqa had reached.
1929      *
1930      * You should not call this method directly. Call
1931      * {@link question_usage_by_activity::start_question_based_on()} instead.
1932      *
1933      * @param question_attempt $oldqa a previous attempt at this quetsion that
1934      *      defines the starting point.
1935      */
1936     public function start_based_on(question_attempt $oldqa) {
1937         $this->start($oldqa->behaviour, $oldqa->get_resume_data());
1938     }
1940     /**
1941      * Used by {@link start_based_on()} to get the data needed to start a new
1942      * attempt from the point this attempt has go to.
1943      * @return array name => value pairs.
1944      */
1945     protected function get_resume_data() {
1946         return $this->behaviour->get_resume_data();
1947     }
1949     /**
1950      * Get a particular parameter from the current request. A wrapper round
1951      * {@link optional_param()}, except that the results is returned without
1952      * slashes.
1953      * @param string $name the paramter name.
1954      * @param int $type one of the PARAM_... constants.
1955      * @param array $postdata (optional, only inteded for testing use) take the
1956      *      data from this array, instead of from $_POST.
1957      * @return mixed the requested value.
1958      */
1959     public static function get_submitted_var($name, $type, $postdata = null) {
1960         // Special case to work around PARAM_NUMBER converting '' to 0.
1961         if ($type == self::PARAM_MARK) {
1962             $mark = self::get_submitted_var($name, PARAM_RAW_TRIMMED, $postdata);
1963             if ($mark === '') {
1964                 return $mark;
1965             } else {
1966                 return self::get_submitted_var($name, PARAM_NUMBER, $postdata);
1967             }
1968         }
1970         if (is_null($postdata)) {
1971             $var = optional_param($name, null, $type);
1972         } else if (array_key_exists($name, $postdata)) {
1973             $var = clean_param($postdata[$name], $type);
1974         } else {
1975             $var = null;
1976         }
1977         if (is_string($var)) {
1978             $var = stripslashes($var);
1979         }
1980         return $var;
1981     }
1983     /**
1984      * Get any data from the request that matches the list of expected params.
1985      * @param array $expected variable name => PARAM_... constant.
1986      * @param string $extraprefix '-' or ''.
1987      * @return array name => value.
1988      */
1989     protected function get_expected_data($expected, $postdata, $extraprefix) {
1990         $submitteddata = array();
1991         foreach ($expected as $name => $type) {
1992             $value = self::get_submitted_var(
1993                     $this->get_field_prefix() . $extraprefix . $name, $type, $postdata);
1994             if (!is_null($value)) {
1995                 $submitteddata[$extraprefix . $name] = $value;
1996             }
1997         }
1998         return $submitteddata;
1999     }
2001     /**
2002      * Get all the submitted question type data for this question, whithout checking
2003      * that it is valid or cleaning it in any way.
2004      * @return array name => value.
2005      */
2006     protected function get_all_submitted_qt_vars($postdata) {
2007         if (is_null($postdata)) {
2008             $postdata = $_POST;
2009         }
2011         $pattern = '/^' . preg_quote($this->get_field_prefix()) . '[^-:]/';
2012         $prefixlen = strlen($this->get_field_prefix());
2014         $submitteddata = array();
2015         foreach ($_POST as $name => $value) {
2016             if (preg_match($pattern, $name)) {
2017                 $submitteddata[substr($name, $prefixlen)] = $value;
2018             }
2019         }
2021         return $submitteddata;
2022     }
2024     /**
2025      * Get all the sumbitted data belonging to this question attempt from the
2026      * current request.
2027      * @param array $postdata (optional, only inteded for testing use) take the
2028      *      data from this array, instead of from $_POST.
2029      * @return array name => value pairs that could be passed to {@link process_action()}.
2030      */
2031     public function get_submitted_data($postdata = null) {
2032         $submitteddata = $this->get_expected_data(
2033                 $this->behaviour->get_expected_data(), $postdata, '-');
2035         $expected = $this->behaviour->get_expected_qt_data();
2036         if ($expected === self::USE_RAW_DATA) {
2037             $submitteddata += $this->get_all_submitted_qt_vars($postdata);
2038         } else {
2039             $submitteddata += $this->get_expected_data($expected, $postdata, '');
2040         }
2041         return $submitteddata;
2042     }
2044     /**
2045      * Get a set of response data for this question attempt that would get the
2046      * best possible mark.
2047      * @return array name => value pairs that could be passed to {@link process_action()}.
2048      */
2049     public function get_correct_response() {
2050         $response = $this->question->get_correct_response();
2051         $imvars = $this->behaviour->get_correct_response();
2052         foreach ($imvars as $name => $value) {
2053             $response['-' . $name] = $value;
2054         }
2055         return $response;
2056     }
2058     /**
2059      * Change the quetsion summary. Note, that this is almost never necessary.
2060      * This method was only added to work around a limitation of the Opaque
2061      * protocol, which only sends questionLine at the end of an attempt.
2062      * @param $questionsummary the new summary to set.
2063      */
2064     public function set_question_summary($questionsummary) {
2065         $this->questionsummary = $questionsummary;
2066         $this->observer->notify_attempt_modified($this);
2067     }
2069     /**
2070      * @return string a simple textual summary of the question that was asked.
2071      */
2072     public function get_question_summary() {
2073         return $this->questionsummary;
2074     }
2076     /**
2077      * @return string a simple textual summary of response given.
2078      */
2079     public function get_response_summary() {
2080         return $this->responsesummary;
2081     }
2083     /**
2084      * @return string a simple textual summary of the correct resonse.
2085      */
2086     public function get_right_answer_summary() {
2087         return $this->rightanswer;
2088     }
2090     /**
2091      * Perform the action described by $submitteddata.
2092      * @param array $submitteddata the submitted data the determines the action.
2093      * @param int $timestamp the time to record for the action. (If not given, use now.)
2094      * @param int $userid the user to attribute the aciton to. (If not given, use the current user.)
2095      */
2096     public function process_action($submitteddata, $timestamp = null, $userid = null) {
2097         $pendingstep = new question_attempt_pending_step($submitteddata, $timestamp, $userid);
2098         if ($this->behaviour->process_action($pendingstep) == self::KEEP) {
2099             $this->add_step($pendingstep);
2100             if ($pendingstep->response_summary_changed()) {
2101                 $this->responsesummary = $pendingstep->get_new_response_summary();
2102             }
2103         }
2104     }
2106     /**
2107      * Perform a finish action on this question attempt. This corresponds to an
2108      * external finish action, for example the user pressing Submit all and finish
2109      * in the quiz, rather than using one of the controls that is part of the
2110      * question.
2111      *
2112      * @param int $timestamp the time to record for the action. (If not given, use now.)
2113      * @param int $userid the user to attribute the aciton to. (If not given, use the current user.)
2114      */
2115     public function finish($timestamp = null, $userid = null) {
2116         $this->process_action(array('-finish' => 1), $timestamp, $userid);
2117     }
2119     /**
2120      * Perform a regrade. This replays all the actions from $oldqa into this
2121      * attempt.
2122      * @param question_attempt $oldqa the attempt to regrade.
2123      * @param bool $finished whether the question attempt should be forced to be finished
2124      *      after the regrade, or whether it may still be in progress (default false).
2125      */
2126     public function regrade(question_attempt $oldqa, $finished) {
2127         $first = true;
2128         foreach ($oldqa->get_step_iterator() as $step) {
2129             if ($first) {
2130                 $first = false;
2131                 $this->start($oldqa->behaviour, $step->get_all_data(),
2132                         $step->get_timecreated(), $step->get_user_id());
2133             } else {
2134                 $this->process_action($step->get_submitted_data(),
2135                         $step->get_timecreated(), $step->get_user_id());
2136             }
2137         }
2138         if ($finished) {
2139             $this->finish();
2140         }
2141     }
2143     /**
2144      * Perform a manual grading action on this attempt.
2145      * @param $comment the comment being added.
2146      * @param $mark the new mark. (Optional, if not given, then only a comment is added.)
2147      * @param int $timestamp the time to record for the action. (If not given, use now.)
2148      * @param int $userid the user to attribute the aciton to. (If not given, use the current user.)
2149      * @return unknown_type
2150      */
2151     public function manual_grade($comment, $mark, $timestamp = null, $userid = null) {
2152         $submitteddata = array('-comment' => $comment);
2153         if (!is_null($mark)) {
2154             $submitteddata['-mark'] = $mark;
2155             $submitteddata['-maxmark'] = $this->maxmark;
2156         }
2157         $this->process_action($submitteddata, $timestamp, $userid);
2158     }
2160     /** @return bool Whether this question attempt has had a manual comment added. */
2161     public function has_manual_comment() {
2162         foreach ($this->steps as $step) {
2163             if ($step->has_behaviour_var('comment')) {
2164                 return true;
2165             }
2166         }
2167         return false;
2168     }
2170     /**
2171      * @return array(string, int) the most recent manual comment that was added
2172      * to this question, and the FORMAT_... it is.
2173      */
2174     public function get_manual_comment() {
2175         foreach ($this->get_reverse_step_iterator() as $step) {
2176             if ($step->has_behaviour_var('comment')) {
2177                 return array($step->get_behaviour_var('comment'),
2178                         $step->get_behaviour_var('commentformat'));
2179             }
2180         }
2181         return array(null, null);
2182     }
2184     /**
2185      * @return array subpartid => object with fields
2186      *      ->responseclassid matches one of the values returned from quetion_type::get_possible_responses.
2187      *      ->response the actual response the student gave to this part, as a string.
2188      *      ->fraction the credit awarded for this subpart, may be null.
2189      *      returns an empty array if no analysis is possible.
2190      */
2191     public function classify_response() {
2192         return $this->behaviour->classify_response();
2193     }
2195     /**
2196      * Create a question_attempt_step from records loaded from the database.
2197      *
2198      * For internal use only.
2199      *
2200      * @param array $records Raw records loaded from the database.
2201      * @param int $questionattemptid The id of the question_attempt to extract.
2202      * @return question_attempt The newly constructed question_attempt_step.
2203      */
2204     public static function load_from_records(&$records, $questionattemptid,
2205             question_usage_observer $observer, $preferredbehaviour) {
2206         $record = current($records);
2207         while ($record->questionattemptid != $questionattemptid) {
2208             $record = next($records);
2209             if (!$record) {
2210                 throw new coding_exception("Question attempt $questionattemptid not found in the database.");
2211             }
2212         }
2214         try {
2215             $question = question_bank::load_question($record->questionid);
2216         } catch (Exception $e) {
2217             // The question must have been deleted somehow. Create a missing
2218             // question to use in its place.
2219             $question = question_bank::get_qtype('missingtype')->make_deleted_instance(
2220                     $record->questionid, $record->maxmark + 0);
2221         }
2223         $qa = new question_attempt($question, $record->questionusageid,
2224                 $record->contextid, null, $record->maxmark + 0);
2225         $qa->set_database_id($record->questionattemptid);
2226         $qa->set_number_in_usage($record->slot);
2227         $qa->minfraction = $record->minfraction + 0;
2228         $qa->set_flagged($record->flagged);
2229         $qa->questionsummary = $record->questionsummary;
2230         $qa->rightanswer = $record->rightanswer;
2231         $qa->responsesummary = $record->responsesummary;
2232         $qa->timemodified = $record->timemodified;
2234         $qa->behaviour = question_engine::make_behaviour(
2235                 $record->behaviour, $qa, $preferredbehaviour);
2237         $i = 0;
2238         while ($record && $record->questionattemptid == $questionattemptid && !is_null($record->attemptstepid)) {
2239             $qa->steps[$i] = question_attempt_step::load_from_records($records, $record->attemptstepid);
2240             if ($i == 0) {
2241                 $question->init_first_step($qa->steps[0]);
2242             }
2243             $i++;
2244             $record = current($records);
2245         }
2247         $qa->observer = $observer;
2249         return $qa;
2250     }
2254 /**
2255  * This subclass of question_attempt pretends that only part of the step history
2256  * exists. It is used for rendering the question in past states.
2257  *
2258  * All methods that try to modify the question_attempt throw exceptions.
2259  *
2260  * @copyright  2010 The Open University
2261  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2262  */
2263 class question_attempt_with_restricted_history extends question_attempt {
2264     /**
2265      * @var question_attempt the underlying question_attempt.
2266      */
2267     protected $baseqa;
2269     /**
2270      * Create a question_attempt_with_restricted_history
2271      * @param question_attempt $baseqa The question_attempt to make a restricted version of.
2272      * @param int $lastseq the index of the last step to include.
2273      * @param string $preferredbehaviour the preferred behaviour. It is slightly
2274      *      annoyting that this needs to be passed, but unavoidable for now.
2275      */
2276     public function __construct(question_attempt $baseqa, $lastseq, $preferredbehaviour) {
2277         if ($lastseq < 0 || $lastseq >= $baseqa->get_num_steps()) {
2278             throw new coding_exception('$seq out of range', $seq);
2279         }
2281         $this->baseqa = $baseqa;
2282         $this->steps = array_slice($baseqa->steps, 0, $lastseq + 1);
2283         $this->observer = new question_usage_null_observer();
2285         // This should be a straight copy of all the remaining fields.
2286         $this->id = $baseqa->id;
2287         $this->usageid = $baseqa->usageid;
2288         $this->slot = $baseqa->slot;
2289         $this->question = $baseqa->question;
2290         $this->maxmark = $baseqa->maxmark;
2291         $this->minfraction = $baseqa->minfraction;
2292         $this->questionsummary = $baseqa->questionsummary;
2293         $this->responsesummary = $baseqa->responsesummary;
2294         $this->rightanswer = $baseqa->rightanswer;
2295         $this->flagged = $baseqa->flagged;
2297         // Except behaviour, where we need to create a new one.
2298         $this->behaviour = question_engine::make_behaviour(
2299                 $baseqa->get_behaviour_name(), $this, $preferredbehaviour);
2300     }
2302     public function get_full_step_iterator() {
2303         return $this->baseqa->get_step_iterator();
2304     }
2306     protected function add_step(question_attempt_step $step) {
2307         coding_exception('Cannot modify a question_attempt_with_restricted_history.');
2308     }
2309     public function process_action($submitteddata, $timestamp = null, $userid = null) {
2310         coding_exception('Cannot modify a question_attempt_with_restricted_history.');
2311     }
2312     public function start($preferredbehaviour, $submitteddata = array(), $timestamp = null, $userid = null) {
2313         coding_exception('Cannot modify a question_attempt_with_restricted_history.');
2314     }
2316     public function set_database_id($id) {
2317         coding_exception('Cannot modify a question_attempt_with_restricted_history.');
2318     }
2319     public function set_flagged($flagged) {
2320         coding_exception('Cannot modify a question_attempt_with_restricted_history.');
2321     }
2322     public function set_number_in_usage($slot) {
2323         coding_exception('Cannot modify a question_attempt_with_restricted_history.');
2324     }
2325     public function set_question_summary($questionsummary) {
2326         coding_exception('Cannot modify a question_attempt_with_restricted_history.');
2327     }
2328     public function set_usage_id($usageid) {
2329         coding_exception('Cannot modify a question_attempt_with_restricted_history.');
2330     }
2334 /**
2335  * A class abstracting access to the {@link question_attempt::$states} array.
2336  *
2337  * This is actively linked to question_attempt. If you add an new step
2338  * mid-iteration, then it will be included.
2339  *
2340  * @copyright  2009 The Open University
2341  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2342  */
2343 class question_attempt_step_iterator implements Iterator, ArrayAccess {
2344     /** @var question_attempt the question_attempt being iterated over. */
2345     protected $qa;
2346     /** @var integer records the current position in the iteration. */
2347     protected $i;
2349     /**
2350      * Do not call this constructor directly.
2351      * Use {@link question_attempt::get_step_iterator()}.
2352      * @param question_attempt $qa the attempt to iterate over.
2353      */
2354     public function __construct(question_attempt $qa) {
2355         $this->qa = $qa;
2356         $this->rewind();
2357     }
2359     /** @return question_attempt_step */
2360     public function current() {
2361         return $this->offsetGet($this->i);
2362     }
2363     /** @return int */
2364     public function key() {
2365         return $this->i;
2366     }
2367     public function next() {
2368         ++$this->i;
2369     }
2370     public function rewind() {
2371         $this->i = 0;
2372     }
2373     /** @return bool */
2374     public function valid() {
2375         return $this->offsetExists($this->i);
2376     }
2378     /** @return bool */
2379     public function offsetExists($i) {
2380         return $i >= 0 && $i < $this->qa->get_num_steps();
2381     }
2382     /** @return question_attempt_step */
2383     public function offsetGet($i) {
2384         return $this->qa->get_step($i);
2385     }
2386     public function offsetSet($offset, $value) {
2387         throw new coding_exception('You are only allowed read-only access to question_attempt::states through a question_attempt_step_iterator. Cannot set.');
2388     }
2389     public function offsetUnset($offset) {
2390         throw new coding_exception('You are only allowed read-only access to question_attempt::states through a question_attempt_step_iterator. Cannot unset.');
2391     }
2395 /**
2396  * A variant of {@link question_attempt_step_iterator} that iterates through the
2397  * steps in reverse order.
2398  *
2399  * @copyright  2009 The Open University
2400  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2401  */
2402 class question_attempt_reverse_step_iterator extends question_attempt_step_iterator {
2403     public function next() {
2404         --$this->i;
2405     }
2407     public function rewind() {
2408         $this->i = $this->qa->get_num_steps() - 1;
2409     }
2413 /**
2414  * Stores one step in a {@link question_attempt}.
2415  *
2416  * The most important attributes of a step are the state, which is one of the
2417  * {@link question_state} constants, the fraction, which may be null, or a
2418  * number bewteen the attempt's minfraction and 1.0, and the array of submitted
2419  * data, about which more later.
2420  *
2421  * A step also tracks the time it was created, and the user responsible for
2422  * creating it.
2423  *
2424  * The submitted data is basically just an array of name => value pairs, with
2425  * certain conventions about the to divide the variables into four = two times two
2426  * categories.
2427  *
2428  * Variables may either belong to the behaviour, in which case the
2429  * name starts with a '-', or they may belong to the question type in which case
2430  * they name does not start with a '-'.
2431  *
2432  * Second, variables may either be ones that came form the original request, in
2433  * which case the name does not start with an _, or they are cached values that
2434  * were created during processing, in which case the name does start with an _.
2435  *
2436  * That is, each name will start with one of '', '_'. '-' or '-_'. The remainder
2437  * of the name should match the regex [a-z][a-z0-9]*.
2438  *
2439  * These variables can be accessed with {@link get_behaviour_var()} and {@link get_qt_var()},
2440  * - to be clear, ->get_behaviour_var('x') gets the variable with name '-x' -
2441  * and values whose names start with '_' can be set using {@link set_behaviour_var()}
2442  * and {@link set_qt_var()}. There are some other methods like {@link has_behaviour_var()}
2443  * to check wether a varaible with a particular name is set, and {@link get_behaviour_data()}
2444  * to get all the behaviour data as an associative array.
2445  *
2446  * @copyright  2009 The Open University
2447  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2448  */
2449 class question_attempt_step {
2450     /** @var integer if this attempts is stored in the question_attempts table, the id of that row. */
2451     private $id = null;
2453     /** @var question_state one of the {@link question_state} constants. The state after this step. */
2454     private $state;
2456     /** @var null|number the fraction (grade on a scale of minfraction .. 1.0) or null. */
2457     private $fraction = null;
2459     /** @var integer the timestamp when this step was created. */
2460     private $timecreated;
2462     /** @var integer the id of the user resonsible for creating this step. */
2463     private $userid;
2465     /** @var array name => value pairs. The submitted data. */
2466     private $data;
2468     /**
2469      * You should not need to call this constructor in your own code. Steps are
2470      * normally created by {@link question_attempt} methods like
2471      * {@link question_attempt::process_action()}.
2472      * @param array $data the submitted data that defines this step.
2473      * @param int $timestamp the time to record for the action. (If not given, use now.)
2474      * @param int $userid the user to attribute the aciton to. (If not given, use the current user.)
2475      */
2476     public function __construct($data = array(), $timecreated = null, $userid = null) {
2477         global $USER;
2478         $this->state = question_state::$unprocessed;
2479         $this->data = $data;
2480         if (is_null($timecreated)) {
2481             $this->timecreated = time();
2482         } else {
2483             $this->timecreated = $timecreated;
2484         }
2485         if (is_null($userid)) {
2486             $this->userid = $USER->id;
2487         } else {
2488             $this->userid = $userid;
2489         }
2490     }
2492     /** @return question_state The state after this step. */
2493     public function get_state() {
2494         return $this->state;
2495     }
2497     /**
2498      * Set the state. Normally only called by behaviours.
2499      * @param question_state $state one of the {@link question_state} constants.
2500      */
2501     public function set_state($state) {
2502         $this->state = $state;
2503     }
2505     /**
2506      * @return null|number the fraction (grade on a scale of minfraction .. 1.0)
2507      * or null if this step has not been marked.
2508      */
2509     public function get_fraction() {
2510         return $this->fraction;
2511     }
2513     /**
2514      * Set the fraction. Normally only called by behaviours.
2515      * @param null|number $fraction the fraction to set.
2516      */
2517     public function set_fraction($fraction) {
2518         $this->fraction = $fraction;
2519     }
2521     /** @return int the id of the user resonsible for creating this step. */
2522     public function get_user_id() {
2523         return $this->userid;
2524     }
2526     /** @return int the timestamp when this step was created. */
2527     public function get_timecreated() {
2528         return $this->timecreated;
2529     }
2531     /**
2532      * @param string $name the name of a question type variable to look for in the submitted data.
2533      * @return bool whether a variable with this name exists in the question type data.
2534      */
2535     public function has_qt_var($name) {
2536         return array_key_exists($name, $this->data);
2537     }
2539     /**
2540      * @param string $name the name of a question type variable to look for in the submitted data.
2541      * @return string the requested variable, or null if the variable is not set.
2542      */
2543     public function get_qt_var($name) {
2544         if (!$this->has_qt_var($name)) {
2545             return null;
2546         }
2547         return $this->data[$name];
2548     }
2550     /**
2551      * Set a cached question type variable.
2552      * @param string $name the name of the variable to set. Must match _[a-z][a-z0-9]*.
2553      * @param string $value the value to set.
2554      */
2555     public function set_qt_var($name, $value) {
2556         if ($name[0] != '_') {
2557             throw new coding_exception('Cannot set question type data ' . $name . ' on an attempt step. You can only set variables with names begining with _.');
2558         }
2559         $this->data[$name] = $value;
2560     }
2562     /**
2563      * Get all the question type variables.
2564      * @param array name => value pairs.
2565      */
2566     public function get_qt_data() {
2567         $result = array();
2568         foreach ($this->data as $name => $value) {
2569             if ($name[0] != '-' && $name[0] != ':') {
2570                 $result[$name] = $value;
2571             }
2572         }
2573         return $result;
2574     }
2576     /**
2577      * @param string $name the name of an behaviour variable to look for in the submitted data.
2578      * @return bool whether a variable with this name exists in the question type data.
2579      */
2580     public function has_behaviour_var($name) {
2581         return array_key_exists('-' . $name, $this->data);
2582     }
2584     /**
2585      * @param string $name the name of an behaviour variable to look for in the submitted data.
2586      * @return string the requested variable, or null if the variable is not set.
2587      */
2588     public function get_behaviour_var($name) {
2589         if (!$this->has_behaviour_var($name)) {
2590             return null;
2591         }
2592         return $this->data['-' . $name];
2593     }
2595     /**
2596      * Set a cached behaviour variable.
2597      * @param string $name the name of the variable to set. Must match _[a-z][a-z0-9]*.
2598      * @param string $value the value to set.
2599      */
2600     public function set_behaviour_var($name, $value) {
2601         if ($name[0] != '_') {
2602             throw new coding_exception('Cannot set question type data ' . $name . ' on an attempt step. You can only set variables with names begining with _.');
2603         }
2604         return $this->data['-' . $name] = $value;
2605     }
2607     /**
2608      * Get all the behaviour variables.
2609      * @param array name => value pairs.
2610      */
2611     public function get_behaviour_data() {
2612         $result = array();
2613         foreach ($this->data as $name => $value) {
2614             if ($name[0] == '-') {
2615                 $result[substr($name, 1)] = $value;
2616             }
2617         }
2618         return $result;
2619     }
2621     /**
2622      * Get all the submitted data, but not the cached data. behaviour
2623      * variables have the - at the start of their name. This is only really
2624      * intended for use by {@link question_attempt::regrade()}, it should not
2625      * be considered part of the public API.
2626      * @param array name => value pairs.
2627      */
2628     public function get_submitted_data() {
2629         $result = array();
2630         foreach ($this->data as $name => $value) {
2631             if ($name[0] == '_' || ($name[0] == '-' && $name[1] == '_')) {
2632                 continue;
2633             }
2634             $result[$name] = $value;
2635         }
2636         return $result;
2637     }
2639     /**
2640      * Get all the data. behaviour variables have the ! at the start of
2641      * their name. This is only intended for internal use, for example by
2642      * {@link question_engine_data_mapper::insert_question_attempt_step()},
2643      * however, it can ocasionally be useful in test code. It should not be
2644      * considered part of the public API of this class.
2645      * @param array name => value pairs.
2646      */
2647     public function get_all_data() {
2648         return $this->data;
2649     }
2651     /**
2652      * Create a question_attempt_step from records loaded from the database.
2653      * @param array $records Raw records loaded from the database.
2654      * @param int $stepid The id of the records to extract.
2655      * @return question_attempt_step The newly constructed question_attempt_step.
2656      */
2657     public static function load_from_records(&$records, $attemptstepid) {
2658         $currentrec = current($records);
2659         while ($currentrec->attemptstepid != $attemptstepid) {
2660             $currentrec = next($records);
2661             if (!$currentrec) {
2662                 throw new coding_exception("Question attempt step $attemptstepid not found in the database.");
2663             }
2664         }
2666         $record = $currentrec;
2667         $data = array();
2668         while ($currentrec && $currentrec->attemptstepid == $attemptstepid) {
2669             if ($currentrec->name) {
2670                 $data[$currentrec->name] = $currentrec->value;
2671             }
2672             $currentrec = next($records);
2673         }
2675         $step = new question_attempt_step_read_only($data, $record->timecreated, $record->userid);
2676         $step->state = question_state::get($record->state);
2677         if (!is_null($record->fraction)) {
2678             $step->fraction = $record->fraction + 0;
2679         }
2680         return $step;
2681     }
2685 /**
2686  * A subclass with a bit of additional funcitonality, for pending steps.
2687  *
2688  * @copyright  2010 The Open University
2689  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2690  */
2691 class question_attempt_pending_step extends question_attempt_step {
2692     /** @var string . */
2693     protected $newresponsesummary = null;
2695     /**
2696      * If as a result of processing this step, the response summary for the
2697      * question attempt should changed, you should call this method to set the
2698      * new summary.
2699      * @param string $responsesummary the new response summary.
2700      */
2701     public function set_new_response_summary($responsesummary) {
2702         $this->newresponsesummary = $responsesummary;
2703     }
2705     /** @return string the new response summary, if any. */
2706     public function get_new_response_summary() {
2707         return $this->newresponsesummary;
2708     }
2710     /** @return string whether this step changes the response summary. */
2711     public function response_summary_changed() {
2712         return !is_null($this->newresponsesummary);
2713     }
2717 /**
2718  * A subclass of {@link question_attempt_step} that cannot be modified.
2719  *
2720  * @copyright  2009 The Open University
2721  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2722  */
2723 class question_attempt_step_read_only extends question_attempt_step {
2724     public function set_state($state) {
2725         throw new coding_exception('Cannot modify a question_attempt_step_read_only.');
2726     }
2727     public function set_fraction($fraction) {
2728         throw new coding_exception('Cannot modify a question_attempt_step_read_only.');
2729     }
2730     public function set_qt_var($name, $value) {
2731         throw new coding_exception('Cannot modify a question_attempt_step_read_only.');
2732     }
2733     public function set_behaviour_var($name, $value) {
2734         throw new coding_exception('Cannot modify a question_attempt_step_read_only.');
2735     }
2739 /**
2740  * A null {@link question_attempt_step} returned from
2741  * {@link question_attempt::get_last_step()} etc. when a an attempt has just been
2742  * created and there is no acutal step.
2743  *
2744  * @copyright  2009 The Open University
2745  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2746  */
2747 class question_null_step {
2748     public function get_state() {
2749         return question_state::$notstarted;
2750     }
2752     public function set_state($state) {
2753         throw new coding_exception('This question has not been started.');
2754     }
2756     public function get_fraction() {
2757         return null;
2758     }
2762 /**
2763  * Interface for things that want to be notified of signficant changes to a
2764  * {@link question_usage_by_activity}.
2765  *
2766  * A question behaviour controls the flow of actions a student can
2767  * take as they work through a question, and later, as a teacher manually grades it.
2768  *
2769  * @copyright  2009 The Open University
2770  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2771  */
2772 interface question_usage_observer {
2773     /** Called when a field of the question_usage_by_activity is changed. */
2774     public function notify_modified();
2776     /**
2777      * Called when the fields of a question attempt in this usage are modified.
2778      * @param question_attempt $qa the newly added question attempt.
2779      */
2780     public function notify_attempt_modified(question_attempt $qa);
2782     /**
2783      * Called when a new question attempt is added to this usage.
2784      * @param question_attempt $qa the newly added question attempt.
2785      */
2786     public function notify_attempt_added(question_attempt $qa);
2788     /**
2789      * Called we want to delete the old step records for an attempt, prior to
2790      * inserting newones. This is used by regrading.
2791      * @param question_attempt $qa the question attempt to delete the steps for.
2792      */
2793     public function notify_delete_attempt_steps(question_attempt $qa);
2795     /**
2796      * Called when a new step is added to a question attempt in this usage.
2797      * @param $step the new step.
2798      * @param $qa the usage it is being added to.
2799      * @param $seq the sequence number of the new step.
2800      */
2801     public function notify_step_added(question_attempt_step $step, question_attempt $qa, $seq);
2805 /**
2806  * Null implmentation of the {@link question_usage_watcher} interface.
2807  * Does nothing.
2808  *
2809  * @copyright  2009 The Open University
2810  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2811  */
2812 class question_usage_null_observer implements question_usage_observer {
2813     public function notify_modified() {
2814     }
2815     public function notify_attempt_modified(question_attempt $qa) {
2816     }
2817     public function notify_attempt_added(question_attempt $qa) {
2818     }
2819     public function notify_delete_attempt_steps(question_attempt $qa) {
2820     }
2821     public function notify_step_added(question_attempt_step $step, question_attempt $qa, $seq) {
2822     }
2826 /**
2827  * Useful functions for writing question types and behaviours.
2828  *
2829  * @copyright 2010 The Open University
2830  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2831  */
2832 abstract class question_utils {
2833     /**
2834      * Tests to see whether two arrays have the same keys, with the same values
2835      * (as compared by ===) for each key. However, the order of the arrays does
2836      * not have to be the same.
2837      * @param array $array1 the first array.
2838      * @param array $array2 the second array.
2839      * @return bool whether the two arrays have the same keys with the same
2840      *      corresponding values.
2841      */
2842     public static function arrays_have_same_keys_and_values(array $array1, array $array2) {
2843         if (count($array1) != count($array2)) {
2844             return false;
2845         }
2846         foreach ($array1 as $key => $value1) {
2847             if (!array_key_exists($key, $array2)) {
2848                 return false;
2849             }
2850             if (((string) $value1) !== ((string) $array2[$key])) {
2851                 return false;
2852             }
2853         }
2854         return true;
2855     }
2857     /**
2858      * Tests to see whether two arrays have the same value at a particular key.
2859      * This method will return true if:
2860      * 1. Neither array contains the key; or
2861      * 2. Both arrays contain the key, and the corresponding values compare
2862      *      identical when cast to strings and compared with ===.
2863      * @param array $array1 the first array.
2864      * @param array $array2 the second array.
2865      * @param string $key an array key.
2866      * @return bool whether the two arrays have the same value (or lack of
2867      *      one) for a given key.
2868      */
2869     public static function arrays_same_at_key(array $array1, array $array2, $key) {
2870         if (array_key_exists($key, $array1) && array_key_exists($key, $array2)) {
2871             return ((string) $array1[$key]) === ((string) $array2[$key]);
2872         }
2873         if (!array_key_exists($key, $array1) && !array_key_exists($key, $array2)) {
2874             return true;
2875         }
2876         return false;
2877     }
2879     /**
2880      * Tests to see whether two arrays have the same value at a particular key.
2881      * Missing values are replaced by '', and then the values are cast to
2882      * strings and compared with ===.
2883      * @param array $array1 the first array.
2884      * @param array $array2 the second array.
2885      * @param string $key an array key.
2886      * @return bool whether the two arrays have the same value (or lack of
2887      *      one) for a given key.
2888      */
2889     public static function arrays_same_at_key_missing_is_blank(
2890             array $array1, array $array2, $key) {
2891         if (array_key_exists($key, $array1)) {
2892             $value1 = $array1[$key];
2893         } else {
2894             $value1 = '';
2895         }
2896         if (array_key_exists($key, $array2)) {
2897             $value2 = $array2[$key];
2898         } else {
2899             $value2 = '';
2900         }
2901         return ((string) $value1) === ((string) $value2);
2902     }
2904     /**
2905      * Tests to see whether two arrays have the same value at a particular key.
2906      * Missing values are replaced by 0, and then the values are cast to
2907      * integers and compared with ===.
2908      * @param array $array1 the first array.
2909      * @param array $array2 the second array.
2910      * @param string $key an array key.
2911      * @return bool whether the two arrays have the same value (or lack of
2912      *      one) for a given key.
2913      */
2914     public static function arrays_same_at_key_integer(
2915             array $array1, array $array2, $key) {
2916         if (array_key_exists($key, $array1)) {
2917             $value1 = $array1[$key];
2918         } else {
2919             $value1 = 0;
2920         }
2921         if (array_key_exists($key, $array2)) {
2922             $value2 = $array2[$key];
2923         } else {
2924             $value2 = 0;
2925         }
2926         return ((integer) $value1) === ((integer) $value2);
2927     }
2929     private static $units = array('', 'i', 'ii', 'iii', 'iv', 'v', 'vi', 'vii', 'viii', 'ix');
2930     private static $tens = array('', 'x', 'xx', 'xxx', 'xl', 'l', 'lx', 'lxx', 'lxxx', 'xc');
2931     private static $hundreds = array('', 'c', 'cc', 'ccc', 'cd', 'd', 'dc', 'dcc', 'dccc', 'cm');
2932     private static $thousands = array('', 'm', 'mm', 'mmm');
2934     /**
2935      * Convert an integer to roman numerals.
2936      * @param int $number an integer between 1 and 3999 inclusive. Anything else will throw an exception.
2937      * @return string the number converted to lower case roman numerals.
2938      */
2939     public static function int_to_roman($number) {
2940         if (!is_integer($number) || $number < 1 || $number > 3999) {
2941             throw new coding_exception('Only integers between 0 and 3999 can be ' .
2942                     'converted to roman numerals.', $number);
2943         }
2945         return self::$thousands[$number / 1000 % 10] . self::$hundreds[$number / 100 % 10] .
2946                 self::$tens[$number / 10 % 10] . self::$units[$number % 10];
2947     }