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