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