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