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