Search moodle.org's
Developer Documentation

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.
  • Differences Between: [Versions 37 and 311] [Versions 38 and 311]

       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       * Constant used when calling classify response.
      72       */
      73      const FIRST_TRY = 'firsttry';
      74  
      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';
      80  
      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';
      86  
      87      /**
      88       * @var bool used to manage the lazy-initialisation of question objects.
      89       */
      90      const QUESTION_STATE_NOT_APPLIED = false;
      91  
      92      /**
      93       * @var bool used to manage the lazy-initialisation of question objects.
      94       */
      95      const QUESTION_STATE_APPLIED = true;
      96  
      97      /** @var integer if this attempts is stored in the question_attempts table, the id of that row. */
      98      protected $id = null;
      99  
     100      /** @var integer|string the id of the question_usage_by_activity we belong to. */
     101      protected $usageid;
     102  
     103      /** @var integer the number used to identify this question_attempt within the usage. */
     104      protected $slot = null;
     105  
     106      /**
     107       * @var question_behaviour the behaviour controlling this attempt.
     108       * null until {@link start()} is called.
     109       */
     110      protected $behaviour = null;
     111  
     112      /** @var question_definition the question this is an attempt at. */
     113      protected $question;
     114  
     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;
     120  
     121      /** @var int which variant of the question to use. */
     122      protected $variant;
     123  
     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;
     130  
     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;
     136  
     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;
     142  
     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;
     148  
     149      /**
     150       * @var string plain text summary of the response the student gave.
     151       * Intended for reporting purposes.
     152       */
     153      protected $responsesummary = null;
     154  
     155      /**
     156       * @var int last modified time.
     157       */
     158      public $timemodified = null;
     159  
     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;
     166  
     167      /** @var array of {@link question_attempt_step}s. The steps in this attempt. */
     168      protected $steps = array();
     169  
     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;
     175  
     176      /** @var boolean whether the user has flagged this attempt within the usage. */
     177      protected $flagged = false;
     178  
     179      /** @var question_usage_observer tracks changes to the useage this attempt is part of.*/
     180      protected $observer;
     181  
     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      /**#@-*/
     189  
     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      }
     218  
     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      }
     227  
     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      }
     241  
     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      }
     250  
     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      }
     258  
     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      }
     267  
     268      /** @return int the number used to identify this question_attempt within the usage. */
     269      public function get_slot() {
     270          return $this->slot;
     271      }
     272  
     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      }
     280  
     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      }
     289  
     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      }
     298  
     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      }
     303  
     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      }
     312  
     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      }
     317  
     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      }
     331  
     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      }
     340  
     341      /** @return bool whether this question is currently flagged. */
     342      public function is_flagged() {
     343          return $this->flagged;
     344      }
     345  
     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      }
     355  
     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      }
     369  
     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      }
     383  
     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      }
     396  
     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      }
     409  
     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      }
     420  
     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      }
     433  
     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      }
     448  
     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      }
     457  
     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      }
     469  
     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      }
     477  
     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      }
     485  
     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      }
     497  
     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      }
     505  
     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      }
     523  
     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      }
     538  
     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      }
     553  
     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      }
     572  
     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      }
     589  
     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      }
     606  
     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          }
     622  
     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      }
     628  
     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      }
     647  
     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      }
     656  
     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      }
     669  
     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      }
     678  
     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      }
     685  
     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      }
     694  
     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      }
     700  
     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      }
     708  
     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          }
     724  
     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          }
     731  
     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      }
     735  
     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      }
     746  
     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      }
     756  
     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      }
     764  
     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      }
     772  
     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      }
     782  
     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      }
     794  
     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      }
     805  
     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      }
     813  
     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      }
     823  
     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      }
     832  
     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      }
     847  
     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      }
     856  
     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      }
     871  
     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      }
     887  
     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      }
     909  
     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      }
     925  
     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      }
     943  
     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      }
     957  
     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      }
     967  
     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      }
     979  
     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          }
     987  
     988          $autosaved = array_pop($this->steps);
     989          $this->autosavedstep = null;
     990          $this->observer->notify_step_deleted($autosaved, $this);
     991      }
     992  
     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          }
    1001  
    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          }
    1006  
    1007          $this->observer->notify_step_modified($this->autosavedstep, $this, key($this->steps));
    1008          $this->autosavedstep = null;
    1009      }
    1010  
    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      }
    1020  
    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) {
    1039  
    1040          if ($this->get_num_steps() > 0) {
    1041              throw new coding_exception('Cannot start a question that is already started.');
    1042          }
    1043  
    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          }
    1053  
    1054          // Record the minimum and maximum fractions.
    1055          $this->minfraction = $this->behaviour->get_min_fraction();
    1056          $this->maxfraction = $this->behaviour->get_max_fraction();
    1057  
    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);
    1068  
    1069          // Record questionline and correct answer.
    1070          $this->questionsummary = $this->behaviour->get_question_summary();
    1071          $this->rightanswer = $this->behaviour->get_right_answer_summary();
    1072      }
    1073  
    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      }
    1087  
    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      }
    1103  
    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) {
    1117  
    1118              case self::PARAM_FILES:
    1119                  return $this->process_response_files($name, $name, $postdata);
    1120  
    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);
    1124  
    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                  }
    1133  
    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                  }
    1140  
    1141                  return $var;
    1142          }
    1143      }
    1144  
    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          }
    1154  
    1155          $mark = question_utils::clean_param_mark($currentmark);
    1156          if ($mark === null) {
    1157              return get_string('manualgradeinvalidformat', 'question');
    1158          }
    1159  
    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          }
    1165  
    1166          return '';
    1167      }
    1168  
    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          }
    1186  
    1187          if (!$draftitemid) {
    1188              return null;
    1189          }
    1190  
    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      }
    1196  
    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      }
    1216  
    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          }
    1228  
    1229          $pattern = '/^' . preg_quote($this->get_field_prefix(), '/') . '[^-:]/';
    1230          $prefixlen = strlen($this->get_field_prefix());
    1231  
    1232          $submitteddata = array();
    1233          foreach ($postdata as $name => $value) {
    1234              if (preg_match($pattern, $name)) {
    1235                  $submitteddata[substr($name, $prefixlen)] = $value;
    1236              }
    1237          }
    1238  
    1239          return $submitteddata;
    1240      }
    1241  
    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();
    1251  
    1252          $submitteddata = $this->get_expected_data(
    1253                  $this->behaviour->get_expected_data(), $postdata, '-');
    1254  
    1255          $expected = $this->behaviour->get_expected_qt_data();
    1256          $this->check_qt_var_name_restrictions($expected);
    1257  
    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      }
    1265  
    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;
    1273  
    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      }
    1282  
    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      }
    1301  
    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      }
    1312  
    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      }
    1319  
    1320      /**
    1321       * @return string a simple textual summary of response given.
    1322       */
    1323      public function get_response_summary() {
    1324          return $this->responsesummary;
    1325      }
    1326  
    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      }
    1333  
    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      }
    1344  
    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      }
    1366  
    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      }
    1383  
    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      }
    1398  
    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);
    1411  
    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());
    1417  
    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());
    1429  
    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          }
    1441  
    1442          if ($finished) {
    1443              $this->finish();
    1444          }
    1445  
    1446          $this->set_flagged($oldqa->is_flagged());
    1447      }
    1448  
    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      }
    1457  
    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      }
    1480  
    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      }
    1490  
    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      }
    1505  
    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      }
    1526  
    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      }
    1543  
    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          }
    1565  
    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          }
    1574  
    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;
    1587  
    1588          $qa->behaviour = question_engine::make_behaviour(
    1589                  $record->behaviour, $qa, $preferredbehaviour);
    1590          $qa->observer = $observer;
    1591  
    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          }
    1600  
    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());
    1608  
    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              }
    1621  
    1622              if ($records->valid()) {
    1623                  $record = $records->current();
    1624              } else {
    1625                  $record = false;
    1626              }
    1627          }
    1628  
    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          }
    1637  
    1638          return $qa;
    1639      }
    1640  
    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          }
    1652  
    1653          if (empty($this->steps)) {
    1654              throw new coding_exception('You must call start() before doing anything to a question_attempt().');
    1655          }
    1656  
    1657          $this->question->apply_attempt_state($this->steps[0]);
    1658          $this->questioninitialised = self::QUESTION_STATE_APPLIED;
    1659      }
    1660  
    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      }
    1670  }
    1671  
    1672  
    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;
    1687  
    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();
    1697  
    1698          if ($lastseq < 0 || $lastseq >= $this->baseqa->get_num_steps()) {
    1699              throw new coding_exception('$lastseq out of range', $lastseq);
    1700          }
    1701  
    1702          $this->steps = array_slice($this->baseqa->steps, 0, $lastseq + 1);
    1703          $this->observer = new question_usage_null_observer();
    1704  
    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;
    1717  
    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      }
    1722  
    1723      public function get_full_qa() {
    1724          return $this->baseqa;
    1725      }
    1726  
    1727      public function get_full_step_iterator() {
    1728          return $this->baseqa->get_step_iterator();
    1729      }
    1730  
    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      }
    1740  
    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      }
    1756  }
    1757  
    1758  
    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;
    1773  
    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      }
    1783  
    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      }
    1802  
    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      }
    1817  }
    1818  
    1819  
    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      }
    1831  
    1832      public function rewind() {
    1833          $this->i = $this->qa->get_num_steps() - 1;
    1834      }
    1835  }
    1836  
    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 {
    1845  
    1846      /** @var question_attempt the question_attempt being iterated over. */
    1847      protected $qa;
    1848  
    1849      /** @var integer records the current position in the iteration. */
    1850      protected $submittedresponseno;
    1851  
    1852      /**
    1853       * Index is the submitted response number and value is the step no.
    1854       *
    1855       * @var int[]
    1856       */
    1857      protected $stepswithsubmittedresponses;
    1858  
    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      }
    1869  
    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          }
    1891  
    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      }
    1902  
    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      }
    1921  
    1922      /**
    1923       * @param int $submittedresponseno
    1924       * @return bool
    1925       */
    1926      public function offsetExists($submittedresponseno) {
    1927          return $submittedresponseno >= 1;
    1928      }
    1929  
    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      }
    1941  
    1942      /**
    1943       * @return int the count of steps with tries.
    1944       */
    1945      public function count() {
    1946          return count($this->stepswithsubmittedresponses);
    1947      }
    1948  
    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      }
    1963  
    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      }
    1972  
    1973  }