"Fossies" - the Fresh Open Source Software Archive

Member "moodle/question/engine/questionattempt.php" (6 Sep 2019, 71385 Bytes) of package /linux/www/moodle-3.6.6.tgz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) PHP source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. For more information about "questionattempt.php" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 3.6.5_vs_3.6.6.

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