Merge branch 'install_310_STABLE' of https://git.in.moodle.com/amosbot/moodle-install...
[moodle.git] / question / engine / questionattempt.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * This file defines the question attempt class, and a few related classes.
19  *
20  * @package    moodlecore
21  * @subpackage questionengine
22  * @copyright  2009 The Open University
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
27 defined('MOODLE_INTERNAL') || die();
30 /**
31  * Tracks an attempt at one particular question in a {@link question_usage_by_activity}.
32  *
33  * Most calling code should need to access objects of this class. They should be
34  * able to do everything through the usage interface. This class is an internal
35  * implementation detail of the question engine.
36  *
37  * Instances of this class correspond to rows in the question_attempts table, and
38  * a collection of {@link question_attempt_steps}. Question inteaction models and
39  * question types do work with question_attempt objects.
40  *
41  * @copyright  2009 The Open University
42  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
43  */
44 class question_attempt {
45     /**
46      * @var string this is a magic value that question types can return from
47      * {@link question_definition::get_expected_data()}.
48      */
49     const USE_RAW_DATA = 'use raw data';
51     /**
52      * @var string Should not longer be used.
53      * @deprecated since Moodle 3.0
54      */
55     const PARAM_MARK = PARAM_RAW_TRIMMED;
57     /**
58      * @var string special value to indicate a response variable that is uploaded
59      * files.
60      */
61     const PARAM_FILES = 'paramfiles';
63     /**
64      * @var string special value to indicate a response variable that is uploaded
65      * files.
66      */
67     const PARAM_RAW_FILES = 'paramrawfiles';
69     /**
70      * @var string means first try at a question during an attempt by a user.
71      * Constant used when calling classify response.
72      */
73     const FIRST_TRY = 'firsttry';
75     /**
76      * @var string means last try at a question during an attempt by a user.
77      * Constant used when calling classify response.
78      */
79     const LAST_TRY = 'lasttry';
81     /**
82      * @var string means all tries at a question during an attempt by a user.
83      * Constant used when calling classify response.
84      */
85     const ALL_TRIES = 'alltries';
87     /**
88      * @var bool used to manage the lazy-initialisation of question objects.
89      */
90     const QUESTION_STATE_NOT_APPLIED = false;
92     /**
93      * @var bool used to manage the lazy-initialisation of question objects.
94      */
95     const QUESTION_STATE_APPLIED = true;
97     /** @var integer if this attempts is stored in the question_attempts table, the id of that row. */
98     protected $id = null;
100     /** @var integer|string the id of the question_usage_by_activity we belong to. */
101     protected $usageid;
103     /** @var integer the number used to identify this question_attempt within the usage. */
104     protected $slot = null;
106     /**
107      * @var question_behaviour the behaviour controlling this attempt.
108      * null until {@link start()} is called.
109      */
110     protected $behaviour = null;
112     /** @var question_definition the question this is an attempt at. */
113     protected $question;
115     /**
116      * @var bool tracks whether $question has had {@link question_definition::start_attempt()} or
117      * {@link question_definition::apply_attempt_state()} called.
118      */
119     protected $questioninitialised;
121     /** @var int which variant of the question to use. */
122     protected $variant;
124     /**
125      * @var float the maximum mark that can be scored at this question.
126      * Actually, this is only really a nominal maximum. It might be better thought
127      * of as the question weight.
128      */
129     protected $maxmark;
131     /**
132      * @var float the minimum fraction that can be scored at this question, so
133      * the minimum mark is $this->minfraction * $this->maxmark.
134      */
135     protected $minfraction = null;
137     /**
138      * @var float the maximum fraction that can be scored at this question, so
139      * the maximum mark is $this->maxfraction * $this->maxmark.
140      */
141     protected $maxfraction = null;
143     /**
144      * @var string plain text summary of the variant of the question the
145      * student saw. Intended for reporting purposes.
146      */
147     protected $questionsummary = null;
149     /**
150      * @var string plain text summary of the response the student gave.
151      * Intended for reporting purposes.
152      */
153     protected $responsesummary = null;
155     /**
156      * @var int last modified time.
157      */
158     public $timemodified = null;
160     /**
161      * @var string plain text summary of the correct response to this question
162      * variant the student saw. The format should be similar to responsesummary.
163      * Intended for reporting purposes.
164      */
165     protected $rightanswer = null;
167     /** @var array of {@link question_attempt_step}s. The steps in this attempt. */
168     protected $steps = array();
170     /**
171      * @var question_attempt_step if, when we loaded the step from the DB, there was
172      * an autosaved step, we save a pointer to it here. (It is also added to the $steps array.)
173      */
174     protected $autosavedstep = null;
176     /** @var boolean whether the user has flagged this attempt within the usage. */
177     protected $flagged = false;
179     /** @var question_usage_observer tracks changes to the useage this attempt is part of.*/
180     protected $observer;
182     /**#@+
183      * Constants used by the intereaction models to indicate whether the current
184      * pending step should be kept or discarded.
185      */
186     const KEEP = true;
187     const DISCARD = false;
188     /**#@-*/
190     /**
191      * Create a new {@link question_attempt}. Normally you should create question_attempts
192      * indirectly, by calling {@link question_usage_by_activity::add_question()}.
193      *
194      * @param question_definition $question the question this is an attempt at.
195      * @param int|string $usageid The id of the
196      *      {@link question_usage_by_activity} we belong to. Used by {@link get_field_prefix()}.
197      * @param question_usage_observer $observer tracks changes to the useage this
198      *      attempt is part of. (Optional, a {@link question_usage_null_observer} is
199      *      used if one is not passed.
200      * @param number $maxmark the maximum grade for this question_attempt. If not
201      * passed, $question->defaultmark is used.
202      */
203     public function __construct(question_definition $question, $usageid,
204             question_usage_observer $observer = null, $maxmark = null) {
205         $this->question = $question;
206         $this->questioninitialised = self::QUESTION_STATE_NOT_APPLIED;
207         $this->usageid = $usageid;
208         if (is_null($observer)) {
209             $observer = new question_usage_null_observer();
210         }
211         $this->observer = $observer;
212         if (!is_null($maxmark)) {
213             $this->maxmark = $maxmark;
214         } else {
215             $this->maxmark = $question->defaultmark;
216         }
217     }
219     /**
220      * This method exists so that {@link question_attempt_with_restricted_history}
221      * can override it. You should not normally need to call it.
222      * @return question_attempt return ourself.
223      */
224     public function get_full_qa() {
225         return $this;
226     }
228     /**
229      * Get the question that is being attempted.
230      *
231      * @param bool $requirequestioninitialised set this to false if you don't need
232      *      the behaviour initialised, which may improve performance.
233      * @return question_definition the question this is an attempt at.
234      */
235     public function get_question($requirequestioninitialised = true) {
236         if ($requirequestioninitialised && !empty($this->steps)) {
237             $this->ensure_question_initialised();
238         }
239         return $this->question;
240     }
242     /**
243      * Get the id of the question being attempted.
244      *
245      * @return int question id.
246      */
247     public function get_question_id() {
248         return $this->question->id;
249     }
251     /**
252      * Get the variant of the question being used in a given slot.
253      * @return int the variant number.
254      */
255     public function get_variant() {
256         return $this->variant;
257     }
259     /**
260      * Set the number used to identify this question_attempt within the usage.
261      * For internal use only.
262      * @param int $slot
263      */
264     public function set_slot($slot) {
265         $this->slot = $slot;
266     }
268     /** @return int the number used to identify this question_attempt within the usage. */
269     public function get_slot() {
270         return $this->slot;
271     }
273     /**
274      * @return int the id of row for this question_attempt, if it is stored in the
275      * database. null if not.
276      */
277     public function get_database_id() {
278         return $this->id;
279     }
281     /**
282      * For internal use only. Set the id of the corresponding database row.
283      * @param int $id the id of row for this question_attempt, if it is
284      * stored in the database.
285      */
286     public function set_database_id($id) {
287         $this->id = $id;
288     }
290     /**
291      * You should almost certainly not call this method from your code. It is for
292      * internal use only.
293      * @param question_usage_observer that should be used to tracking changes made to this qa.
294      */
295     public function set_observer($observer) {
296         $this->observer = $observer;
297     }
299     /** @return int|string the id of the {@link question_usage_by_activity} we belong to. */
300     public function get_usage_id() {
301         return $this->usageid;
302     }
304     /**
305      * Set the id of the {@link question_usage_by_activity} we belong to.
306      * For internal use only.
307      * @param int|string the new id.
308      */
309     public function set_usage_id($usageid) {
310         $this->usageid = $usageid;
311     }
313     /** @return string the name of the behaviour that is controlling this attempt. */
314     public function get_behaviour_name() {
315         return $this->behaviour->get_name();
316     }
318     /**
319      * For internal use only.
320      *
321      * @param bool $requirequestioninitialised set this to false if you don't need
322      *      the behaviour initialised, which may improve performance.
323      * @return question_behaviour the behaviour that is controlling this attempt.
324      */
325     public function get_behaviour($requirequestioninitialised = true) {
326         if ($requirequestioninitialised && !empty($this->steps)) {
327             $this->ensure_question_initialised();
328         }
329         return $this->behaviour;
330     }
332     /**
333      * Set the flagged state of this question.
334      * @param bool $flagged the new state.
335      */
336     public function set_flagged($flagged) {
337         $this->flagged = $flagged;
338         $this->observer->notify_attempt_modified($this);
339     }
341     /** @return bool whether this question is currently flagged. */
342     public function is_flagged() {
343         return $this->flagged;
344     }
346     /**
347      * Get the name (in the sense a HTML name="" attribute, or a $_POST variable
348      * name) to use for the field that indicates whether this question is flagged.
349      *
350      * @return string The field name to use.
351      */
352     public function get_flag_field_name() {
353         return $this->get_control_field_name('flagged');
354     }
356     /**
357      * Get the name (in the sense a HTML name="" attribute, or a $_POST variable
358      * name) to use for a question_type variable belonging to this question_attempt.
359      *
360      * See the comment on {@link question_attempt_step} for an explanation of
361      * question type and behaviour variables.
362      *
363      * @param string $varname The short form of the variable name.
364      * @return string The field name to use.
365      */
366     public function get_qt_field_name($varname) {
367         return $this->get_field_prefix() . $varname;
368     }
370     /**
371      * Get the name (in the sense a HTML name="" attribute, or a $_POST variable
372      * name) to use for a question_type variable belonging to this question_attempt.
373      *
374      * See the comment on {@link question_attempt_step} for an explanation of
375      * question type and behaviour variables.
376      *
377      * @param string $varname The short form of the variable name.
378      * @return string The field name to use.
379      */
380     public function get_behaviour_field_name($varname) {
381         return $this->get_field_prefix() . '-' . $varname;
382     }
384     /**
385      * Get the name (in the sense a HTML name="" attribute, or a $_POST variable
386      * name) to use for a control variables belonging to this question_attempt.
387      *
388      * Examples are :sequencecheck and :flagged
389      *
390      * @param string $varname The short form of the variable name.
391      * @return string The field name to use.
392      */
393     public function get_control_field_name($varname) {
394         return $this->get_field_prefix() . ':' . $varname;
395     }
397     /**
398      * Get the prefix added to variable names to give field names for this
399      * question attempt.
400      *
401      * You should not use this method directly. This is an implementation detail
402      * anyway, but if you must access it, use {@link question_usage_by_activity::get_field_prefix()}.
403      *
404      * @return string The field name to use.
405      */
406     public function get_field_prefix() {
407         return 'q' . $this->usageid . ':' . $this->slot . '_';
408     }
410     /**
411      * When the question is rendered, this unique id is added to the
412      * outer div of the question. It can be used to uniquely reference
413      * the question from JavaScript.
414      *
415      * @return string id added to the outer <div class="que ..."> when the question is rendered.
416      */
417     public function get_outer_question_div_unique_id() {
418         return 'question-' . $this->usageid . '-' . $this->slot;
419     }
421     /**
422      * Get one of the steps in this attempt.
423      *
424      * @param int $i the step number, which counts from 0.
425      * @return question_attempt_step
426      */
427     public function get_step($i) {
428         if ($i < 0 || $i >= count($this->steps)) {
429             throw new coding_exception('Index out of bounds in question_attempt::get_step.');
430         }
431         return $this->steps[$i];
432     }
434     /**
435      * Get the number of real steps in this attempt.
436      * This is put as a hidden field in the HTML, so that when we receive some
437      * data to process, then we can check that it came from the question
438      * in the state we are now it.
439      * @return int a number that summarises the current state of this question attempt.
440      */
441     public function get_sequence_check_count() {
442         $numrealsteps = $this->get_num_steps();
443         if ($this->has_autosaved_step()) {
444             $numrealsteps -= 1;
445         }
446         return $numrealsteps;
447     }
449     /**
450      * Get the number of steps in this attempt.
451      * For internal/test code use only.
452      * @return int the number of steps we currently have.
453      */
454     public function get_num_steps() {
455         return count($this->steps);
456     }
458     /**
459      * Return the latest step in this question_attempt.
460      * For internal/test code use only.
461      * @return question_attempt_step
462      */
463     public function get_last_step() {
464         if (count($this->steps) == 0) {
465             return new question_null_step();
466         }
467         return end($this->steps);
468     }
470     /**
471      * @return boolean whether this question_attempt has autosaved data from
472      * some time in the past.
473      */
474     public function has_autosaved_step() {
475         return !is_null($this->autosavedstep);
476     }
478     /**
479      * @return question_attempt_step_iterator for iterating over the steps in
480      * this attempt, in order.
481      */
482     public function get_step_iterator() {
483         return new question_attempt_step_iterator($this);
484     }
486     /**
487      * The same as {@link get_step_iterator()}. However, for a
488      * {@link question_attempt_with_restricted_history} this returns the full
489      * list of steps, while {@link get_step_iterator()} returns only the
490      * limited history.
491      * @return question_attempt_step_iterator for iterating over the steps in
492      * this attempt, in order.
493      */
494     public function get_full_step_iterator() {
495         return $this->get_step_iterator();
496     }
498     /**
499      * @return question_attempt_reverse_step_iterator for iterating over the steps in
500      * this attempt, in reverse order.
501      */
502     public function get_reverse_step_iterator() {
503         return new question_attempt_reverse_step_iterator($this);
504     }
506     /**
507      * Get the qt data from the latest step that has any qt data. Return $default
508      * array if it is no step has qt data.
509      *
510      * @param mixed default the value to return no step has qt data.
511      *      (Optional, defaults to an empty array.)
512      * @return array|mixed the data, or $default if there is not any.
513      */
514     public function get_last_qt_data($default = array()) {
515         foreach ($this->get_reverse_step_iterator() as $step) {
516             $response = $step->get_qt_data();
517             if (!empty($response)) {
518                 return $response;
519             }
520         }
521         return $default;
522     }
524     /**
525      * Get the last step with a particular question type varialbe set.
526      * @param string $name the name of the variable to get.
527      * @return question_attempt_step the last step, or a step with no variables
528      * if there was not a real step.
529      */
530     public function get_last_step_with_qt_var($name) {
531         foreach ($this->get_reverse_step_iterator() as $step) {
532             if ($step->has_qt_var($name)) {
533                 return $step;
534             }
535         }
536         return new question_attempt_step_read_only();
537     }
539     /**
540      * Get the last step with a particular behaviour variable set.
541      * @param string $name the name of the variable to get.
542      * @return question_attempt_step the last step, or a step with no variables
543      * if there was not a real step.
544      */
545     public function get_last_step_with_behaviour_var($name) {
546         foreach ($this->get_reverse_step_iterator() as $step) {
547             if ($step->has_behaviour_var($name)) {
548                 return $step;
549             }
550         }
551         return new question_attempt_step_read_only();
552     }
554     /**
555      * Get the latest value of a particular question type variable. That is, get
556      * the value from the latest step that has it set. Return null if it is not
557      * set in any step.
558      *
559      * @param string $name the name of the variable to get.
560      * @param mixed default the value to return in the variable has never been set.
561      *      (Optional, defaults to null.)
562      * @return mixed string value, or $default if it has never been set.
563      */
564     public function get_last_qt_var($name, $default = null) {
565         $step = $this->get_last_step_with_qt_var($name);
566         if ($step->has_qt_var($name)) {
567             return $step->get_qt_var($name);
568         } else {
569             return $default;
570         }
571     }
573     /**
574      * Get the latest set of files for a particular question type variable of
575      * type question_attempt::PARAM_FILES.
576      *
577      * @param string $name the name of the associated variable.
578      * @param int $contextid the context to which the files are linked.
579      * @return array of {@link stored_files}.
580      */
581     public function get_last_qt_files($name, $contextid) {
582         foreach ($this->get_reverse_step_iterator() as $step) {
583             if ($step->has_qt_var($name)) {
584                 return $step->get_qt_files($name, $contextid);
585             }
586         }
587         return array();
588     }
590     /**
591      * Get the URL of a file that belongs to a response variable of this
592      * question_attempt.
593      * @param stored_file $file the file to link to.
594      * @return string the URL of that file.
595      */
596     public function get_response_file_url(stored_file $file) {
597         return file_encode_url(new moodle_url('/pluginfile.php'), '/' . implode('/', array(
598                 $file->get_contextid(),
599                 $file->get_component(),
600                 $file->get_filearea(),
601                 $this->usageid,
602                 $this->slot,
603                 $file->get_itemid())) .
604                 $file->get_filepath() . $file->get_filename(), true);
605     }
607     /**
608      * Prepare a draft file are for the files belonging the a response variable
609      * of this question attempt. The draft area is populated with the files from
610      * the most recent step having files.
611      *
612      * @param string $name the variable name the files belong to.
613      * @param int $contextid the id of the context the quba belongs to.
614      * @return int the draft itemid.
615      */
616     public function prepare_response_files_draft_itemid($name, $contextid) {
617         foreach ($this->get_reverse_step_iterator() as $step) {
618             if ($step->has_qt_var($name)) {
619                 return $step->prepare_response_files_draft_itemid($name, $contextid);
620             }
621         }
623         // No files yet.
624         $draftid = 0; // Will be filled in by file_prepare_draft_area.
625         file_prepare_draft_area($draftid, $contextid, 'question', 'response_' . $name, null);
626         return $draftid;
627     }
629     /**
630      * Get the latest value of a particular behaviour variable. That is,
631      * get the value from the latest step that has it set. Return null if it is
632      * not set in any step.
633      *
634      * @param string $name the name of the variable to get.
635      * @param mixed default the value to return in the variable has never been set.
636      *      (Optional, defaults to null.)
637      * @return mixed string value, or $default if it has never been set.
638      */
639     public function get_last_behaviour_var($name, $default = null) {
640         foreach ($this->get_reverse_step_iterator() as $step) {
641             if ($step->has_behaviour_var($name)) {
642                 return $step->get_behaviour_var($name);
643             }
644         }
645         return $default;
646     }
648     /**
649      * Get the current state of this question attempt. That is, the state of the
650      * latest step.
651      * @return question_state
652      */
653     public function get_state() {
654         return $this->get_last_step()->get_state();
655     }
657     /**
658      * @param bool $showcorrectness Whether right/partial/wrong states should
659      * be distinguised.
660      * @return string A brief textual description of the current state.
661      */
662     public function get_state_string($showcorrectness) {
663         // Special case when attempt is based on previous one, see MDL-31226.
664         if ($this->get_num_steps() == 1 && $this->get_state() == question_state::$complete) {
665             return get_string('notchanged', 'question');
666         }
667         return $this->behaviour->get_state_string($showcorrectness);
668     }
670     /**
671      * @param bool $showcorrectness Whether right/partial/wrong states should
672      * be distinguised.
673      * @return string a CSS class name for the current state.
674      */
675     public function get_state_class($showcorrectness) {
676         return $this->get_state()->get_state_class($showcorrectness);
677     }
679     /**
680      * @return int the timestamp of the most recent step in this question attempt.
681      */
682     public function get_last_action_time() {
683         return $this->get_last_step()->get_timecreated();
684     }
686     /**
687      * Get the current fraction of this question attempt. That is, the fraction
688      * of the latest step, or null if this question has not yet been graded.
689      * @return number the current fraction.
690      */
691     public function get_fraction() {
692         return $this->get_last_step()->get_fraction();
693     }
695     /** @return bool whether this question attempt has a non-zero maximum mark. */
696     public function has_marks() {
697         // Since grades are stored in the database as NUMBER(12,7).
698         return $this->maxmark >= question_utils::MARK_TOLERANCE;
699     }
701     /**
702      * @return number the current mark for this question.
703      * {@link get_fraction()} * {@link get_max_mark()}.
704      */
705     public function get_mark() {
706         return $this->fraction_to_mark($this->get_fraction());
707     }
709     /**
710      * This is used by the manual grading code, particularly in association with
711      * validation. It gets the current manual mark for a question, in exactly the string
712      * form that the teacher entered it, if possible. This may come from the current
713      * POST request, if there is one, otherwise from the database.
714      *
715      * @return string the current manual mark for this question, in the format the teacher typed,
716      *     if possible.
717      */
718     public function get_current_manual_mark() {
719         // Is there a current value in the current POST data? If so, use that.
720         $mark = $this->get_submitted_var($this->get_behaviour_field_name('mark'), PARAM_RAW_TRIMMED);
721         if ($mark !== null) {
722             return $mark;
723         }
725         // Otherwise, use the stored value.
726         // If the question max mark has not changed, use the stored value that was input.
727         $storedmaxmark = $this->get_last_behaviour_var('maxmark');
728         if ($storedmaxmark !== null && ($storedmaxmark - $this->get_max_mark()) < 0.0000005) {
729             return $this->get_last_behaviour_var('mark');
730         }
732         // The max mark for this question has changed so we must re-scale the current mark.
733         return format_float($this->get_mark(), 7, true, true);
734     }
736     /**
737      * @param number|null $fraction a fraction.
738      * @return number|null the corresponding mark.
739      */
740     public function fraction_to_mark($fraction) {
741         if (is_null($fraction)) {
742             return null;
743         }
744         return $fraction * $this->maxmark;
745     }
747     /**
748      * @return float the maximum mark possible for this question attempt.
749      * In fact, this is not strictly the maximum, becuase get_max_fraction may
750      * return a number greater than 1. It might be better to think of this as a
751      * question weight.
752      */
753     public function get_max_mark() {
754         return $this->maxmark;
755     }
757     /** @return float the maximum mark possible for this question attempt. */
758     public function get_min_fraction() {
759         if (is_null($this->minfraction)) {
760             throw new coding_exception('This question_attempt has not been started yet, the min fraction is not yet known.');
761         }
762         return $this->minfraction;
763     }
765     /** @return float the maximum mark possible for this question attempt. */
766     public function get_max_fraction() {
767         if (is_null($this->maxfraction)) {
768             throw new coding_exception('This question_attempt has not been started yet, the max fraction is not yet known.');
769         }
770         return $this->maxfraction;
771     }
773     /**
774      * The current mark, formatted to the stated number of decimal places. Uses
775      * {@link format_float()} to format floats according to the current locale.
776      * @param int $dp number of decimal places.
777      * @return string formatted mark.
778      */
779     public function format_mark($dp) {
780         return $this->format_fraction_as_mark($this->get_fraction(), $dp);
781     }
783     /**
784      * The a mark, formatted to the stated number of decimal places. Uses
785      * {@link format_float()} to format floats according to the current locale.
786      *
787      * @param number $fraction a fraction.
788      * @param int $dp number of decimal places.
789      * @return string formatted mark.
790      */
791     public function format_fraction_as_mark($fraction, $dp) {
792         return format_float($this->fraction_to_mark($fraction), $dp);
793     }
795     /**
796      * The maximum mark for this question attempt, formatted to the stated number
797      * of decimal places. Uses {@link format_float()} to format floats according
798      * to the current locale.
799      * @param int $dp number of decimal places.
800      * @return string formatted maximum mark.
801      */
802     public function format_max_mark($dp) {
803         return format_float($this->maxmark, $dp);
804     }
806     /**
807      * Return the hint that applies to the question in its current state, or null.
808      * @return question_hint|null
809      */
810     public function get_applicable_hint() {
811         return $this->behaviour->get_applicable_hint();
812     }
814     /**
815      * Produce a plain-text summary of what the user did during a step.
816      * @param question_attempt_step $step the step in question.
817      * @return string a summary of what was done during that step.
818      */
819     public function summarise_action(question_attempt_step $step) {
820         $this->ensure_question_initialised();
821         return $this->behaviour->summarise_action($step);
822     }
824     /**
825      * Return one of the bits of metadata for a this question attempt.
826      * @param string $name the name of the metadata variable to return.
827      * @return string the value of that metadata variable.
828      */
829     public function get_metadata($name) {
830         return $this->get_step(0)->get_metadata_var($name);
831     }
833     /**
834      * Set some metadata for this question attempt.
835      * @param string $name the name of the metadata variable to return.
836      * @param string $value the value to set that metadata variable to.
837      */
838     public function set_metadata($name, $value) {
839         $firststep = $this->get_step(0);
840         if (!$firststep->has_metadata_var($name)) {
841             $this->observer->notify_metadata_added($this, $name);
842         } else if ($value !== $firststep->get_metadata_var($name)) {
843             $this->observer->notify_metadata_modified($this, $name);
844         }
845         $firststep->set_metadata_var($name, $value);
846     }
848     /**
849      * Helper function used by {@link rewrite_pluginfile_urls()} and
850      * {@link rewrite_response_pluginfile_urls()}.
851      * @return array ids that need to go into the file paths.
852      */
853     protected function extra_file_path_components() {
854         return array($this->get_usage_id(), $this->get_slot());
855     }
857     /**
858      * Calls {@link question_rewrite_question_urls()} with appropriate parameters
859      * for content belonging to this question.
860      * @param string $text the content to output.
861      * @param string $component the component name (normally 'question' or 'qtype_...')
862      * @param string $filearea the name of the file area.
863      * @param int $itemid the item id.
864      * @return string the content with the URLs rewritten.
865      */
866     public function rewrite_pluginfile_urls($text, $component, $filearea, $itemid) {
867         return question_rewrite_question_urls($text, 'pluginfile.php',
868                 $this->question->contextid, $component, $filearea,
869                 $this->extra_file_path_components(), $itemid);
870     }
872     /**
873      * Calls {@link question_rewrite_question_urls()} with appropriate parameters
874      * for content belonging to responses to this question.
875      *
876      * @param string $text the text to update the URLs in.
877      * @param int $contextid the id of the context the quba belongs to.
878      * @param string $name the variable name the files belong to.
879      * @param question_attempt_step $step the step the response is coming from.
880      * @return string the content with the URLs rewritten.
881      */
882     public function rewrite_response_pluginfile_urls($text, $contextid, $name,
883             question_attempt_step $step) {
884         return $step->rewrite_response_pluginfile_urls($text, $contextid, $name,
885                 $this->extra_file_path_components());
886     }
888     /**
889      * Get the {@link core_question_renderer}, in collaboration with appropriate
890      * {@link qbehaviour_renderer} and {@link qtype_renderer} subclasses, to generate the
891      * HTML to display this question attempt in its current state.
892      *
893      * @param question_display_options $options controls how the question is rendered.
894      * @param string|null $number The question number to display.
895      * @param moodle_page|null $page the page the question is being redered to.
896      *      (Optional. Defaults to $PAGE.)
897      * @return string HTML fragment representing the question.
898      */
899     public function render($options, $number, $page = null) {
900         $this->ensure_question_initialised();
901         if (is_null($page)) {
902             global $PAGE;
903             $page = $PAGE;
904         }
905         $qoutput = $page->get_renderer('core', 'question');
906         $qtoutput = $this->question->get_renderer($page);
907         return $this->behaviour->render($options, $number, $qoutput, $qtoutput);
908     }
910     /**
911      * Generate any bits of HTML that needs to go in the <head> tag when this question
912      * attempt is displayed in the body.
913      * @return string HTML fragment.
914      */
915     public function render_head_html($page = null) {
916         $this->ensure_question_initialised();
917         if (is_null($page)) {
918             global $PAGE;
919             $page = $PAGE;
920         }
921         // TODO go via behaviour.
922         return $this->question->get_renderer($page)->head_code($this) .
923                 $this->behaviour->get_renderer($page)->head_code($this);
924     }
926     /**
927      * Like {@link render_question()} but displays the question at the past step
928      * indicated by $seq, rather than showing the latest step.
929      *
930      * @param int $seq the seq number of the past state to display.
931      * @param question_display_options $options controls how the question is rendered.
932      * @param string|null $number The question number to display. 'i' is a special
933      *      value that gets displayed as Information. Null means no number is displayed.
934      * @param string $preferredbehaviour the preferred behaviour. It is slightly
935      *      annoying that this needs to be passed, but unavoidable for now.
936      * @return string HTML fragment representing the question.
937      */
938     public function render_at_step($seq, $options, $number, $preferredbehaviour) {
939         $this->ensure_question_initialised();
940         $restrictedqa = new question_attempt_with_restricted_history($this, $seq, $preferredbehaviour);
941         return $restrictedqa->render($options, $number);
942     }
944     /**
945      * Checks whether the users is allow to be served a particular file.
946      * @param question_display_options $options the options that control display of the question.
947      * @param string $component the name of the component we are serving files for.
948      * @param string $filearea the name of the file area.
949      * @param array $args the remaining bits of the file path.
950      * @param bool $forcedownload whether the user must be forced to download the file.
951      * @return bool true if the user can access this file.
952      */
953     public function check_file_access($options, $component, $filearea, $args, $forcedownload) {
954         $this->ensure_question_initialised();
955         return $this->behaviour->check_file_access($options, $component, $filearea, $args, $forcedownload);
956     }
958     /**
959      * Add a step to this question attempt.
960      * @param question_attempt_step $step the new step.
961      */
962     protected function add_step(question_attempt_step $step) {
963         $this->steps[] = $step;
964         end($this->steps);
965         $this->observer->notify_step_added($step, $this, key($this->steps));
966     }
968     /**
969      * Add an auto-saved step to this question attempt. We mark auto-saved steps by
970      * changing saving the step number with a - sign.
971      * @param question_attempt_step $step the new step.
972      */
973     protected function add_autosaved_step(question_attempt_step $step) {
974         $this->steps[] = $step;
975         $this->autosavedstep = $step;
976         end($this->steps);
977         $this->observer->notify_step_added($step, $this, -key($this->steps));
978     }
980     /**
981      * Discard any auto-saved data belonging to this question attempt.
982      */
983     public function discard_autosaved_step() {
984         if (!$this->has_autosaved_step()) {
985             return;
986         }
988         $autosaved = array_pop($this->steps);
989         $this->autosavedstep = null;
990         $this->observer->notify_step_deleted($autosaved, $this);
991     }
993     /**
994      * If there is an autosaved step, convert it into a real save, so that it
995      * is preserved.
996      */
997     protected function convert_autosaved_step_to_real_step() {
998         if ($this->autosavedstep === null) {
999             return;
1000         }
1002         $laststep = end($this->steps);
1003         if ($laststep !== $this->autosavedstep) {
1004             throw new coding_exception('Cannot convert autosaved step to real step, since other steps have been added.');
1005         }
1007         $this->observer->notify_step_modified($this->autosavedstep, $this, key($this->steps));
1008         $this->autosavedstep = null;
1009     }
1011     /**
1012      * Use a strategy to pick a variant.
1013      * @param question_variant_selection_strategy $variantstrategy a strategy.
1014      * @return int the selected variant.
1015      */
1016     public function select_variant(question_variant_selection_strategy $variantstrategy) {
1017         return $variantstrategy->choose_variant($this->get_question()->get_num_variants(),
1018                 $this->get_question()->get_variants_selection_seed());
1019     }
1021     /**
1022      * Start this question attempt.
1023      *
1024      * You should not call this method directly. Call
1025      * {@link question_usage_by_activity::start_question()} instead.
1026      *
1027      * @param string|question_behaviour $preferredbehaviour the name of the
1028      *      desired archetypal behaviour, or an actual behaviour instance.
1029      * @param int $variant the variant of the question to start. Between 1 and
1030      *      $this->get_question()->get_num_variants() inclusive.
1031      * @param array $submitteddata optional, used when re-starting to keep the same initial state.
1032      * @param int $timestamp optional, the timstamp to record for this action. Defaults to now.
1033      * @param int $userid optional, the user to attribute this action to. Defaults to the current user.
1034      * @param int $existingstepid optional, if this step is going to replace an existing step
1035      *      (for example, during a regrade) this is the id of the previous step we are replacing.
1036      */
1037     public function start($preferredbehaviour, $variant, $submitteddata = array(),
1038             $timestamp = null, $userid = null, $existingstepid = null) {
1040         if ($this->get_num_steps() > 0) {
1041             throw new coding_exception('Cannot start a question that is already started.');
1042         }
1044         // Initialise the behaviour.
1045         $this->variant = $variant;
1046         if (is_string($preferredbehaviour)) {
1047             $this->behaviour =
1048                     $this->question->make_behaviour($this, $preferredbehaviour);
1049         } else {
1050             $class = get_class($preferredbehaviour);
1051             $this->behaviour = new $class($this, $preferredbehaviour);
1052         }
1054         // Record the minimum and maximum fractions.
1055         $this->minfraction = $this->behaviour->get_min_fraction();
1056         $this->maxfraction = $this->behaviour->get_max_fraction();
1058         // Initialise the first step.
1059         $firststep = new question_attempt_step($submitteddata, $timestamp, $userid, $existingstepid);
1060         if ($submitteddata) {
1061             $firststep->set_state(question_state::$complete);
1062             $this->behaviour->apply_attempt_state($firststep);
1063         } else {
1064             $this->behaviour->init_first_step($firststep, $variant);
1065         }
1066         $this->questioninitialised = self::QUESTION_STATE_APPLIED;
1067         $this->add_step($firststep);
1069         // Record questionline and correct answer.
1070         $this->questionsummary = $this->behaviour->get_question_summary();
1071         $this->rightanswer = $this->behaviour->get_right_answer_summary();
1072     }
1074     /**
1075      * Start this question attempt, starting from the point that the previous
1076      * attempt $oldqa had reached.
1077      *
1078      * You should not call this method directly. Call
1079      * {@link question_usage_by_activity::start_question_based_on()} instead.
1080      *
1081      * @param question_attempt $oldqa a previous attempt at this quetsion that
1082      *      defines the starting point.
1083      */
1084     public function start_based_on(question_attempt $oldqa) {
1085         $this->start($oldqa->behaviour, $oldqa->get_variant(), $oldqa->get_resume_data());
1086     }
1088     /**
1089      * Used by {@link start_based_on()} to get the data needed to start a new
1090      * attempt from the point this attempt has go to.
1091      * @return array name => value pairs.
1092      */
1093     protected function get_resume_data() {
1094         $this->ensure_question_initialised();
1095         $resumedata = $this->behaviour->get_resume_data();
1096         foreach ($resumedata as $name => $value) {
1097             if ($value instanceof question_file_loader) {
1098                 $resumedata[$name] = $value->get_question_file_saver();
1099             }
1100         }
1101         return $resumedata;
1102     }
1104     /**
1105      * Get a particular parameter from the current request. A wrapper round
1106      * {@link optional_param()}, except that the results is returned without
1107      * slashes.
1108      * @param string $name the paramter name.
1109      * @param int $type one of the standard PARAM_... constants, or one of the
1110      *      special extra constands defined by this class.
1111      * @param array $postdata (optional, only inteded for testing use) take the
1112      *      data from this array, instead of from $_POST.
1113      * @return mixed the requested value.
1114      */
1115     public function get_submitted_var($name, $type, $postdata = null) {
1116         switch ($type) {
1118             case self::PARAM_FILES:
1119                 return $this->process_response_files($name, $name, $postdata);
1121             case self::PARAM_RAW_FILES:
1122                 $var = $this->get_submitted_var($name, PARAM_RAW, $postdata);
1123                 return $this->process_response_files($name, $name . ':itemid', $postdata, $var);
1125             default:
1126                 if (is_null($postdata)) {
1127                     $var = optional_param($name, null, $type);
1128                 } else if (array_key_exists($name, $postdata)) {
1129                     $var = clean_param($postdata[$name], $type);
1130                 } else {
1131                     $var = null;
1132                 }
1134                 if ($var !== null) {
1135                     // Ensure that, if set, $var is a string. This is because later, after
1136                     // it has been saved to the database and loaded back it will be a string,
1137                     // so better if the type is predictably always a string.
1138                     $var = (string) $var;
1139                 }
1141                 return $var;
1142         }
1143     }
1145     /**
1146      * Validate the manual mark for a question.
1147      * @param string $currentmark the user input (e.g. '1,0', '1,0' or 'invalid'.
1148      * @return string any errors with the value, or '' if it is OK.
1149      */
1150     public function validate_manual_mark($currentmark) {
1151         if ($currentmark === null || $currentmark === '') {
1152             return '';
1153         }
1155         $mark = question_utils::clean_param_mark($currentmark);
1156         if ($mark === null) {
1157             return get_string('manualgradeinvalidformat', 'question');
1158         }
1160         $maxmark = $this->get_max_mark();
1161         if ($mark > $maxmark * $this->get_max_fraction() + question_utils::MARK_TOLERANCE ||
1162                 $mark < $maxmark * $this->get_min_fraction() - question_utils::MARK_TOLERANCE) {
1163             return get_string('manualgradeoutofrange', 'question');
1164         }
1166         return '';
1167     }
1169     /**
1170      * Handle a submitted variable representing uploaded files.
1171      * @param string $name the field name.
1172      * @param string $draftidname the field name holding the draft file area id.
1173      * @param array $postdata (optional, only inteded for testing use) take the
1174      *      data from this array, instead of from $_POST. At the moment, this
1175      *      behaves as if there were no files.
1176      * @param string $text optional reponse text.
1177      * @return question_file_saver that can be used to save the files later.
1178      */
1179     protected function process_response_files($name, $draftidname, $postdata = null, $text = null) {
1180         if ($postdata) {
1181             // For simulated posts, get the draft itemid from there.
1182             $draftitemid = $this->get_submitted_var($draftidname, PARAM_INT, $postdata);
1183         } else {
1184             $draftitemid = file_get_submitted_draft_itemid($draftidname);
1185         }
1187         if (!$draftitemid) {
1188             return null;
1189         }
1191         $filearea = str_replace($this->get_field_prefix(), '', $name);
1192         $filearea = str_replace('-', 'bf_', $filearea);
1193         $filearea = 'response_' . $filearea;
1194         return new question_file_saver($draftitemid, 'question', $filearea, $text);
1195     }
1197     /**
1198      * Get any data from the request that matches the list of expected params.
1199      *
1200      * @param array $expected variable name => PARAM_... constant.
1201      * @param null|array $postdata null to use real post data, otherwise an array of data to use.
1202      * @param string $extraprefix '-' or ''.
1203      * @return array name => value.
1204      */
1205     protected function get_expected_data($expected, $postdata, $extraprefix) {
1206         $submitteddata = array();
1207         foreach ($expected as $name => $type) {
1208             $value = $this->get_submitted_var(
1209                     $this->get_field_prefix() . $extraprefix . $name, $type, $postdata);
1210             if (!is_null($value)) {
1211                 $submitteddata[$extraprefix . $name] = $value;
1212             }
1213         }
1214         return $submitteddata;
1215     }
1217     /**
1218      * Get all the submitted question type data for this question, whithout checking
1219      * that it is valid or cleaning it in any way.
1220      *
1221      * @param null|array $postdata null to use real post data, otherwise an array of data to use.
1222      * @return array name => value.
1223      */
1224     public function get_all_submitted_qt_vars($postdata) {
1225         if (is_null($postdata)) {
1226             $postdata = $_POST;
1227         }
1229         $pattern = '/^' . preg_quote($this->get_field_prefix(), '/') . '[^-:]/';
1230         $prefixlen = strlen($this->get_field_prefix());
1232         $submitteddata = array();
1233         foreach ($postdata as $name => $value) {
1234             if (preg_match($pattern, $name)) {
1235                 $submitteddata[substr($name, $prefixlen)] = $value;
1236             }
1237         }
1239         return $submitteddata;
1240     }
1242     /**
1243      * Get all the sumbitted data belonging to this question attempt from the
1244      * current request.
1245      * @param array $postdata (optional, only inteded for testing use) take the
1246      *      data from this array, instead of from $_POST.
1247      * @return array name => value pairs that could be passed to {@link process_action()}.
1248      */
1249     public function get_submitted_data($postdata = null) {
1250         $this->ensure_question_initialised();
1252         $submitteddata = $this->get_expected_data(
1253                 $this->behaviour->get_expected_data(), $postdata, '-');
1255         $expected = $this->behaviour->get_expected_qt_data();
1256         $this->check_qt_var_name_restrictions($expected);
1258         if ($expected === self::USE_RAW_DATA) {
1259             $submitteddata += $this->get_all_submitted_qt_vars($postdata);
1260         } else {
1261             $submitteddata += $this->get_expected_data($expected, $postdata, '');
1262         }
1263         return $submitteddata;
1264     }
1266     /**
1267      * Ensure that no reserved prefixes are being used by installed
1268      * question types.
1269      * @param array $expected An array of question type variables
1270      */
1271     protected function check_qt_var_name_restrictions($expected) {
1272         global $CFG;
1274         if ($CFG->debugdeveloper && $expected !== self::USE_RAW_DATA) {
1275             foreach ($expected as $key => $value) {
1276                 if (strpos($key, 'bf_') !== false) {
1277                     debugging('The bf_ prefix is reserved and cannot be used by question types', DEBUG_DEVELOPER);
1278                 }
1279             }
1280         }
1281     }
1283     /**
1284      * Get a set of response data for this question attempt that would get the
1285      * best possible mark. If it is not possible to compute a correct
1286      * response, this method should return null.
1287      * @return array|null name => value pairs that could be passed to {@link process_action()}.
1288      */
1289     public function get_correct_response() {
1290         $this->ensure_question_initialised();
1291         $response = $this->question->get_correct_response();
1292         if (is_null($response)) {
1293             return null;
1294         }
1295         $imvars = $this->behaviour->get_correct_response();
1296         foreach ($imvars as $name => $value) {
1297             $response['-' . $name] = $value;
1298         }
1299         return $response;
1300     }
1302     /**
1303      * Change the quetsion summary. Note, that this is almost never necessary.
1304      * This method was only added to work around a limitation of the Opaque
1305      * protocol, which only sends questionLine at the end of an attempt.
1306      * @param string $questionsummary the new summary to set.
1307      */
1308     public function set_question_summary($questionsummary) {
1309         $this->questionsummary = $questionsummary;
1310         $this->observer->notify_attempt_modified($this);
1311     }
1313     /**
1314      * @return string a simple textual summary of the question that was asked.
1315      */
1316     public function get_question_summary() {
1317         return $this->questionsummary;
1318     }
1320     /**
1321      * @return string a simple textual summary of response given.
1322      */
1323     public function get_response_summary() {
1324         return $this->responsesummary;
1325     }
1327     /**
1328      * @return string a simple textual summary of the correct resonse.
1329      */
1330     public function get_right_answer_summary() {
1331         return $this->rightanswer;
1332     }
1334     /**
1335      * Whether this attempt at this question could be completed just by the
1336      * student interacting with the question, before {@link finish()} is called.
1337      *
1338      * @return boolean whether this attempt can finish naturally.
1339      */
1340     public function can_finish_during_attempt() {
1341         $this->ensure_question_initialised();
1342         return $this->behaviour->can_finish_during_attempt();
1343     }
1345     /**
1346      * Perform the action described by $submitteddata.
1347      * @param array $submitteddata the submitted data the determines the action.
1348      * @param int $timestamp the time to record for the action. (If not given, use now.)
1349      * @param int $userid the user to attribute the action to. (If not given, use the current user.)
1350      * @param int $existingstepid used by the regrade code.
1351      */
1352     public function process_action($submitteddata, $timestamp = null, $userid = null, $existingstepid = null) {
1353         $this->ensure_question_initialised();
1354         $pendingstep = new question_attempt_pending_step($submitteddata, $timestamp, $userid, $existingstepid);
1355         $this->discard_autosaved_step();
1356         if ($this->behaviour->process_action($pendingstep) == self::KEEP) {
1357             $this->add_step($pendingstep);
1358             if ($pendingstep->response_summary_changed()) {
1359                 $this->responsesummary = $pendingstep->get_new_response_summary();
1360             }
1361             if ($pendingstep->variant_number_changed()) {
1362                 $this->variant = $pendingstep->get_new_variant_number();
1363             }
1364         }
1365     }
1367     /**
1368      * Process an autosave.
1369      * @param array $submitteddata the submitted data the determines the action.
1370      * @param int $timestamp the time to record for the action. (If not given, use now.)
1371      * @param int $userid the user to attribute the action to. (If not given, use the current user.)
1372      * @return bool whether anything was saved.
1373      */
1374     public function process_autosave($submitteddata, $timestamp = null, $userid = null) {
1375         $this->ensure_question_initialised();
1376         $pendingstep = new question_attempt_pending_step($submitteddata, $timestamp, $userid);
1377         if ($this->behaviour->process_autosave($pendingstep) == self::KEEP) {
1378             $this->add_autosaved_step($pendingstep);
1379             return true;
1380         }
1381         return false;
1382     }
1384     /**
1385      * Perform a finish action on this question attempt. This corresponds to an
1386      * external finish action, for example the user pressing Submit all and finish
1387      * in the quiz, rather than using one of the controls that is part of the
1388      * question.
1389      *
1390      * @param int $timestamp the time to record for the action. (If not given, use now.)
1391      * @param int $userid the user to attribute the aciton to. (If not given, use the current user.)
1392      */
1393     public function finish($timestamp = null, $userid = null) {
1394         $this->ensure_question_initialised();
1395         $this->convert_autosaved_step_to_real_step();
1396         $this->process_action(array('-finish' => 1), $timestamp, $userid);
1397     }
1399     /**
1400      * Perform a regrade. This replays all the actions from $oldqa into this
1401      * attempt.
1402      * @param question_attempt $oldqa the attempt to regrade.
1403      * @param bool $finished whether the question attempt should be forced to be finished
1404      *      after the regrade, or whether it may still be in progress (default false).
1405      */
1406     public function regrade(question_attempt $oldqa, $finished) {
1407         $oldqa->ensure_question_initialised();
1408         $first = true;
1409         foreach ($oldqa->get_step_iterator() as $step) {
1410             $this->observer->notify_step_deleted($step, $this);
1412             if ($first) {
1413                 // First step of the attempt.
1414                 $first = false;
1415                 $this->start($oldqa->behaviour, $oldqa->get_variant(), $step->get_all_data(),
1416                         $step->get_timecreated(), $step->get_user_id(), $step->get_id());
1418             } else if ($step->has_behaviour_var('finish') && count($step->get_submitted_data()) > 1) {
1419                 // This case relates to MDL-32062. The upgrade code from 2.0
1420                 // generates attempts where the final submit of the question
1421                 // data, and the finish action, are in the same step. The system
1422                 // cannot cope with that, so convert the single old step into
1423                 // two new steps.
1424                 $submitteddata = $step->get_submitted_data();
1425                 unset($submitteddata['-finish']);
1426                 $this->process_action($submitteddata,
1427                         $step->get_timecreated(), $step->get_user_id(), $step->get_id());
1428                 $this->finish($step->get_timecreated(), $step->get_user_id());
1430             } else {
1431                 // This is the normal case. Replay the next step of the attempt.
1432                 if ($step === $oldqa->autosavedstep) {
1433                     $this->process_autosave($step->get_submitted_data(),
1434                             $step->get_timecreated(), $step->get_user_id());
1435                 } else {
1436                     $this->process_action($step->get_submitted_data(),
1437                             $step->get_timecreated(), $step->get_user_id(), $step->get_id());
1438                 }
1439             }
1440         }
1442         if ($finished) {
1443             $this->finish();
1444         }
1446         $this->set_flagged($oldqa->is_flagged());
1447     }
1449     /**
1450      * Change the max mark for this question_attempt.
1451      * @param float $maxmark the new max mark.
1452      */
1453     public function set_max_mark($maxmark) {
1454         $this->maxmark = $maxmark;
1455         $this->observer->notify_attempt_modified($this);
1456     }
1458     /**
1459      * Perform a manual grading action on this attempt.
1460      * @param string $comment the comment being added.
1461      * @param float $mark the new mark. If null, then only a comment is added.
1462      * @param int $commentformat the FORMAT_... for $comment. Must be given.
1463      * @param int $timestamp the time to record for the action. (If not given, use now.)
1464      * @param int $userid the user to attribute the aciton to. (If not given, use the current user.)
1465      */
1466     public function manual_grade($comment, $mark, $commentformat = null, $timestamp = null, $userid = null) {
1467         $this->ensure_question_initialised();
1468         $submitteddata = array('-comment' => $comment);
1469         if (is_null($commentformat)) {
1470             debugging('You should pass $commentformat to manual_grade.', DEBUG_DEVELOPER);
1471             $commentformat = FORMAT_HTML;
1472         }
1473         $submitteddata['-commentformat'] = $commentformat;
1474         if (!is_null($mark)) {
1475             $submitteddata['-mark'] = $mark;
1476             $submitteddata['-maxmark'] = $this->maxmark;
1477         }
1478         $this->process_action($submitteddata, $timestamp, $userid);
1479     }
1481     /** @return bool Whether this question attempt has had a manual comment added. */
1482     public function has_manual_comment() {
1483         foreach ($this->steps as $step) {
1484             if ($step->has_behaviour_var('comment')) {
1485                 return true;
1486             }
1487         }
1488         return false;
1489     }
1491     /**
1492      * @return array(string, int) the most recent manual comment that was added
1493      * to this question, the FORMAT_... it is and the step itself.
1494      */
1495     public function get_manual_comment() {
1496         foreach ($this->get_reverse_step_iterator() as $step) {
1497             if ($step->has_behaviour_var('comment')) {
1498                 return array($step->get_behaviour_var('comment'),
1499                         $step->get_behaviour_var('commentformat'),
1500                         $step);
1501             }
1502         }
1503         return array(null, null, null);
1504     }
1506     /**
1507      * This is used by the manual grading code, particularly in association with
1508      * validation. If there is a comment submitted in the request, then use that,
1509      * otherwise use the latest comment for this question.
1510      *
1511      * @return array with three elements, comment, commentformat and mark.
1512      */
1513     public function get_current_manual_comment() {
1514         $comment = $this->get_submitted_var($this->get_behaviour_field_name('comment'), PARAM_RAW);
1515         if (is_null($comment)) {
1516             return $this->get_manual_comment();
1517         } else {
1518             $commentformat = $this->get_submitted_var(
1519                     $this->get_behaviour_field_name('commentformat'), PARAM_INT);
1520             if ($commentformat === null) {
1521                 $commentformat = FORMAT_HTML;
1522             }
1523             return array($comment, $commentformat, null);
1524         }
1525     }
1527     /**
1528      * Break down a student response by sub part and classification. See also {@link question::classify_response}.
1529      * Used for response analysis.
1530      *
1531      * @param string $whichtries which tries to analyse for response analysis. Will be one of
1532      *      question_attempt::FIRST_TRY, LAST_TRY or ALL_TRIES. Defaults to question_attempt::LAST_TRY.
1533      * @return question_classified_response[]|question_classified_response[][] If $whichtries is
1534      *      question_attempt::FIRST_TRY or LAST_TRY index is subpartid and values are
1535      *      question_classified_response instances.
1536      *      If $whichtries is question_attempt::ALL_TRIES then first key is submitted response no
1537      *      and the second key is subpartid.
1538      */
1539     public function classify_response($whichtries = self::LAST_TRY) {
1540         $this->ensure_question_initialised();
1541         return $this->behaviour->classify_response($whichtries);
1542     }
1544     /**
1545      * Create a question_attempt_step from records loaded from the database.
1546      *
1547      * For internal use only.
1548      *
1549      * @param Iterator $records Raw records loaded from the database.
1550      * @param int $questionattemptid The id of the question_attempt to extract.
1551      * @param question_usage_observer $observer the observer that will be monitoring changes in us.
1552      * @param string $preferredbehaviour the preferred behaviour under which we are operating.
1553      * @return question_attempt The newly constructed question_attempt.
1554      */
1555     public static function load_from_records($records, $questionattemptid,
1556             question_usage_observer $observer, $preferredbehaviour) {
1557         $record = $records->current();
1558         while ($record->questionattemptid != $questionattemptid) {
1559             $records->next();
1560             if (!$records->valid()) {
1561                 throw new coding_exception("Question attempt {$questionattemptid} not found in the database.");
1562             }
1563             $record = $records->current();
1564         }
1566         try {
1567             $question = question_bank::load_question($record->questionid);
1568         } catch (Exception $e) {
1569             // The question must have been deleted somehow. Create a missing
1570             // question to use in its place.
1571             $question = question_bank::get_qtype('missingtype')->make_deleted_instance(
1572                     $record->questionid, $record->maxmark + 0);
1573         }
1575         $qa = new question_attempt($question, $record->questionusageid,
1576                 null, $record->maxmark + 0);
1577         $qa->set_database_id($record->questionattemptid);
1578         $qa->set_slot($record->slot);
1579         $qa->variant = $record->variant + 0;
1580         $qa->minfraction = $record->minfraction + 0;
1581         $qa->maxfraction = $record->maxfraction + 0;
1582         $qa->set_flagged($record->flagged);
1583         $qa->questionsummary = $record->questionsummary;
1584         $qa->rightanswer = $record->rightanswer;
1585         $qa->responsesummary = $record->responsesummary;
1586         $qa->timemodified = $record->timemodified;
1588         $qa->behaviour = question_engine::make_behaviour(
1589                 $record->behaviour, $qa, $preferredbehaviour);
1590         $qa->observer = $observer;
1592         // If attemptstepid is null (which should not happen, but has happened
1593         // due to corrupt data, see MDL-34251) then the current pointer in $records
1594         // will not be advanced in the while loop below, and we get stuck in an
1595         // infinite loop, since this method is supposed to always consume at
1596         // least one record. Therefore, in this case, advance the record here.
1597         if (is_null($record->attemptstepid)) {
1598             $records->next();
1599         }
1601         $i = 0;
1602         $autosavedstep = null;
1603         $autosavedsequencenumber = null;
1604         while ($record && $record->questionattemptid == $questionattemptid && !is_null($record->attemptstepid)) {
1605             $sequencenumber = $record->sequencenumber;
1606             $nextstep = question_attempt_step::load_from_records($records, $record->attemptstepid,
1607                     $qa->get_question(false)->get_type_name());
1609             if ($sequencenumber < 0) {
1610                 if (!$autosavedstep) {
1611                     $autosavedstep = $nextstep;
1612                     $autosavedsequencenumber = -$sequencenumber;
1613                 } else {
1614                     // Old redundant data. Mark it for deletion.
1615                     $qa->observer->notify_step_deleted($nextstep, $qa);
1616                 }
1617             } else {
1618                 $qa->steps[$i] = $nextstep;
1619                 $i++;
1620             }
1622             if ($records->valid()) {
1623                 $record = $records->current();
1624             } else {
1625                 $record = false;
1626             }
1627         }
1629         if ($autosavedstep) {
1630             if ($autosavedsequencenumber >= $i) {
1631                 $qa->autosavedstep = $autosavedstep;
1632                 $qa->steps[$i] = $qa->autosavedstep;
1633             } else {
1634                 $qa->observer->notify_step_deleted($autosavedstep, $qa);
1635             }
1636         }
1638         return $qa;
1639     }
1641     /**
1642      * This method is part of the lazy-initialisation of question objects.
1643      *
1644      * Methods which require $this->question to be fully initialised
1645      * (to have had init_first_step or apply_attempt_state called on it)
1646      * should call this method before proceeding.
1647      */
1648     protected function ensure_question_initialised() {
1649         if ($this->questioninitialised === self::QUESTION_STATE_APPLIED) {
1650             return; // Already done.
1651         }
1653         if (empty($this->steps)) {
1654             throw new coding_exception('You must call start() before doing anything to a question_attempt().');
1655         }
1657         $this->question->apply_attempt_state($this->steps[0]);
1658         $this->questioninitialised = self::QUESTION_STATE_APPLIED;
1659     }
1661     /**
1662      * Allow access to steps with responses submitted by students for grading in a question attempt.
1663      *
1664      * @return question_attempt_steps_with_submitted_response_iterator to access all steps with submitted data for questions that
1665      *                                                      allow multiple submissions that count towards grade, per attempt.
1666      */
1667     public function get_steps_with_submitted_response_iterator() {
1668         return new question_attempt_steps_with_submitted_response_iterator($this);
1669     }
1673 /**
1674  * This subclass of question_attempt pretends that only part of the step history
1675  * exists. It is used for rendering the question in past states.
1676  *
1677  * All methods that try to modify the question_attempt throw exceptions.
1678  *
1679  * @copyright  2010 The Open University
1680  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1681  */
1682 class question_attempt_with_restricted_history extends question_attempt {
1683     /**
1684      * @var question_attempt the underlying question_attempt.
1685      */
1686     protected $baseqa;
1688     /**
1689      * Create a question_attempt_with_restricted_history
1690      * @param question_attempt $baseqa The question_attempt to make a restricted version of.
1691      * @param int $lastseq the index of the last step to include.
1692      * @param string $preferredbehaviour the preferred behaviour. It is slightly
1693      *      annoying that this needs to be passed, but unavoidable for now.
1694      */
1695     public function __construct(question_attempt $baseqa, $lastseq, $preferredbehaviour) {
1696         $this->baseqa = $baseqa->get_full_qa();
1698         if ($lastseq < 0 || $lastseq >= $this->baseqa->get_num_steps()) {
1699             throw new coding_exception('$lastseq out of range', $lastseq);
1700         }
1702         $this->steps = array_slice($this->baseqa->steps, 0, $lastseq + 1);
1703         $this->observer = new question_usage_null_observer();
1705         // This should be a straight copy of all the remaining fields.
1706         $this->id = $this->baseqa->id;
1707         $this->usageid = $this->baseqa->usageid;
1708         $this->slot = $this->baseqa->slot;
1709         $this->question = $this->baseqa->question;
1710         $this->maxmark = $this->baseqa->maxmark;
1711         $this->minfraction = $this->baseqa->minfraction;
1712         $this->maxfraction = $this->baseqa->maxfraction;
1713         $this->questionsummary = $this->baseqa->questionsummary;
1714         $this->responsesummary = $this->baseqa->responsesummary;
1715         $this->rightanswer = $this->baseqa->rightanswer;
1716         $this->flagged = $this->baseqa->flagged;
1718         // Except behaviour, where we need to create a new one.
1719         $this->behaviour = question_engine::make_behaviour(
1720                 $this->baseqa->get_behaviour_name(), $this, $preferredbehaviour);
1721     }
1723     public function get_full_qa() {
1724         return $this->baseqa;
1725     }
1727     public function get_full_step_iterator() {
1728         return $this->baseqa->get_step_iterator();
1729     }
1731     protected function add_step(question_attempt_step $step) {
1732         throw new coding_exception('Cannot modify a question_attempt_with_restricted_history.');
1733     }
1734     public function process_action($submitteddata, $timestamp = null, $userid = null, $existingstepid = null) {
1735         throw new coding_exception('Cannot modify a question_attempt_with_restricted_history.');
1736     }
1737     public function start($preferredbehaviour, $variant, $submitteddata = array(), $timestamp = null, $userid = null, $existingstepid = null) {
1738         throw new coding_exception('Cannot modify a question_attempt_with_restricted_history.');
1739     }
1741     public function set_database_id($id) {
1742         throw new coding_exception('Cannot modify a question_attempt_with_restricted_history.');
1743     }
1744     public function set_flagged($flagged) {
1745         throw new coding_exception('Cannot modify a question_attempt_with_restricted_history.');
1746     }
1747     public function set_slot($slot) {
1748         throw new coding_exception('Cannot modify a question_attempt_with_restricted_history.');
1749     }
1750     public function set_question_summary($questionsummary) {
1751         throw new coding_exception('Cannot modify a question_attempt_with_restricted_history.');
1752     }
1753     public function set_usage_id($usageid) {
1754         throw new coding_exception('Cannot modify a question_attempt_with_restricted_history.');
1755     }
1759 /**
1760  * A class abstracting access to the {@link question_attempt::$states} array.
1761  *
1762  * This is actively linked to question_attempt. If you add an new step
1763  * mid-iteration, then it will be included.
1764  *
1765  * @copyright  2009 The Open University
1766  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1767  */
1768 class question_attempt_step_iterator implements Iterator, ArrayAccess {
1769     /** @var question_attempt the question_attempt being iterated over. */
1770     protected $qa;
1771     /** @var integer records the current position in the iteration. */
1772     protected $i;
1774     /**
1775      * Do not call this constructor directly.
1776      * Use {@link question_attempt::get_step_iterator()}.
1777      * @param question_attempt $qa the attempt to iterate over.
1778      */
1779     public function __construct(question_attempt $qa) {
1780         $this->qa = $qa;
1781         $this->rewind();
1782     }
1784     /** @return question_attempt_step */
1785     public function current() {
1786         return $this->offsetGet($this->i);
1787     }
1788     /** @return int */
1789     public function key() {
1790         return $this->i;
1791     }
1792     public function next() {
1793         ++$this->i;
1794     }
1795     public function rewind() {
1796         $this->i = 0;
1797     }
1798     /** @return bool */
1799     public function valid() {
1800         return $this->offsetExists($this->i);
1801     }
1803     /** @return bool */
1804     public function offsetExists($i) {
1805         return $i >= 0 && $i < $this->qa->get_num_steps();
1806     }
1807     /** @return question_attempt_step */
1808     public function offsetGet($i) {
1809         return $this->qa->get_step($i);
1810     }
1811     public function offsetSet($offset, $value) {
1812         throw new coding_exception('You are only allowed read-only access to question_attempt::states through a question_attempt_step_iterator. Cannot set.');
1813     }
1814     public function offsetUnset($offset) {
1815         throw new coding_exception('You are only allowed read-only access to question_attempt::states through a question_attempt_step_iterator. Cannot unset.');
1816     }
1820 /**
1821  * A variant of {@link question_attempt_step_iterator} that iterates through the
1822  * steps in reverse order.
1823  *
1824  * @copyright  2009 The Open University
1825  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1826  */
1827 class question_attempt_reverse_step_iterator extends question_attempt_step_iterator {
1828     public function next() {
1829         --$this->i;
1830     }
1832     public function rewind() {
1833         $this->i = $this->qa->get_num_steps() - 1;
1834     }
1837 /**
1838  * A variant of {@link question_attempt_step_iterator} that iterates through the
1839  * steps with submitted tries.
1840  *
1841  * @copyright  2014 The Open University
1842  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1843  */
1844 class question_attempt_steps_with_submitted_response_iterator extends question_attempt_step_iterator implements Countable {
1846     /** @var question_attempt the question_attempt being iterated over. */
1847     protected $qa;
1849     /** @var integer records the current position in the iteration. */
1850     protected $submittedresponseno;
1852     /**
1853      * Index is the submitted response number and value is the step no.
1854      *
1855      * @var int[]
1856      */
1857     protected $stepswithsubmittedresponses;
1859     /**
1860      * Do not call this constructor directly.
1861      * Use {@link question_attempt::get_submission_step_iterator()}.
1862      * @param question_attempt $qa the attempt to iterate over.
1863      */
1864     public function __construct(question_attempt $qa) {
1865         $this->qa = $qa;
1866         $this->find_steps_with_submitted_response();
1867         $this->rewind();
1868     }
1870     /**
1871      * Find the step nos  in which a student has submitted a response. Including any step with a response that is saved before
1872      * the question attempt finishes.
1873      *
1874      * Called from constructor, should not be called from elsewhere.
1875      *
1876      */
1877     protected function find_steps_with_submitted_response() {
1878         $stepnos = array();
1879         $lastsavedstep = null;
1880         foreach ($this->qa->get_step_iterator() as $stepno => $step) {
1881             if ($this->qa->get_behaviour()->step_has_a_submitted_response($step)) {
1882                 $stepnos[] = $stepno;
1883                 $lastsavedstep = null;
1884             } else {
1885                 $qtdata = $step->get_qt_data();
1886                 if (count($qtdata)) {
1887                     $lastsavedstep = $stepno;
1888                 }
1889             }
1890         }
1892         if (!is_null($lastsavedstep)) {
1893             $stepnos[] = $lastsavedstep;
1894         }
1895         if (empty($stepnos)) {
1896             $this->stepswithsubmittedresponses = array();
1897         } else {
1898             // Re-index array so index starts with 1.
1899             $this->stepswithsubmittedresponses = array_combine(range(1, count($stepnos)), $stepnos);
1900         }
1901     }
1903     /** @return question_attempt_step */
1904     public function current() {
1905         return $this->offsetGet($this->submittedresponseno);
1906     }
1907     /** @return int */
1908     public function key() {
1909         return $this->submittedresponseno;
1910     }
1911     public function next() {
1912         ++$this->submittedresponseno;
1913     }
1914     public function rewind() {
1915         $this->submittedresponseno = 1;
1916     }
1917     /** @return bool */
1918     public function valid() {
1919         return $this->submittedresponseno >= 1 && $this->submittedresponseno <= count($this->stepswithsubmittedresponses);
1920     }
1922     /**
1923      * @param int $submittedresponseno
1924      * @return bool
1925      */
1926     public function offsetExists($submittedresponseno) {
1927         return $submittedresponseno >= 1;
1928     }
1930     /**
1931      * @param int $submittedresponseno
1932      * @return question_attempt_step
1933      */
1934     public function offsetGet($submittedresponseno) {
1935         if ($submittedresponseno > count($this->stepswithsubmittedresponses)) {
1936             return null;
1937         } else {
1938             return $this->qa->get_step($this->step_no_for_try($submittedresponseno));
1939         }
1940     }
1942     /**
1943      * @return int the count of steps with tries.
1944      */
1945     public function count() {
1946         return count($this->stepswithsubmittedresponses);
1947     }
1949     /**
1950      * @param int $submittedresponseno
1951      * @throws coding_exception
1952      * @return int|null the step number or null if there is no such submitted response.
1953      */
1954     public function step_no_for_try($submittedresponseno) {
1955         if (isset($this->stepswithsubmittedresponses[$submittedresponseno])) {
1956             return $this->stepswithsubmittedresponses[$submittedresponseno];
1957         } else if ($submittedresponseno > count($this->stepswithsubmittedresponses)) {
1958             return null;
1959         } else {
1960             throw new coding_exception('Try number not found. It should be 1 or more.');
1961         }
1962     }
1964     public function offsetSet($offset, $value) {
1965         throw new coding_exception('You are only allowed read-only access to question_attempt::states '.
1966                                    'through a question_attempt_step_iterator. Cannot set.');
1967     }
1968     public function offsetUnset($offset) {
1969         throw new coding_exception('You are only allowed read-only access to question_attempt::states '.
1970                                    'through a question_attempt_step_iterator. Cannot unset.');
1971     }