| 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 usage class, and a few related classes.
 | 
        
           |  |  | 19 |  *
 | 
        
           |  |  | 20 |  * @package    moodlecore
 | 
        
           |  |  | 21 |  * @subpackage questionengine
 | 
        
           |  |  | 22 |  * @copyright  2009 The Open University
 | 
        
           |  |  | 23 |  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | 
        
           |  |  | 24 |  */
 | 
        
           |  |  | 25 |   | 
        
           |  |  | 26 |   | 
        
           |  |  | 27 | defined('MOODLE_INTERNAL') || die();
 | 
        
           |  |  | 28 |   | 
        
           |  |  | 29 |   | 
        
           |  |  | 30 | /**
 | 
        
           |  |  | 31 |  * This class keeps track of a group of questions that are being attempted,
 | 
        
           |  |  | 32 |  * and which state, and so on, each one is currently in.
 | 
        
           |  |  | 33 |  *
 | 
        
           |  |  | 34 |  * A quiz attempt or a lesson attempt could use an instance of this class to
 | 
        
           |  |  | 35 |  * keep track of all the questions in the attempt and process student submissions.
 | 
        
           |  |  | 36 |  * It is basically a collection of {@question_attempt} objects.
 | 
        
           |  |  | 37 |  *
 | 
        
           |  |  | 38 |  * The questions being attempted as part of this usage are identified by an integer
 | 
        
           |  |  | 39 |  * that is passed into many of the methods as $slot. ($question->id is not
 | 
        
           |  |  | 40 |  * used so that the same question can be used more than once in an attempt.)
 | 
        
           |  |  | 41 |  *
 | 
        
           |  |  | 42 |  * Normally, calling code should be able to do everything it needs to be calling
 | 
        
           |  |  | 43 |  * methods of this class. You should not normally need to get individual
 | 
        
           |  |  | 44 |  * {@question_attempt} objects and play around with their inner workind, in code
 | 
        
           |  |  | 45 |  * that it outside the quetsion engine.
 | 
        
           |  |  | 46 |  *
 | 
        
           |  |  | 47 |  * Instances of this class correspond to rows in the question_usages table.
 | 
        
           |  |  | 48 |  *
 | 
        
           |  |  | 49 |  * @copyright  2009 The Open University
 | 
        
           |  |  | 50 |  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | 
        
           |  |  | 51 |  */
 | 
        
           |  |  | 52 | class question_usage_by_activity {
 | 
        
           |  |  | 53 |     /**
 | 
        
           |  |  | 54 |      * @var integer|string the id for this usage. If this usage was loaded from
 | 
        
           |  |  | 55 |      * the database, then this is the database id. Otherwise a unique random
 | 
        
           |  |  | 56 |      * string is used.
 | 
        
           |  |  | 57 |      */
 | 
        
           |  |  | 58 |     protected $id = null;
 | 
        
           |  |  | 59 |   | 
        
           |  |  | 60 |     /**
 | 
        
           |  |  | 61 |      * @var string name of an archetypal behaviour, that should be used
 | 
        
           |  |  | 62 |      * by questions in this usage if possible.
 | 
        
           |  |  | 63 |      */
 | 
        
           |  |  | 64 |     protected $preferredbehaviour = null;
 | 
        
           |  |  | 65 |   | 
        
           |  |  | 66 |     /** @var context the context this usage belongs to. */
 | 
        
           |  |  | 67 |     protected $context;
 | 
        
           |  |  | 68 |   | 
        
           |  |  | 69 |     /** @var string plugin name of the plugin this usage belongs to. */
 | 
        
           |  |  | 70 |     protected $owningcomponent;
 | 
        
           |  |  | 71 |   | 
        
           |  |  | 72 |     /** @var question_attempt[] {@link question_attempt}s that make up this usage. */
 | 
        
           |  |  | 73 |     protected $questionattempts = array();
 | 
        
           |  |  | 74 |   | 
        
           |  |  | 75 |     /** @var question_usage_observer that tracks changes to this usage. */
 | 
        
           |  |  | 76 |     protected $observer;
 | 
        
           |  |  | 77 |   | 
        
           |  |  | 78 |     /**
 | 
        
           |  |  | 79 |      * Create a new instance. Normally, calling code should use
 | 
        
           |  |  | 80 |      * {@link question_engine::make_questions_usage_by_activity()} or
 | 
        
           |  |  | 81 |      * {@link question_engine::load_questions_usage_by_activity()} rather than
 | 
        
           |  |  | 82 |      * calling this constructor directly.
 | 
        
           |  |  | 83 |      *
 | 
        
           |  |  | 84 |      * @param string $component the plugin creating this attempt. For example mod_quiz.
 | 
        
           |  |  | 85 |      * @param object $context the context this usage belongs to.
 | 
        
           |  |  | 86 |      */
 | 
        
           |  |  | 87 |     public function __construct($component, $context) {
 | 
        
           |  |  | 88 |         $this->owningcomponent = $component;
 | 
        
           |  |  | 89 |         $this->context = $context;
 | 
        
           |  |  | 90 |         $this->observer = new question_usage_null_observer();
 | 
        
           |  |  | 91 |     }
 | 
        
           |  |  | 92 |   | 
        
           |  |  | 93 |     /**
 | 
        
           |  |  | 94 |      * @param string $behaviour the name of an archetypal behaviour, that should
 | 
        
           |  |  | 95 |      * be used by questions in this usage if possible.
 | 
        
           |  |  | 96 |      */
 | 
        
           |  |  | 97 |     public function set_preferred_behaviour($behaviour) {
 | 
        
           |  |  | 98 |         $this->preferredbehaviour = $behaviour;
 | 
        
           |  |  | 99 |         $this->observer->notify_modified();
 | 
        
           |  |  | 100 |     }
 | 
        
           |  |  | 101 |   | 
        
           |  |  | 102 |     /** @return string the name of the preferred behaviour. */
 | 
        
           |  |  | 103 |     public function get_preferred_behaviour() {
 | 
        
           |  |  | 104 |         return $this->preferredbehaviour;
 | 
        
           |  |  | 105 |     }
 | 
        
           |  |  | 106 |   | 
        
           |  |  | 107 |     /** @return context the context this usage belongs to. */
 | 
        
           |  |  | 108 |     public function get_owning_context() {
 | 
        
           |  |  | 109 |         return $this->context;
 | 
        
           |  |  | 110 |     }
 | 
        
           |  |  | 111 |   | 
        
           |  |  | 112 |     /** @return string the name of the plugin that owns this attempt. */
 | 
        
           |  |  | 113 |     public function get_owning_component() {
 | 
        
           |  |  | 114 |         return $this->owningcomponent;
 | 
        
           |  |  | 115 |     }
 | 
        
           |  |  | 116 |   | 
        
           |  |  | 117 |     /** @return int|string If this usage came from the database, then the id
 | 
        
           |  |  | 118 |      * from the question_usages table is returned. Otherwise a random string is
 | 
        
           |  |  | 119 |      * returned. */
 | 
        
           |  |  | 120 |     public function get_id() {
 | 
        
           |  |  | 121 |         if (is_null($this->id)) {
 | 
        
           |  |  | 122 |             $this->id = random_string(10);
 | 
        
           |  |  | 123 |         }
 | 
        
           |  |  | 124 |         return $this->id;
 | 
        
           |  |  | 125 |     }
 | 
        
           |  |  | 126 |   | 
        
           |  |  | 127 |     /**
 | 
        
           |  |  | 128 |      * For internal use only. Used by {@link question_engine_data_mapper} to set
 | 
        
           |  |  | 129 |      * the id when a usage is saved to the database.
 | 
        
           |  |  | 130 |      * @param int $id the newly determined id for this usage.
 | 
        
           |  |  | 131 |      */
 | 
        
           |  |  | 132 |     public function set_id_from_database($id) {
 | 
        
           |  |  | 133 |         $this->id = $id;
 | 
        
           |  |  | 134 |         foreach ($this->questionattempts as $qa) {
 | 
        
           |  |  | 135 |             $qa->set_usage_id($id);
 | 
        
           |  |  | 136 |         }
 | 
        
           |  |  | 137 |     }
 | 
        
           |  |  | 138 |   | 
        
           |  |  | 139 |     /** @return question_usage_observer that is tracking changes made to this usage. */
 | 
        
           |  |  | 140 |     public function get_observer() {
 | 
        
           |  |  | 141 |         return $this->observer;
 | 
        
           |  |  | 142 |     }
 | 
        
           |  |  | 143 |   | 
        
           |  |  | 144 |     /**
 | 
        
           |  |  | 145 |      * You should almost certainly not call this method from your code. It is for
 | 
        
           |  |  | 146 |      * internal use only.
 | 
        
           |  |  | 147 |      * @param question_usage_observer that should be used to tracking changes made to this usage.
 | 
        
           |  |  | 148 |      */
 | 
        
           |  |  | 149 |     public function set_observer($observer) {
 | 
        
           |  |  | 150 |         $this->observer = $observer;
 | 
        
           |  |  | 151 |         foreach ($this->questionattempts as $qa) {
 | 
        
           |  |  | 152 |             $qa->set_observer($observer);
 | 
        
           |  |  | 153 |         }
 | 
        
           |  |  | 154 |     }
 | 
        
           |  |  | 155 |   | 
        
           |  |  | 156 |     /**
 | 
        
           |  |  | 157 |      * Add another question to this usage.
 | 
        
           |  |  | 158 |      *
 | 
        
           |  |  | 159 |      * The added question is not started until you call {@link start_question()}
 | 
        
           |  |  | 160 |      * on it.
 | 
        
           |  |  | 161 |      *
 | 
        
           |  |  | 162 |      * @param question_definition $question the question to add.
 | 
        
           |  |  | 163 |      * @param number $maxmark the maximum this question will be marked out of in
 | 
        
           |  |  | 164 |      *      this attempt (optional). If not given, $question->defaultmark is used.
 | 
        
           |  |  | 165 |      * @return int the number used to identify this question within this usage.
 | 
        
           |  |  | 166 |      */
 | 
        
           |  |  | 167 |     public function add_question(question_definition $question, $maxmark = null) {
 | 
        
           |  |  | 168 |         $qa = new question_attempt($question, $this->get_id(), $this->observer, $maxmark);
 | 
        
           |  |  | 169 |         $qa->set_slot($this->next_slot_number());
 | 
        
           |  |  | 170 |         $this->questionattempts[$this->next_slot_number()] = $qa;
 | 
        
           |  |  | 171 |         $this->observer->notify_attempt_added($qa);
 | 
        
           |  |  | 172 |         return $qa->get_slot();
 | 
        
           |  |  | 173 |     }
 | 
        
           |  |  | 174 |   | 
        
           |  |  | 175 |     /**
 | 
        
           |  |  | 176 |      * Add another question to this usage, in the place of an existing slot.
 | 
        
           |  |  | 177 |      *
 | 
        
           |  |  | 178 |      * Depending on $keepoldquestionattempt, the question_attempt that was in
 | 
        
           |  |  | 179 |      * that slot is moved to the end at a new slot number, which is returned.
 | 
        
           |  |  | 180 |      * Otherwise the existing attempt is completely removed and replaced.
 | 
        
           |  |  | 181 |      *
 | 
        
           |  |  | 182 |      * The added question is not started until you call {@link start_question()}
 | 
        
           |  |  | 183 |      * on it.
 | 
        
           |  |  | 184 |      *
 | 
        
           |  |  | 185 |      * @param int $slot the slot-number of the question to replace.
 | 
        
           |  |  | 186 |      * @param question_definition $question the question to add.
 | 
        
           |  |  | 187 |      * @param number $maxmark the maximum this question will be marked out of in
 | 
        
           |  |  | 188 |      *      this attempt (optional). If not given, the max mark from the $qa we
 | 
        
           |  |  | 189 |      *      are replacing is used.
 | 
        
           |  |  | 190 |      * @param bool $keepoldquestionattempt if true (the default) we keep the existing
 | 
        
           |  |  | 191 |      *      question_attempt, moving it to a new slot
 | 
        
           |  |  | 192 |      * @return int the new slot number of the question that was displaced.
 | 
        
           |  |  | 193 |      */
 | 
        
           |  |  | 194 |     public function add_question_in_place_of_other(
 | 
        
           |  |  | 195 |         $slot,
 | 
        
           |  |  | 196 |         question_definition $question,
 | 
        
           |  |  | 197 |         $maxmark = null,
 | 
        
           |  |  | 198 |         bool $keepoldquestionattempt = true,
 | 
        
           |  |  | 199 |     ) {
 | 
        
           |  |  | 200 |   | 
        
           |  |  | 201 |         $oldqa = $this->get_question_attempt($slot);
 | 
        
           |  |  | 202 |   | 
        
           |  |  | 203 |         if ($maxmark === null) {
 | 
        
           |  |  | 204 |             $maxmark = $oldqa->get_max_mark();
 | 
        
           |  |  | 205 |         }
 | 
        
           |  |  | 206 |   | 
        
           |  |  | 207 |         $qa = new question_attempt($question, $this->get_id(), $this->observer, $maxmark);
 | 
        
           |  |  | 208 |         $qa->set_slot($slot);
 | 
        
           |  |  | 209 |   | 
        
           |  |  | 210 |         if ($keepoldquestionattempt) {
 | 
        
           |  |  | 211 |             $newslot = $this->next_slot_number();
 | 
        
           |  |  | 212 |             $oldqa->set_slot($newslot);
 | 
        
           |  |  | 213 |             $this->questionattempts[$newslot] = $oldqa;
 | 
        
           |  |  | 214 |   | 
        
           |  |  | 215 |             $this->observer->notify_attempt_moved($oldqa, $slot);
 | 
        
           |  |  | 216 |             $this->observer->notify_attempt_added($qa);
 | 
        
           |  |  | 217 |   | 
        
           |  |  | 218 |         } else {
 | 
        
           |  |  | 219 |             $newslot = $slot;
 | 
        
           |  |  | 220 |             $qa->set_database_id($oldqa->get_database_id());
 | 
        
           |  |  | 221 |   | 
        
           |  |  | 222 |             foreach ($oldqa->get_step_iterator() as $oldstep) {
 | 
        
           |  |  | 223 |                 $this->observer->notify_step_deleted($oldstep, $oldqa);
 | 
        
           |  |  | 224 |             }
 | 
        
           |  |  | 225 |             $this->observer->notify_attempt_modified($qa);
 | 
        
           |  |  | 226 |         }
 | 
        
           |  |  | 227 |   | 
        
           |  |  | 228 |         $this->questionattempts[$slot] = $qa;
 | 
        
           |  |  | 229 |   | 
        
           |  |  | 230 |         return $newslot;
 | 
        
           |  |  | 231 |     }
 | 
        
           |  |  | 232 |   | 
        
           |  |  | 233 |     /**
 | 
        
           |  |  | 234 |      * The slot number that will be allotted to the next question added.
 | 
        
           |  |  | 235 |      */
 | 
        
           |  |  | 236 |     public function next_slot_number() {
 | 
        
           |  |  | 237 |         return count($this->questionattempts) + 1;
 | 
        
           |  |  | 238 |     }
 | 
        
           |  |  | 239 |   | 
        
           |  |  | 240 |     /**
 | 
        
           |  |  | 241 |      * Get the question_definition for a question in this attempt.
 | 
        
           |  |  | 242 |      * @param int $slot the number used to identify this question within this usage.
 | 
        
           |  |  | 243 |      * @param bool $requirequestioninitialised set this to false if you don't need
 | 
        
           |  |  | 244 |      *      the behaviour initialised, which may improve performance.
 | 
        
           |  |  | 245 |      * @return question_definition the requested question object.
 | 
        
           |  |  | 246 |      */
 | 
        
           |  |  | 247 |     public function get_question($slot, $requirequestioninitialised = true) {
 | 
        
           |  |  | 248 |         return $this->get_question_attempt($slot)->get_question($requirequestioninitialised);
 | 
        
           |  |  | 249 |     }
 | 
        
           |  |  | 250 |   | 
        
           |  |  | 251 |     /** @return array all the identifying numbers of all the questions in this usage. */
 | 
        
           |  |  | 252 |     public function get_slots() {
 | 
        
           |  |  | 253 |         return array_keys($this->questionattempts);
 | 
        
           |  |  | 254 |     }
 | 
        
           |  |  | 255 |   | 
        
           |  |  | 256 |     /** @return int the identifying number of the first question that was added to this usage. */
 | 
        
           |  |  | 257 |     public function get_first_question_number() {
 | 
        
           |  |  | 258 |         reset($this->questionattempts);
 | 
        
           |  |  | 259 |         return key($this->questionattempts);
 | 
        
           |  |  | 260 |     }
 | 
        
           |  |  | 261 |   | 
        
           |  |  | 262 |     /** @return int the number of questions that are currently in this usage. */
 | 
        
           |  |  | 263 |     public function question_count() {
 | 
        
           |  |  | 264 |         return count($this->questionattempts);
 | 
        
           |  |  | 265 |     }
 | 
        
           |  |  | 266 |   | 
        
           |  |  | 267 |     /**
 | 
        
           |  |  | 268 |      * Note the part of the {@link question_usage_by_activity} comment that explains
 | 
        
           |  |  | 269 |      * that {@link question_attempt} objects should be considered part of the inner
 | 
        
           |  |  | 270 |      * workings of the question engine, and should not, if possible, be accessed directly.
 | 
        
           |  |  | 271 |      *
 | 
        
           |  |  | 272 |      * @return question_attempt_iterator for iterating over all the questions being
 | 
        
           |  |  | 273 |      * attempted. as part of this usage.
 | 
        
           |  |  | 274 |      */
 | 
        
           |  |  | 275 |     public function get_attempt_iterator() {
 | 
        
           |  |  | 276 |         return new question_attempt_iterator($this);
 | 
        
           |  |  | 277 |     }
 | 
        
           |  |  | 278 |   | 
        
           |  |  | 279 |     /**
 | 
        
           |  |  | 280 |      * Check whether $number actually corresponds to a question attempt that is
 | 
        
           |  |  | 281 |      * part of this usage. Throws an exception if not.
 | 
        
           |  |  | 282 |      *
 | 
        
           |  |  | 283 |      * @param int $slot a number allegedly identifying a question within this usage.
 | 
        
           |  |  | 284 |      */
 | 
        
           |  |  | 285 |     protected function check_slot($slot) {
 | 
        
           |  |  | 286 |         if (!array_key_exists($slot, $this->questionattempts)) {
 | 
        
           |  |  | 287 |             throw new coding_exception('There is no question_attempt number ' . $slot .
 | 
        
           |  |  | 288 |                     ' in this attempt.');
 | 
        
           |  |  | 289 |         }
 | 
        
           |  |  | 290 |     }
 | 
        
           |  |  | 291 |   | 
        
           |  |  | 292 |     /**
 | 
        
           |  |  | 293 |      * Note the part of the {@link question_usage_by_activity} comment that explains
 | 
        
           |  |  | 294 |      * that {@link question_attempt} objects should be considered part of the inner
 | 
        
           |  |  | 295 |      * workings of the question engine, and should not, if possible, be accessed directly.
 | 
        
           |  |  | 296 |      *
 | 
        
           |  |  | 297 |      * @param int $slot the number used to identify this question within this usage.
 | 
        
           |  |  | 298 |      * @return question_attempt the corresponding {@link question_attempt} object.
 | 
        
           |  |  | 299 |      */
 | 
        
           |  |  | 300 |     public function get_question_attempt($slot) {
 | 
        
           |  |  | 301 |         $this->check_slot($slot);
 | 
        
           |  |  | 302 |         return $this->questionattempts[$slot];
 | 
        
           |  |  | 303 |     }
 | 
        
           |  |  | 304 |   | 
        
           |  |  | 305 |     /**
 | 
        
           |  |  | 306 |      * Get the current state of the attempt at a question.
 | 
        
           |  |  | 307 |      * @param int $slot the number used to identify this question within this usage.
 | 
        
           |  |  | 308 |      * @return question_state.
 | 
        
           |  |  | 309 |      */
 | 
        
           |  |  | 310 |     public function get_question_state($slot) {
 | 
        
           |  |  | 311 |         return $this->get_question_attempt($slot)->get_state();
 | 
        
           |  |  | 312 |     }
 | 
        
           |  |  | 313 |   | 
        
           |  |  | 314 |     /**
 | 
        
           |  |  | 315 |      * @param int $slot the number used to identify this question within this usage.
 | 
        
           |  |  | 316 |      * @param bool $showcorrectness Whether right/partial/wrong states should
 | 
        
           |  |  | 317 |      * be distinguised.
 | 
        
           |  |  | 318 |      * @return string A brief textual description of the current state.
 | 
        
           |  |  | 319 |      */
 | 
        
           |  |  | 320 |     public function get_question_state_string($slot, $showcorrectness) {
 | 
        
           |  |  | 321 |         return $this->get_question_attempt($slot)->get_state_string($showcorrectness);
 | 
        
           |  |  | 322 |     }
 | 
        
           |  |  | 323 |   | 
        
           |  |  | 324 |     /**
 | 
        
           |  |  | 325 |      * @param int $slot the number used to identify this question within this usage.
 | 
        
           |  |  | 326 |      * @param bool $showcorrectness Whether right/partial/wrong states should
 | 
        
           |  |  | 327 |      * be distinguised.
 | 
        
           |  |  | 328 |      * @return string a CSS class name for the current state.
 | 
        
           |  |  | 329 |      */
 | 
        
           |  |  | 330 |     public function get_question_state_class($slot, $showcorrectness) {
 | 
        
           |  |  | 331 |         return $this->get_question_attempt($slot)->get_state_class($showcorrectness);
 | 
        
           |  |  | 332 |     }
 | 
        
           |  |  | 333 |   | 
        
           |  |  | 334 |     /**
 | 
        
           |  |  | 335 |      * Whether this attempt at a given question could be completed just by the
 | 
        
           |  |  | 336 |      * student interacting with the question, before {@link finish_question()} is called.
 | 
        
           |  |  | 337 |      *
 | 
        
           |  |  | 338 |      * @param int $slot the number used to identify this question within this usage.
 | 
        
           |  |  | 339 |      * @return boolean whether the attempt at the given question can finish naturally.
 | 
        
           |  |  | 340 |      */
 | 
        
           |  |  | 341 |     public function can_question_finish_during_attempt($slot) {
 | 
        
           |  |  | 342 |         return $this->get_question_attempt($slot)->can_finish_during_attempt();
 | 
        
           |  |  | 343 |     }
 | 
        
           |  |  | 344 |   | 
        
           |  |  | 345 |     /**
 | 
        
           |  |  | 346 |      * Get the time of the most recent action performed on a question.
 | 
        
           |  |  | 347 |      * @param int $slot the number used to identify this question within this usage.
 | 
        
           |  |  | 348 |      * @return int timestamp.
 | 
        
           |  |  | 349 |      */
 | 
        
           |  |  | 350 |     public function get_question_action_time($slot) {
 | 
        
           |  |  | 351 |         return $this->get_question_attempt($slot)->get_last_action_time();
 | 
        
           |  |  | 352 |     }
 | 
        
           |  |  | 353 |   | 
        
           |  |  | 354 |     /**
 | 
        
           |  |  | 355 |      * Get the current fraction awarded for the attempt at a question.
 | 
        
           |  |  | 356 |      * @param int $slot the number used to identify this question within this usage.
 | 
        
           |  |  | 357 |      * @return number|null The current fraction for this question, or null if one has
 | 
        
           |  |  | 358 |      * not been assigned yet.
 | 
        
           |  |  | 359 |      */
 | 
        
           |  |  | 360 |     public function get_question_fraction($slot) {
 | 
        
           |  |  | 361 |         return $this->get_question_attempt($slot)->get_fraction();
 | 
        
           |  |  | 362 |     }
 | 
        
           |  |  | 363 |   | 
        
           |  |  | 364 |     /**
 | 
        
           |  |  | 365 |      * Get the current mark awarded for the attempt at a question.
 | 
        
           |  |  | 366 |      * @param int $slot the number used to identify this question within this usage.
 | 
        
           |  |  | 367 |      * @return number|null The current mark for this question, or null if one has
 | 
        
           |  |  | 368 |      * not been assigned yet.
 | 
        
           |  |  | 369 |      */
 | 
        
           |  |  | 370 |     public function get_question_mark($slot) {
 | 
        
           |  |  | 371 |         return $this->get_question_attempt($slot)->get_mark();
 | 
        
           |  |  | 372 |     }
 | 
        
           |  |  | 373 |   | 
        
           |  |  | 374 |     /**
 | 
        
           |  |  | 375 |      * Get the maximum mark possible for the attempt at a question.
 | 
        
           |  |  | 376 |      * @param int $slot the number used to identify this question within this usage.
 | 
        
           |  |  | 377 |      * @return number the available marks for this question.
 | 
        
           |  |  | 378 |      */
 | 
        
           |  |  | 379 |     public function get_question_max_mark($slot) {
 | 
        
           |  |  | 380 |         return $this->get_question_attempt($slot)->get_max_mark();
 | 
        
           |  |  | 381 |     }
 | 
        
           |  |  | 382 |   | 
        
           |  |  | 383 |     /**
 | 
        
           |  |  | 384 |      * Get the total mark for all questions in this usage.
 | 
        
           |  |  | 385 |      * @return number The sum of marks of all the question_attempts in this usage.
 | 
        
           |  |  | 386 |      */
 | 
        
           |  |  | 387 |     public function get_total_mark() {
 | 
        
           |  |  | 388 |         $mark = 0;
 | 
        
           |  |  | 389 |         foreach ($this->questionattempts as $qa) {
 | 
        
           |  |  | 390 |             if ($qa->get_max_mark() > 0 && $qa->get_state() == question_state::$needsgrading) {
 | 
        
           |  |  | 391 |                 return null;
 | 
        
           |  |  | 392 |             }
 | 
        
           |  |  | 393 |             $mark += $qa->get_mark();
 | 
        
           |  |  | 394 |         }
 | 
        
           |  |  | 395 |         return $mark;
 | 
        
           |  |  | 396 |     }
 | 
        
           |  |  | 397 |   | 
        
           |  |  | 398 |     /**
 | 
        
           |  |  | 399 |      * Get summary information about this usage.
 | 
        
           |  |  | 400 |      *
 | 
        
           |  |  | 401 |      * Some behaviours may be able to provide interesting summary information
 | 
        
           |  |  | 402 |      * about the attempt as a whole, and this method provides access to that data.
 | 
        
           |  |  | 403 |      * To see how this works, try setting a quiz to one of the CBM behaviours,
 | 
        
           |  |  | 404 |      * and then look at the extra information displayed at the top of the quiz
 | 
        
           |  |  | 405 |      * review page once you have sumitted an attempt.
 | 
        
           |  |  | 406 |      *
 | 
        
           |  |  | 407 |      * In the return value, the array keys are identifiers of the form
 | 
        
           |  |  | 408 |      * qbehaviour_behaviourname_meaningfullkey. For qbehaviour_deferredcbm_highsummary.
 | 
        
           |  |  | 409 |      * The values are arrays with two items, title and content. Each of these
 | 
        
           |  |  | 410 |      * will be either a string, or a renderable.
 | 
        
           |  |  | 411 |      *
 | 
        
           |  |  | 412 |      * @param question_display_options $options display options to apply.
 | 
        
           |  |  | 413 |      * @return array as described above.
 | 
        
           |  |  | 414 |      */
 | 
        
           |  |  | 415 |     public function get_summary_information(question_display_options $options) {
 | 
        
           |  |  | 416 |         return question_engine::get_behaviour_type($this->preferredbehaviour)
 | 
        
           |  |  | 417 |                 ->summarise_usage($this, $options);
 | 
        
           |  |  | 418 |     }
 | 
        
           |  |  | 419 |   | 
        
           |  |  | 420 |     /**
 | 
        
           |  |  | 421 |      * Get a simple textual summary of the question that was asked.
 | 
        
           |  |  | 422 |      *
 | 
        
           |  |  | 423 |      * @param int $slot the slot number of the question to summarise.
 | 
        
           |  |  | 424 |      * @return string the question summary.
 | 
        
           |  |  | 425 |      */
 | 
        
           |  |  | 426 |     public function get_question_summary($slot) {
 | 
        
           |  |  | 427 |         return $this->get_question_attempt($slot)->get_question_summary();
 | 
        
           |  |  | 428 |     }
 | 
        
           |  |  | 429 |   | 
        
           |  |  | 430 |     /**
 | 
        
           |  |  | 431 |      * Get a simple textual summary of response given.
 | 
        
           |  |  | 432 |      *
 | 
        
           |  |  | 433 |      * @param int $slot the slot number of the question to get the response summary for.
 | 
        
           |  |  | 434 |      * @return string the response summary.
 | 
        
           |  |  | 435 |      */
 | 
        
           |  |  | 436 |     public function get_response_summary($slot) {
 | 
        
           |  |  | 437 |         return $this->get_question_attempt($slot)->get_response_summary();
 | 
        
           |  |  | 438 |     }
 | 
        
           |  |  | 439 |   | 
        
           |  |  | 440 |     /**
 | 
        
           |  |  | 441 |      * Get a simple textual summary of the correct response to a question.
 | 
        
           |  |  | 442 |      *
 | 
        
           |  |  | 443 |      * @param int $slot the slot number of the question to get the right answer summary for.
 | 
        
           |  |  | 444 |      * @return string the right answer summary.
 | 
        
           |  |  | 445 |      */
 | 
        
           |  |  | 446 |     public function get_right_answer_summary($slot) {
 | 
        
           |  |  | 447 |         return $this->get_question_attempt($slot)->get_right_answer_summary();
 | 
        
           |  |  | 448 |     }
 | 
        
           |  |  | 449 |   | 
        
           |  |  | 450 |     /**
 | 
        
           |  |  | 451 |      * Return one of the bits of metadata for a particular question attempt in
 | 
        
           |  |  | 452 |      * this usage.
 | 
        
           |  |  | 453 |      * @param int $slot the slot number of the question of inereest.
 | 
        
           |  |  | 454 |      * @param string $name the name of the metadata variable to return.
 | 
        
           |  |  | 455 |      * @return string the value of that metadata variable.
 | 
        
           |  |  | 456 |      */
 | 
        
           |  |  | 457 |     public function get_question_attempt_metadata($slot, $name) {
 | 
        
           |  |  | 458 |         return $this->get_question_attempt($slot)->get_metadata($name);
 | 
        
           |  |  | 459 |     }
 | 
        
           |  |  | 460 |   | 
        
           |  |  | 461 |     /**
 | 
        
           |  |  | 462 |      * Set some metadata for a particular question attempt in this usage.
 | 
        
           |  |  | 463 |      * @param int $slot the slot number of the question of inerest.
 | 
        
           |  |  | 464 |      * @param string $name the name of the metadata variable to return.
 | 
        
           |  |  | 465 |      * @param string $value the value to set that metadata variable to.
 | 
        
           |  |  | 466 |      */
 | 
        
           |  |  | 467 |     public function set_question_attempt_metadata($slot, $name, $value) {
 | 
        
           |  |  | 468 |         $this->get_question_attempt($slot)->set_metadata($name, $value);
 | 
        
           |  |  | 469 |     }
 | 
        
           |  |  | 470 |   | 
        
           |  |  | 471 |     /**
 | 
        
           |  |  | 472 |      * Get the {@link core_question_renderer}, in collaboration with appropriate
 | 
        
           |  |  | 473 |      * {@link qbehaviour_renderer} and {@link qtype_renderer} subclasses, to generate the
 | 
        
           |  |  | 474 |      * HTML to display this question.
 | 
        
           |  |  | 475 |      * @param int $slot the number used to identify this question within this usage.
 | 
        
           |  |  | 476 |      * @param question_display_options $options controls how the question is rendered.
 | 
        
           |  |  | 477 |      * @param string|null $number The question number to display. 'i' is a special
 | 
        
           |  |  | 478 |      *      value that gets displayed as Information. Null means no number is displayed.
 | 
        
           |  |  | 479 |      * @return string HTML fragment representing the question.
 | 
        
           |  |  | 480 |      */
 | 
        
           |  |  | 481 |     public function render_question($slot, $options, $number = null) {
 | 
        
           |  |  | 482 |         $options->context = $this->context;
 | 
        
           |  |  | 483 |         return $this->get_question_attempt($slot)->render($options, $number);
 | 
        
           |  |  | 484 |     }
 | 
        
           |  |  | 485 |   | 
        
           |  |  | 486 |     /**
 | 
        
           |  |  | 487 |      * Generate any bits of HTML that needs to go in the <head> tag when this question
 | 
        
           |  |  | 488 |      * is displayed in the body.
 | 
        
           |  |  | 489 |      * @param int $slot the number used to identify this question within this usage.
 | 
        
           |  |  | 490 |      * @return string HTML fragment.
 | 
        
           |  |  | 491 |      */
 | 
        
           |  |  | 492 |     public function render_question_head_html($slot) {
 | 
        
           |  |  | 493 |         //$options->context = $this->context;
 | 
        
           |  |  | 494 |         return $this->get_question_attempt($slot)->render_head_html();
 | 
        
           |  |  | 495 |     }
 | 
        
           |  |  | 496 |   | 
        
           |  |  | 497 |     /**
 | 
        
           |  |  | 498 |      * Like {@link render_question()} but displays the question at the past step
 | 
        
           |  |  | 499 |      * indicated by $seq, rather than showing the latest step.
 | 
        
           |  |  | 500 |      *
 | 
        
           |  |  | 501 |      * @param int $slot the number used to identify this question within this usage.
 | 
        
           |  |  | 502 |      * @param int $seq the seq number of the past state to display.
 | 
        
           |  |  | 503 |      * @param question_display_options $options controls how the question is rendered.
 | 
        
           |  |  | 504 |      * @param string|null $number The question number to display. 'i' is a special
 | 
        
           |  |  | 505 |      *      value that gets displayed as Information. Null means no number is displayed.
 | 
        
           |  |  | 506 |      * @return string HTML fragment representing the question.
 | 
        
           |  |  | 507 |      */
 | 
        
           |  |  | 508 |     public function render_question_at_step($slot, $seq, $options, $number = null) {
 | 
        
           |  |  | 509 |         $options->context = $this->context;
 | 
        
           |  |  | 510 |         return $this->get_question_attempt($slot)->render_at_step(
 | 
        
           |  |  | 511 |                 $seq, $options, $number, $this->preferredbehaviour);
 | 
        
           |  |  | 512 |     }
 | 
        
           |  |  | 513 |   | 
        
           |  |  | 514 |     /**
 | 
        
           |  |  | 515 |      * Checks whether the users is allow to be served a particular file.
 | 
        
           |  |  | 516 |      * @param int $slot the number used to identify this question within this usage.
 | 
        
           |  |  | 517 |      * @param question_display_options $options the options that control display of the question.
 | 
        
           |  |  | 518 |      * @param string $component the name of the component we are serving files for.
 | 
        
           |  |  | 519 |      * @param string $filearea the name of the file area.
 | 
        
           |  |  | 520 |      * @param array $args the remaining bits of the file path.
 | 
        
           |  |  | 521 |      * @param bool $forcedownload whether the user must be forced to download the file.
 | 
        
           |  |  | 522 |      * @return bool true if the user can access this file.
 | 
        
           |  |  | 523 |      */
 | 
        
           |  |  | 524 |     public function check_file_access($slot, $options, $component, $filearea,
 | 
        
           |  |  | 525 |             $args, $forcedownload) {
 | 
        
           |  |  | 526 |         return $this->get_question_attempt($slot)->check_file_access(
 | 
        
           |  |  | 527 |                 $options, $component, $filearea, $args, $forcedownload);
 | 
        
           |  |  | 528 |     }
 | 
        
           |  |  | 529 |   | 
        
           |  |  | 530 |     /**
 | 
        
           |  |  | 531 |      * Replace a particular question_attempt with a different one.
 | 
        
           |  |  | 532 |      *
 | 
        
           |  |  | 533 |      * For internal use only. Used when reloading the state of a question from the
 | 
        
           |  |  | 534 |      * database.
 | 
        
           |  |  | 535 |      *
 | 
        
           |  |  | 536 |      * @param int $slot the slot number of the question to replace.
 | 
        
           |  |  | 537 |      * @param question_attempt $qa the question attempt to put in that place.
 | 
        
           |  |  | 538 |      */
 | 
        
           |  |  | 539 |     public function replace_loaded_question_attempt_info($slot, $qa) {
 | 
        
           |  |  | 540 |         $this->check_slot($slot);
 | 
        
           |  |  | 541 |         $this->questionattempts[$slot] = $qa;
 | 
        
           |  |  | 542 |     }
 | 
        
           |  |  | 543 |   | 
        
           |  |  | 544 |     /**
 | 
        
           |  |  | 545 |      * You should probably not use this method in code outside the question engine.
 | 
        
           |  |  | 546 |      * The main reason for exposing it was for the benefit of unit tests.
 | 
        
           |  |  | 547 |      * @param int $slot the number used to identify this question within this usage.
 | 
        
           |  |  | 548 |      * @return string return the prefix that is pre-pended to field names in the HTML
 | 
        
           |  |  | 549 |      * that is output.
 | 
        
           |  |  | 550 |      */
 | 
        
           |  |  | 551 |     public function get_field_prefix($slot) {
 | 
        
           |  |  | 552 |         return $this->get_question_attempt($slot)->get_field_prefix();
 | 
        
           |  |  | 553 |     }
 | 
        
           |  |  | 554 |   | 
        
           |  |  | 555 |     /**
 | 
        
           |  |  | 556 |      * Get the number of variants available for the question in this slot.
 | 
        
           |  |  | 557 |      * @param int $slot the number used to identify this question within this usage.
 | 
        
           |  |  | 558 |      * @return int the number of variants available.
 | 
        
           |  |  | 559 |      */
 | 
        
           |  |  | 560 |     public function get_num_variants($slot) {
 | 
        
           |  |  | 561 |         return $this->get_question_attempt($slot)->get_question()->get_num_variants();
 | 
        
           |  |  | 562 |     }
 | 
        
           |  |  | 563 |   | 
        
           |  |  | 564 |     /**
 | 
        
           |  |  | 565 |      * Get the variant of the question being used in a given slot.
 | 
        
           |  |  | 566 |      * @param int $slot the number used to identify this question within this usage.
 | 
        
           |  |  | 567 |      * @return int the variant of this question that is being used.
 | 
        
           |  |  | 568 |      */
 | 
        
           |  |  | 569 |     public function get_variant($slot) {
 | 
        
           |  |  | 570 |         return $this->get_question_attempt($slot)->get_variant();
 | 
        
           |  |  | 571 |     }
 | 
        
           |  |  | 572 |   | 
        
           |  |  | 573 |     /**
 | 
        
           |  |  | 574 |      * Start the attempt at a question that has been added to this usage.
 | 
        
           |  |  | 575 |      * @param int $slot the number used to identify this question within this usage.
 | 
        
           |  |  | 576 |      * @param int $variant which variant of the question to use. Must be between
 | 
        
           |  |  | 577 |      *      1 and ->get_num_variants($slot) inclusive. If not give, a variant is
 | 
        
           |  |  | 578 |      *      chosen at random.
 | 
        
           |  |  | 579 |      * @param int|null $timenow optional, the timstamp to record for this action. Defaults to now.
 | 
        
           |  |  | 580 |      */
 | 
        
           |  |  | 581 |     public function start_question($slot, $variant = null, $timenow = null) {
 | 
        
           |  |  | 582 |         if (is_null($variant)) {
 | 
        
           |  |  | 583 |             $variant = rand(1, $this->get_num_variants($slot));
 | 
        
           |  |  | 584 |         }
 | 
        
           |  |  | 585 |   | 
        
           |  |  | 586 |         $qa = $this->get_question_attempt($slot);
 | 
        
           |  |  | 587 |         $qa->start($this->preferredbehaviour, $variant, array(), $timenow);
 | 
        
           |  |  | 588 |         $this->observer->notify_attempt_modified($qa);
 | 
        
           |  |  | 589 |     }
 | 
        
           |  |  | 590 |   | 
        
           |  |  | 591 |     /**
 | 
        
           |  |  | 592 |      * Start the attempt at all questions that has been added to this usage.
 | 
        
           |  |  | 593 |      * @param question_variant_selection_strategy how to pick which variant of each question to use.
 | 
        
           |  |  | 594 |      * @param int $timestamp optional, the timstamp to record for this action. Defaults to now.
 | 
        
           |  |  | 595 |      * @param int $userid optional, the user to attribute this action to. Defaults to the current user.
 | 
        
           |  |  | 596 |      */
 | 
        
           |  |  | 597 |     public function start_all_questions(question_variant_selection_strategy $variantstrategy = null,
 | 
        
           |  |  | 598 |             $timestamp = null, $userid = null) {
 | 
        
           |  |  | 599 |         if (is_null($variantstrategy)) {
 | 
        
           |  |  | 600 |             $variantstrategy = new question_variant_random_strategy();
 | 
        
           |  |  | 601 |         }
 | 
        
           |  |  | 602 |   | 
        
           |  |  | 603 |         foreach ($this->questionattempts as $qa) {
 | 
        
           |  |  | 604 |             $qa->start($this->preferredbehaviour, $qa->select_variant($variantstrategy), array(),
 | 
        
           |  |  | 605 |                     $timestamp, $userid);
 | 
        
           |  |  | 606 |             $this->observer->notify_attempt_modified($qa);
 | 
        
           |  |  | 607 |         }
 | 
        
           |  |  | 608 |     }
 | 
        
           |  |  | 609 |   | 
        
           |  |  | 610 |     /**
 | 
        
           |  |  | 611 |      * Start the attempt at a question, starting from the point where the previous
 | 
        
           |  |  | 612 |      * question_attempt $oldqa had reached. This is used by the quiz 'Each attempt
 | 
        
           |  |  | 613 |      * builds on last' mode.
 | 
        
           |  |  | 614 |      * @param int $slot the number used to identify this question within this usage.
 | 
        
           |  |  | 615 |      * @param question_attempt $oldqa a previous attempt at this quetsion that
 | 
        
           |  |  | 616 |      *      defines the starting point.
 | 
        
           |  |  | 617 |      */
 | 
        
           |  |  | 618 |     public function start_question_based_on($slot, question_attempt $oldqa) {
 | 
        
           |  |  | 619 |         $qa = $this->get_question_attempt($slot);
 | 
        
           |  |  | 620 |         $qa->start_based_on($oldqa);
 | 
        
           |  |  | 621 |         $this->observer->notify_attempt_modified($qa);
 | 
        
           |  |  | 622 |     }
 | 
        
           |  |  | 623 |   | 
        
           |  |  | 624 |     /**
 | 
        
           |  |  | 625 |      * Process all the question actions in the current request.
 | 
        
           |  |  | 626 |      *
 | 
        
           |  |  | 627 |      * If there is a parameter slots included in the post data, then only
 | 
        
           |  |  | 628 |      * those question numbers will be processed, otherwise all questions in this
 | 
        
           |  |  | 629 |      * useage will be.
 | 
        
           |  |  | 630 |      *
 | 
        
           |  |  | 631 |      * This function also does {@link update_question_flags()}.
 | 
        
           |  |  | 632 |      *
 | 
        
           |  |  | 633 |      * @param int $timestamp optional, use this timestamp as 'now'.
 | 
        
           |  |  | 634 |      * @param array $postdata optional, only intended for testing. Use this data
 | 
        
           |  |  | 635 |      * instead of the data from $_POST.
 | 
        
           |  |  | 636 |      */
 | 
        
           |  |  | 637 |     public function process_all_actions($timestamp = null, $postdata = null) {
 | 
        
           |  |  | 638 |         foreach ($this->get_slots_in_request($postdata) as $slot) {
 | 
        
           |  |  | 639 |             if (!$this->validate_sequence_number($slot, $postdata)) {
 | 
        
           |  |  | 640 |                 continue;
 | 
        
           |  |  | 641 |             }
 | 
        
           |  |  | 642 |             $submitteddata = $this->extract_responses($slot, $postdata);
 | 
        
           |  |  | 643 |             $this->process_action($slot, $submitteddata, $timestamp);
 | 
        
           |  |  | 644 |         }
 | 
        
           |  |  | 645 |         $this->update_question_flags($postdata);
 | 
        
           |  |  | 646 |     }
 | 
        
           |  |  | 647 |   | 
        
           |  |  | 648 |     /**
 | 
        
           |  |  | 649 |      * Process all the question autosave data in the current request.
 | 
        
           |  |  | 650 |      *
 | 
        
           |  |  | 651 |      * If there is a parameter slots included in the post data, then only
 | 
        
           |  |  | 652 |      * those question numbers will be processed, otherwise all questions in this
 | 
        
           |  |  | 653 |      * useage will be.
 | 
        
           |  |  | 654 |      *
 | 
        
           |  |  | 655 |      * This function also does {@link update_question_flags()}.
 | 
        
           |  |  | 656 |      *
 | 
        
           |  |  | 657 |      * @param int $timestamp optional, use this timestamp as 'now'.
 | 
        
           |  |  | 658 |      * @param array $postdata optional, only intended for testing. Use this data
 | 
        
           |  |  | 659 |      * instead of the data from $_POST.
 | 
        
           |  |  | 660 |      */
 | 
        
           |  |  | 661 |     public function process_all_autosaves($timestamp = null, $postdata = null) {
 | 
        
           |  |  | 662 |         foreach ($this->get_slots_in_request($postdata) as $slot) {
 | 
        
           |  |  | 663 |             if (!$this->is_autosave_required($slot, $postdata)) {
 | 
        
           |  |  | 664 |                 continue;
 | 
        
           |  |  | 665 |             }
 | 
        
           |  |  | 666 |             $submitteddata = $this->extract_responses($slot, $postdata);
 | 
        
           |  |  | 667 |             $this->process_autosave($slot, $submitteddata, $timestamp);
 | 
        
           |  |  | 668 |         }
 | 
        
           |  |  | 669 |         $this->update_question_flags($postdata);
 | 
        
           |  |  | 670 |     }
 | 
        
           |  |  | 671 |   | 
        
           |  |  | 672 |     /**
 | 
        
           |  |  | 673 |      * Get the list of slot numbers that should be processed as part of processing
 | 
        
           |  |  | 674 |      * the current request.
 | 
        
           |  |  | 675 |      * @param array $postdata optional, only intended for testing. Use this data
 | 
        
           |  |  | 676 |      * instead of the data from $_POST.
 | 
        
           |  |  | 677 |      * @return array of slot numbers.
 | 
        
           |  |  | 678 |      */
 | 
        
           |  |  | 679 |     protected function get_slots_in_request($postdata = null) {
 | 
        
           |  |  | 680 |         // Note: we must not use "question_attempt::get_submitted_var()" because there is no attempt instance!!!
 | 
        
           |  |  | 681 |         if (is_null($postdata)) {
 | 
        
           |  |  | 682 |             $slots = optional_param('slots', null, PARAM_SEQUENCE);
 | 
        
           |  |  | 683 |         } else if (array_key_exists('slots', $postdata)) {
 | 
        
           |  |  | 684 |             $slots = clean_param($postdata['slots'], PARAM_SEQUENCE);
 | 
        
           |  |  | 685 |         } else {
 | 
        
           |  |  | 686 |             $slots = null;
 | 
        
           |  |  | 687 |         }
 | 
        
           |  |  | 688 |         if (is_null($slots)) {
 | 
        
           |  |  | 689 |             $slots = $this->get_slots();
 | 
        
           |  |  | 690 |         } else if (!$slots) {
 | 
        
           |  |  | 691 |             $slots = array();
 | 
        
           |  |  | 692 |         } else {
 | 
        
           |  |  | 693 |             $slots = explode(',', $slots);
 | 
        
           |  |  | 694 |         }
 | 
        
           |  |  | 695 |         return $slots;
 | 
        
           |  |  | 696 |     }
 | 
        
           |  |  | 697 |   | 
        
           |  |  | 698 |     /**
 | 
        
           |  |  | 699 |      * Get the submitted data from the current request that belongs to this
 | 
        
           |  |  | 700 |      * particular question.
 | 
        
           |  |  | 701 |      *
 | 
        
           |  |  | 702 |      * @param int $slot the number used to identify this question within this usage.
 | 
        
           |  |  | 703 |      * @param array|null $postdata optional, only intended for testing. Use this data
 | 
        
           |  |  | 704 |      * instead of the data from $_POST.
 | 
        
           |  |  | 705 |      * @return array submitted data specific to this question.
 | 
        
           |  |  | 706 |      */
 | 
        
           |  |  | 707 |     public function extract_responses($slot, $postdata = null) {
 | 
        
           |  |  | 708 |         return $this->get_question_attempt($slot)->get_submitted_data($postdata);
 | 
        
           |  |  | 709 |     }
 | 
        
           |  |  | 710 |   | 
        
           |  |  | 711 |     /**
 | 
        
           |  |  | 712 |      * Transform an array of response data for slots to an array of post data as you would get from quiz attempt form.
 | 
        
           |  |  | 713 |      *
 | 
        
           |  |  | 714 |      * @param $simulatedresponses array keys are slot nos => contains arrays representing student
 | 
        
           |  |  | 715 |      *                                   responses which will be passed to question_definition::prepare_simulated_post_data method
 | 
        
           |  |  | 716 |      *                                   and then have the appropriate prefix added.
 | 
        
           |  |  | 717 |      * @return array simulated post data
 | 
        
           |  |  | 718 |      */
 | 
        
           |  |  | 719 |     public function prepare_simulated_post_data($simulatedresponses) {
 | 
        
           |  |  | 720 |         $simulatedpostdata = array();
 | 
        
           |  |  | 721 |         $simulatedpostdata['slots'] = implode(',', array_keys($simulatedresponses));
 | 
        
           |  |  | 722 |         foreach ($simulatedresponses as $slot => $responsedata) {
 | 
        
           |  |  | 723 |             $slotresponse = array();
 | 
        
           |  |  | 724 |   | 
        
           |  |  | 725 |             // Behaviour vars should not be processed by question type, just add prefix.
 | 
        
           |  |  | 726 |             $behaviourvars = $this->get_question_attempt($slot)->get_behaviour()->get_expected_data();
 | 
        
           |  |  | 727 |             foreach (array_keys($responsedata) as $responsedatakey) {
 | 
        
           |  |  | 728 |                 if (is_string($responsedatakey) && $responsedatakey[0] === '-') {
 | 
        
           |  |  | 729 |                     $behaviourvarname = substr($responsedatakey, 1);
 | 
        
           |  |  | 730 |                     if (isset($behaviourvars[$behaviourvarname])) {
 | 
        
           |  |  | 731 |                         // Expected behaviour var found.
 | 
        
           |  |  | 732 |                         if ($responsedata[$responsedatakey]) {
 | 
        
           |  |  | 733 |                             // Only set the behaviour var if the column value from the cvs file is non zero.
 | 
        
           |  |  | 734 |                             // The behaviours only look at whether the var is set or not they don't look at the value.
 | 
        
           |  |  | 735 |                             $slotresponse[$responsedatakey] = $responsedata[$responsedatakey];
 | 
        
           |  |  | 736 |                         }
 | 
        
           |  |  | 737 |                     }
 | 
        
           |  |  | 738 |                     // Remove both expected and unexpected vars from data passed to question type.
 | 
        
           |  |  | 739 |                     unset($responsedata[$responsedatakey]);
 | 
        
           |  |  | 740 |                 }
 | 
        
           |  |  | 741 |             }
 | 
        
           |  |  | 742 |   | 
        
           |  |  | 743 |             $slotresponse += $this->get_question($slot)->prepare_simulated_post_data($responsedata);
 | 
        
           |  |  | 744 |             $slotresponse[':sequencecheck'] = $this->get_question_attempt($slot)->get_sequence_check_count();
 | 
        
           |  |  | 745 |   | 
        
           |  |  | 746 |             // Add this slot's prefix to slot data.
 | 
        
           |  |  | 747 |             $prefix = $this->get_field_prefix($slot);
 | 
        
           |  |  | 748 |             foreach ($slotresponse as $key => $value) {
 | 
        
           |  |  | 749 |                 $simulatedpostdata[$prefix.$key] = $value;
 | 
        
           |  |  | 750 |             }
 | 
        
           |  |  | 751 |         }
 | 
        
           |  |  | 752 |         return $simulatedpostdata;
 | 
        
           |  |  | 753 |     }
 | 
        
           |  |  | 754 |   | 
        
           |  |  | 755 |     /**
 | 
        
           |  |  | 756 |      * Process a specific action on a specific question.
 | 
        
           |  |  | 757 |      * @param int $slot the number used to identify this question within this usage.
 | 
        
           |  |  | 758 |      * @param array $submitteddata the submitted data that constitutes the action.
 | 
        
           |  |  | 759 |      * @param int|null $timestamp (optional) the timestamp to consider 'now'.
 | 
        
           |  |  | 760 |      */
 | 
        
           |  |  | 761 |     public function process_action($slot, $submitteddata, $timestamp = null) {
 | 
        
           |  |  | 762 |         $qa = $this->get_question_attempt($slot);
 | 
        
           |  |  | 763 |         $qa->process_action($submitteddata, $timestamp);
 | 
        
           |  |  | 764 |         $this->observer->notify_attempt_modified($qa);
 | 
        
           |  |  | 765 |     }
 | 
        
           |  |  | 766 |   | 
        
           |  |  | 767 |     /**
 | 
        
           |  |  | 768 |      * Process an autosave action on a specific question.
 | 
        
           |  |  | 769 |      * @param int $slot the number used to identify this question within this usage.
 | 
        
           |  |  | 770 |      * @param array $submitteddata the submitted data that constitutes the action.
 | 
        
           |  |  | 771 |      * @param int|null $timestamp (optional) the timestamp to consider 'now'.
 | 
        
           |  |  | 772 |      */
 | 
        
           |  |  | 773 |     public function process_autosave($slot, $submitteddata, $timestamp = null) {
 | 
        
           |  |  | 774 |         $qa = $this->get_question_attempt($slot);
 | 
        
           |  |  | 775 |         if ($qa->process_autosave($submitteddata, $timestamp)) {
 | 
        
           |  |  | 776 |             $this->observer->notify_attempt_modified($qa);
 | 
        
           |  |  | 777 |         }
 | 
        
           |  |  | 778 |     }
 | 
        
           |  |  | 779 |   | 
        
           |  |  | 780 |     /**
 | 
        
           |  |  | 781 |      * Check that the sequence number, that detects weird things like the student clicking back, is OK.
 | 
        
           |  |  | 782 |      *
 | 
        
           |  |  | 783 |      * If the sequence check variable is not present, returns
 | 
        
           |  |  | 784 |      * false. If the check variable is present and correct, returns true. If the
 | 
        
           |  |  | 785 |      * variable is present and wrong, throws an exception.
 | 
        
           |  |  | 786 |      *
 | 
        
           |  |  | 787 |      * @param int $slot the number used to identify this question within this usage.
 | 
        
           |  |  | 788 |      * @param array|null $postdata (optional) data to use in place of $_POST.
 | 
        
           |  |  | 789 |      * @return bool true if the check variable is present and correct. False if it
 | 
        
           |  |  | 790 |      * is missing. (Throws an exception if the check fails.)
 | 
        
           |  |  | 791 |      */
 | 
        
           |  |  | 792 |     public function validate_sequence_number($slot, $postdata = null) {
 | 
        
           |  |  | 793 |         $qa = $this->get_question_attempt($slot);
 | 
        
           |  |  | 794 |         $sequencecheck = $qa->get_submitted_var(
 | 
        
           |  |  | 795 |                 $qa->get_control_field_name('sequencecheck'), PARAM_INT, $postdata);
 | 
        
           |  |  | 796 |         if (is_null($sequencecheck)) {
 | 
        
           |  |  | 797 |             return false;
 | 
        
           |  |  | 798 |         } else if ($sequencecheck != $qa->get_sequence_check_count()) {
 | 
        
           |  |  | 799 |             throw new question_out_of_sequence_exception($this->id, $slot, $postdata);
 | 
        
           |  |  | 800 |         } else {
 | 
        
           |  |  | 801 |             return true;
 | 
        
           |  |  | 802 |         }
 | 
        
           |  |  | 803 |     }
 | 
        
           |  |  | 804 |   | 
        
           |  |  | 805 |     /**
 | 
        
           |  |  | 806 |      * Check, based on the sequence number, whether this auto-save is still required.
 | 
        
           |  |  | 807 |      *
 | 
        
           |  |  | 808 |      * @param int $slot the number used to identify this question within this usage.
 | 
        
           |  |  | 809 |      * @param array|null $postdata the submitted data that constitutes the action.
 | 
        
           |  |  | 810 |      * @return bool true if the check variable is present and correct, otherwise false.
 | 
        
           |  |  | 811 |      */
 | 
        
           |  |  | 812 |     public function is_autosave_required($slot, $postdata = null) {
 | 
        
           |  |  | 813 |         $qa = $this->get_question_attempt($slot);
 | 
        
           |  |  | 814 |         $sequencecheck = $qa->get_submitted_var(
 | 
        
           |  |  | 815 |                 $qa->get_control_field_name('sequencecheck'), PARAM_INT, $postdata);
 | 
        
           |  |  | 816 |         if (is_null($sequencecheck)) {
 | 
        
           |  |  | 817 |             return false;
 | 
        
           |  |  | 818 |         } else if ($sequencecheck != $qa->get_sequence_check_count()) {
 | 
        
           |  |  | 819 |             return false;
 | 
        
           |  |  | 820 |         } else {
 | 
        
           |  |  | 821 |             return true;
 | 
        
           |  |  | 822 |         }
 | 
        
           |  |  | 823 |     }
 | 
        
           |  |  | 824 |   | 
        
           |  |  | 825 |     /**
 | 
        
           |  |  | 826 |      * Update the flagged state for all question_attempts in this usage, if their
 | 
        
           |  |  | 827 |      * flagged state was changed in the request.
 | 
        
           |  |  | 828 |      *
 | 
        
           |  |  | 829 |      * @param array|null $postdata optional, only intended for testing. Use this data
 | 
        
           |  |  | 830 |      * instead of the data from $_POST.
 | 
        
           |  |  | 831 |      */
 | 
        
           |  |  | 832 |     public function update_question_flags($postdata = null) {
 | 
        
           |  |  | 833 |         foreach ($this->questionattempts as $qa) {
 | 
        
           |  |  | 834 |             $flagged = $qa->get_submitted_var(
 | 
        
           |  |  | 835 |                     $qa->get_flag_field_name(), PARAM_BOOL, $postdata);
 | 
        
           |  |  | 836 |             if (!is_null($flagged) && $flagged != $qa->is_flagged()) {
 | 
        
           |  |  | 837 |                 $qa->set_flagged($flagged);
 | 
        
           |  |  | 838 |             }
 | 
        
           |  |  | 839 |         }
 | 
        
           |  |  | 840 |     }
 | 
        
           |  |  | 841 |   | 
        
           |  |  | 842 |     /**
 | 
        
           |  |  | 843 |      * Get the correct response to a particular question. Passing the results of
 | 
        
           |  |  | 844 |      * this method to {@link process_action()} will probably result in full marks.
 | 
        
           |  |  | 845 |      * If it is not possible to compute a correct response, this method should return null.
 | 
        
           |  |  | 846 |      * @param int $slot the number used to identify this question within this usage.
 | 
        
           |  |  | 847 |      * @return array that constitutes a correct response to this question.
 | 
        
           |  |  | 848 |      */
 | 
        
           |  |  | 849 |     public function get_correct_response($slot) {
 | 
        
           |  |  | 850 |         return $this->get_question_attempt($slot)->get_correct_response();
 | 
        
           |  |  | 851 |     }
 | 
        
           |  |  | 852 |   | 
        
           |  |  | 853 |     /**
 | 
        
           |  |  | 854 |      * Finish the active phase of an attempt at a question.
 | 
        
           |  |  | 855 |      *
 | 
        
           |  |  | 856 |      * This is an external act of finishing the attempt. Think, for example, of
 | 
        
           |  |  | 857 |      * the 'Submit all and finish' button in the quiz. Some behaviours,
 | 
        
           |  |  | 858 |      * (for example, immediatefeedback) give a way of finishing the active phase
 | 
        
           |  |  | 859 |      * of a question attempt as part of a {@link process_action()} call.
 | 
        
           |  |  | 860 |      *
 | 
        
           |  |  | 861 |      * After the active phase is over, the only changes possible are things like
 | 
        
           |  |  | 862 |      * manual grading, or changing the flag state.
 | 
        
           |  |  | 863 |      *
 | 
        
           |  |  | 864 |      * @param int $slot the number used to identify this question within this usage.
 | 
        
           |  |  | 865 |      * @param int|null $timestamp (optional) the timestamp to consider 'now'.
 | 
        
           |  |  | 866 |      */
 | 
        
           |  |  | 867 |     public function finish_question($slot, $timestamp = null) {
 | 
        
           |  |  | 868 |         $qa = $this->get_question_attempt($slot);
 | 
        
           |  |  | 869 |         $qa->finish($timestamp);
 | 
        
           |  |  | 870 |         $this->observer->notify_attempt_modified($qa);
 | 
        
           |  |  | 871 |     }
 | 
        
           |  |  | 872 |   | 
        
           |  |  | 873 |     /**
 | 
        
           |  |  | 874 |      * Finish the active phase of an attempt at a question. See {@link finish_question()}
 | 
        
           |  |  | 875 |      * for a fuller description of what 'finish' means.
 | 
        
           |  |  | 876 |      *
 | 
        
           |  |  | 877 |      * @param int|null $timestamp (optional) the timestamp to consider 'now'.
 | 
        
           |  |  | 878 |      */
 | 
        
           |  |  | 879 |     public function finish_all_questions($timestamp = null) {
 | 
        
           |  |  | 880 |         foreach ($this->questionattempts as $qa) {
 | 
        
           |  |  | 881 |             $qa->finish($timestamp);
 | 
        
           |  |  | 882 |             $this->observer->notify_attempt_modified($qa);
 | 
        
           |  |  | 883 |         }
 | 
        
           |  |  | 884 |     }
 | 
        
           |  |  | 885 |   | 
        
           |  |  | 886 |     /**
 | 
        
           |  |  | 887 |      * Perform a manual grading action on a question attempt.
 | 
        
           |  |  | 888 |      * @param int $slot the number used to identify this question within this usage.
 | 
        
           |  |  | 889 |      * @param string $comment the comment being added to the question attempt.
 | 
        
           |  |  | 890 |      * @param number $mark the mark that is being assigned. Can be null to just
 | 
        
           |  |  | 891 |      * add a comment.
 | 
        
           |  |  | 892 |      * @param int $commentformat one of the FORMAT_... constants. The format of $comment.
 | 
        
           |  |  | 893 |      */
 | 
        
           |  |  | 894 |     public function manual_grade($slot, $comment, $mark, $commentformat = null) {
 | 
        
           |  |  | 895 |         $qa = $this->get_question_attempt($slot);
 | 
        
           |  |  | 896 |         $qa->manual_grade($comment, $mark, $commentformat);
 | 
        
           |  |  | 897 |         $this->observer->notify_attempt_modified($qa);
 | 
        
           |  |  | 898 |     }
 | 
        
           |  |  | 899 |   | 
        
           |  |  | 900 |     /**
 | 
        
           |  |  | 901 |      * Verify if the question_attempt in the given slot can be regraded with that other question version.
 | 
        
           |  |  | 902 |      *
 | 
        
           |  |  | 903 |      * @param int $slot the number used to identify this question within this usage.
 | 
        
           |  |  | 904 |      * @param question_definition $otherversion a different version of the question to use in the regrade.
 | 
        
           |  |  | 905 |      * @return string|null null if the regrade can proceed, else a reason why not.
 | 
        
           |  |  | 906 |      */
 | 
        
           |  |  | 907 |     public function validate_can_regrade_with_other_version(int $slot, question_definition $otherversion): ?string {
 | 
        
           |  |  | 908 |         return $this->get_question_attempt($slot)->validate_can_regrade_with_other_version($otherversion);
 | 
        
           |  |  | 909 |     }
 | 
        
           |  |  | 910 |   | 
        
           |  |  | 911 |     /**
 | 
        
           |  |  | 912 |      * Regrade a question in this usage. This replays the sequence of submitted
 | 
        
           |  |  | 913 |      * actions to recompute the outcomes.
 | 
        
           |  |  | 914 |      *
 | 
        
           |  |  | 915 |      * @param int $slot the number used to identify this question within this usage.
 | 
        
           |  |  | 916 |      * @param bool $finished whether the question attempt should be forced to be finished
 | 
        
           |  |  | 917 |      *      after the regrade, or whether it may still be in progress (default false).
 | 
        
           |  |  | 918 |      * @param number $newmaxmark (optional) if given, will change the max mark while regrading.
 | 
        
           |  |  | 919 |      * @param question_definition|null $otherversion a different version of the question to use
 | 
        
           |  |  | 920 |      *      in the regrade. (By default, the regrode will use exactly the same question version.)
 | 
        
           |  |  | 921 |      */
 | 
        
           |  |  | 922 |     public function regrade_question($slot, $finished = false, $newmaxmark = null,
 | 
        
           |  |  | 923 |             question_definition $otherversion = null) {
 | 
        
           |  |  | 924 |         $oldqa = $this->get_question_attempt($slot);
 | 
        
           |  |  | 925 |         if ($otherversion &&
 | 
        
           |  |  | 926 |                 $otherversion->questionbankentryid !== $oldqa->get_question(false)->questionbankentryid) {
 | 
        
           |  |  | 927 |             throw new coding_exception('You can only regrade using a different version of the same question, ' .
 | 
        
           |  |  | 928 |                     'not a completely different question.');
 | 
        
           |  |  | 929 |         }
 | 
        
           |  |  | 930 |         if (is_null($newmaxmark)) {
 | 
        
           |  |  | 931 |             $newmaxmark = $oldqa->get_max_mark();
 | 
        
           |  |  | 932 |         }
 | 
        
           |  |  | 933 |         $newqa = new question_attempt($otherversion ?? $oldqa->get_question(false),
 | 
        
           |  |  | 934 |                 $oldqa->get_usage_id(), $this->observer, $newmaxmark);
 | 
        
           |  |  | 935 |         $newqa->set_database_id($oldqa->get_database_id());
 | 
        
           |  |  | 936 |         $newqa->set_slot($oldqa->get_slot());
 | 
        
           |  |  | 937 |         $newqa->regrade($oldqa, $finished);
 | 
        
           |  |  | 938 |   | 
        
           |  |  | 939 |         $this->questionattempts[$slot] = $newqa;
 | 
        
           |  |  | 940 |         $this->observer->notify_attempt_modified($newqa);
 | 
        
           |  |  | 941 |     }
 | 
        
           |  |  | 942 |   | 
        
           |  |  | 943 |     /**
 | 
        
           |  |  | 944 |      * Regrade all the questions in this usage (without changing their max mark).
 | 
        
           |  |  | 945 |      * @param bool $finished whether each question should be forced to be finished
 | 
        
           |  |  | 946 |      *      after the regrade, or whether it may still be in progress (default false).
 | 
        
           |  |  | 947 |      */
 | 
        
           |  |  | 948 |     public function regrade_all_questions($finished = false) {
 | 
        
           |  |  | 949 |         foreach ($this->questionattempts as $slot => $notused) {
 | 
        
           |  |  | 950 |             $this->regrade_question($slot, $finished);
 | 
        
           |  |  | 951 |         }
 | 
        
           |  |  | 952 |     }
 | 
        
           |  |  | 953 |   | 
        
           |  |  | 954 |     /**
 | 
        
           |  |  | 955 |      * Change the max mark for this question_attempt.
 | 
        
           |  |  | 956 |      * @param int $slot the slot number of the question of inerest.
 | 
        
           |  |  | 957 |      * @param float $maxmark the new max mark.
 | 
        
           |  |  | 958 |      */
 | 
        
           |  |  | 959 |     public function set_max_mark($slot, $maxmark) {
 | 
        
           |  |  | 960 |         $this->get_question_attempt($slot)->set_max_mark($maxmark);
 | 
        
           |  |  | 961 |     }
 | 
        
           |  |  | 962 |   | 
        
           |  |  | 963 |     /**
 | 
        
           |  |  | 964 |      * Create a question_usage_by_activity from records loaded from the database.
 | 
        
           |  |  | 965 |      *
 | 
        
           |  |  | 966 |      * For internal use only.
 | 
        
           |  |  | 967 |      *
 | 
        
           |  |  | 968 |      * @param Iterator $records Raw records loaded from the database.
 | 
        
           |  |  | 969 |      * @param int $qubaid The id of the question usage we are loading.
 | 
        
           |  |  | 970 |      * @return question_usage_by_activity The newly constructed usage.
 | 
        
           |  |  | 971 |      */
 | 
        
           |  |  | 972 |     public static function load_from_records($records, $qubaid) {
 | 
        
           |  |  | 973 |         $record = $records->current();
 | 
        
           |  |  | 974 |         while ($record->qubaid != $qubaid) {
 | 
        
           |  |  | 975 |             $records->next();
 | 
        
           |  |  | 976 |             if (!$records->valid()) {
 | 
        
           |  |  | 977 |                 throw new coding_exception("Question usage {$qubaid} not found in the database.");
 | 
        
           |  |  | 978 |             }
 | 
        
           |  |  | 979 |             $record = $records->current();
 | 
        
           |  |  | 980 |         }
 | 
        
           |  |  | 981 |   | 
        
           |  |  | 982 |         $quba = new question_usage_by_activity($record->component,
 | 
        
           |  |  | 983 |             context::instance_by_id($record->contextid, IGNORE_MISSING));
 | 
        
           |  |  | 984 |         $quba->set_id_from_database($record->qubaid);
 | 
        
           |  |  | 985 |         $quba->set_preferred_behaviour($record->preferredbehaviour);
 | 
        
           |  |  | 986 |   | 
        
           |  |  | 987 |         $quba->observer = new question_engine_unit_of_work($quba);
 | 
        
           |  |  | 988 |   | 
        
           |  |  | 989 |         // If slot is null then the current pointer in $records will not be
 | 
        
           |  |  | 990 |         // advanced in the while loop below, and we get stuck in an infinite loop,
 | 
        
           |  |  | 991 |         // since this method is supposed to always consume at least one record.
 | 
        
           |  |  | 992 |         // Therefore, in this case, advance the record here.
 | 
        
           |  |  | 993 |         if (is_null($record->slot)) {
 | 
        
           |  |  | 994 |             $records->next();
 | 
        
           |  |  | 995 |         }
 | 
        
           |  |  | 996 |   | 
        
           |  |  | 997 |         while ($record && $record->qubaid == $qubaid && !is_null($record->slot)) {
 | 
        
           |  |  | 998 |             $quba->questionattempts[$record->slot] =
 | 
        
           |  |  | 999 |                     question_attempt::load_from_records($records,
 | 
        
           |  |  | 1000 |                     $record->questionattemptid, $quba->observer,
 | 
        
           |  |  | 1001 |                     $quba->get_preferred_behaviour());
 | 
        
           |  |  | 1002 |             if ($records->valid()) {
 | 
        
           |  |  | 1003 |                 $record = $records->current();
 | 
        
           |  |  | 1004 |             } else {
 | 
        
           |  |  | 1005 |                 $record = false;
 | 
        
           |  |  | 1006 |             }
 | 
        
           |  |  | 1007 |         }
 | 
        
           |  |  | 1008 |   | 
        
           |  |  | 1009 |         return $quba;
 | 
        
           |  |  | 1010 |     }
 | 
        
           |  |  | 1011 |   | 
        
           |  |  | 1012 |     /**
 | 
        
           |  |  | 1013 |      * Preload users of all question attempt steps.
 | 
        
           |  |  | 1014 |      *
 | 
        
           |  |  | 1015 |      * @throws dml_exception
 | 
        
           |  |  | 1016 |      */
 | 
        
           |  |  | 1017 |     public function preload_all_step_users(): void {
 | 
        
           |  |  | 1018 |         global $DB;
 | 
        
           |  |  | 1019 |   | 
        
           |  |  | 1020 |         // Get all user ids.
 | 
        
           |  |  | 1021 |         $userids = [];
 | 
        
           |  |  | 1022 |         foreach ($this->questionattempts as $qa) {
 | 
        
           |  |  | 1023 |             foreach ($qa->get_full_step_iterator() as $step) {
 | 
        
           |  |  | 1024 |                 $userids[$step->get_user_id()] = 1;
 | 
        
           |  |  | 1025 |             }
 | 
        
           |  |  | 1026 |         }
 | 
        
           |  |  | 1027 |   | 
        
           |  |  | 1028 |         // Load user information.
 | 
        
           |  |  | 1029 |         $users = $DB->get_records_list('user', 'id', array_keys($userids), '', '*');
 | 
        
           |  |  | 1030 |         // Update user information for steps.
 | 
        
           |  |  | 1031 |         foreach ($this->questionattempts as $qa) {
 | 
        
           |  |  | 1032 |             foreach ($qa->get_full_step_iterator() as $step) {
 | 
        
           |  |  | 1033 |                 if (isset($users[$step->get_user_id()])) {
 | 
        
           |  |  | 1034 |                     $step->add_full_user_object($users[$step->get_user_id()]);
 | 
        
           |  |  | 1035 |                 }
 | 
        
           |  |  | 1036 |             }
 | 
        
           |  |  | 1037 |         }
 | 
        
           |  |  | 1038 |     }
 | 
        
           |  |  | 1039 | }
 | 
        
           |  |  | 1040 |   | 
        
           |  |  | 1041 |   | 
        
           |  |  | 1042 | /**
 | 
        
           |  |  | 1043 |  * A class abstracting access to the {@link question_usage_by_activity::$questionattempts} array.
 | 
        
           |  |  | 1044 |  *
 | 
        
           |  |  | 1045 |  * This class snapshots the list of {@link question_attempts} to iterate over
 | 
        
           |  |  | 1046 |  * when it is created. If a question is added to the usage mid-iteration, it
 | 
        
           |  |  | 1047 |  * will now show up.
 | 
        
           |  |  | 1048 |  *
 | 
        
           |  |  | 1049 |  * To create an instance of this class, use
 | 
        
           |  |  | 1050 |  * {@link question_usage_by_activity::get_attempt_iterator()}
 | 
        
           |  |  | 1051 |  *
 | 
        
           |  |  | 1052 |  * @copyright  2009 The Open University
 | 
        
           |  |  | 1053 |  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | 
        
           |  |  | 1054 |  */
 | 
        
           |  |  | 1055 | class question_attempt_iterator implements Iterator, ArrayAccess {
 | 
        
           |  |  | 1056 |   | 
        
           |  |  | 1057 |     /** @var question_usage_by_activity that we are iterating over. */
 | 
        
           |  |  | 1058 |     protected $quba;
 | 
        
           |  |  | 1059 |   | 
        
           |  |  | 1060 |     /** @var array of slot numbers. */
 | 
        
           |  |  | 1061 |     protected $slots;
 | 
        
           |  |  | 1062 |   | 
        
           |  |  | 1063 |     /**
 | 
        
           |  |  | 1064 |      * To create an instance of this class, use
 | 
        
           |  |  | 1065 |      * {@link question_usage_by_activity::get_attempt_iterator()}.
 | 
        
           |  |  | 1066 |      *
 | 
        
           |  |  | 1067 |      * @param question_usage_by_activity $quba the usage to iterate over.
 | 
        
           |  |  | 1068 |      */
 | 
        
           |  |  | 1069 |     public function __construct(question_usage_by_activity $quba) {
 | 
        
           |  |  | 1070 |         $this->quba = $quba;
 | 
        
           |  |  | 1071 |         $this->slots = $quba->get_slots();
 | 
        
           |  |  | 1072 |         $this->rewind();
 | 
        
           |  |  | 1073 |     }
 | 
        
           |  |  | 1074 |   | 
        
           |  |  | 1075 |     /**
 | 
        
           |  |  | 1076 |      * Standard part of the Iterator interface.
 | 
        
           |  |  | 1077 |      *
 | 
        
           |  |  | 1078 |      * @return question_attempt
 | 
        
           |  |  | 1079 |      */
 | 
        
           |  |  | 1080 |     #[\ReturnTypeWillChange]
 | 
        
           |  |  | 1081 |     public function current() {
 | 
        
           |  |  | 1082 |         return $this->offsetGet(current($this->slots));
 | 
        
           |  |  | 1083 |     }
 | 
        
           |  |  | 1084 |   | 
        
           |  |  | 1085 |     /**
 | 
        
           |  |  | 1086 |      * Standard part of the Iterator interface.
 | 
        
           |  |  | 1087 |      *
 | 
        
           |  |  | 1088 |      * @return int
 | 
        
           |  |  | 1089 |      */
 | 
        
           |  |  | 1090 |     #[\ReturnTypeWillChange]
 | 
        
           |  |  | 1091 |     public function key() {
 | 
        
           |  |  | 1092 |         return current($this->slots);
 | 
        
           |  |  | 1093 |     }
 | 
        
           |  |  | 1094 |   | 
        
           |  |  | 1095 |     /**
 | 
        
           |  |  | 1096 |      * Standard part of the Iterator interface.
 | 
        
           |  |  | 1097 |      */
 | 
        
           |  |  | 1098 |     public function next(): void {
 | 
        
           |  |  | 1099 |         next($this->slots);
 | 
        
           |  |  | 1100 |     }
 | 
        
           |  |  | 1101 |   | 
        
           |  |  | 1102 |     /**
 | 
        
           |  |  | 1103 |      * Standard part of the Iterator interface.
 | 
        
           |  |  | 1104 |      */
 | 
        
           |  |  | 1105 |     public function rewind(): void {
 | 
        
           |  |  | 1106 |         reset($this->slots);
 | 
        
           |  |  | 1107 |     }
 | 
        
           |  |  | 1108 |   | 
        
           |  |  | 1109 |     /**
 | 
        
           |  |  | 1110 |      * Standard part of the Iterator interface.
 | 
        
           |  |  | 1111 |      *
 | 
        
           |  |  | 1112 |      * @return bool
 | 
        
           |  |  | 1113 |      */
 | 
        
           |  |  | 1114 |     public function valid(): bool {
 | 
        
           |  |  | 1115 |         return current($this->slots) !== false;
 | 
        
           |  |  | 1116 |     }
 | 
        
           |  |  | 1117 |   | 
        
           |  |  | 1118 |     /**
 | 
        
           |  |  | 1119 |      * Standard part of the ArrayAccess interface.
 | 
        
           |  |  | 1120 |      *
 | 
        
           |  |  | 1121 |      * @param int $slot
 | 
        
           |  |  | 1122 |      * @return bool
 | 
        
           |  |  | 1123 |      */
 | 
        
           |  |  | 1124 |     public function offsetExists($slot): bool {
 | 
        
           |  |  | 1125 |         return in_array($slot, $this->slots);
 | 
        
           |  |  | 1126 |     }
 | 
        
           |  |  | 1127 |   | 
        
           |  |  | 1128 |     /**
 | 
        
           |  |  | 1129 |      * Standard part of the ArrayAccess interface.
 | 
        
           |  |  | 1130 |      *
 | 
        
           |  |  | 1131 |      * @param int $slot
 | 
        
           |  |  | 1132 |      * @return question_attempt
 | 
        
           |  |  | 1133 |      */
 | 
        
           |  |  | 1134 |     #[\ReturnTypeWillChange]
 | 
        
           |  |  | 1135 |     public function offsetGet($slot) {
 | 
        
           |  |  | 1136 |         return $this->quba->get_question_attempt($slot);
 | 
        
           |  |  | 1137 |     }
 | 
        
           |  |  | 1138 |   | 
        
           |  |  | 1139 |     /**
 | 
        
           |  |  | 1140 |      * Standard part of the ArrayAccess interface.
 | 
        
           |  |  | 1141 |      *
 | 
        
           |  |  | 1142 |      * @param int $slot
 | 
        
           |  |  | 1143 |      * @param question_attempt $value
 | 
        
           |  |  | 1144 |      */
 | 
        
           |  |  | 1145 |     public function offsetSet($slot, $value): void {
 | 
        
           |  |  | 1146 |         throw new coding_exception('You are only allowed read-only access to ' .
 | 
        
           |  |  | 1147 |                 'question_attempt::states through a question_attempt_step_iterator. Cannot set.');
 | 
        
           |  |  | 1148 |     }
 | 
        
           |  |  | 1149 |   | 
        
           |  |  | 1150 |     /**
 | 
        
           |  |  | 1151 |      * Standard part of the ArrayAccess interface.
 | 
        
           |  |  | 1152 |      *
 | 
        
           |  |  | 1153 |      * @param int $slot
 | 
        
           |  |  | 1154 |      */
 | 
        
           |  |  | 1155 |     public function offsetUnset($slot): void {
 | 
        
           |  |  | 1156 |         throw new coding_exception('You are only allowed read-only access to ' .
 | 
        
           |  |  | 1157 |                 'question_attempt::states through a question_attempt_step_iterator. Cannot unset.');
 | 
        
           |  |  | 1158 |     }
 | 
        
           |  |  | 1159 | }
 | 
        
           |  |  | 1160 |   | 
        
           |  |  | 1161 |   | 
        
           |  |  | 1162 | /**
 | 
        
           |  |  | 1163 |  * Interface for things that want to be notified of signficant changes to a
 | 
        
           |  |  | 1164 |  * {@link question_usage_by_activity}.
 | 
        
           |  |  | 1165 |  *
 | 
        
           |  |  | 1166 |  * A question behaviour controls the flow of actions a student can
 | 
        
           |  |  | 1167 |  * take as they work through a question, and later, as a teacher manually grades it.
 | 
        
           |  |  | 1168 |  *
 | 
        
           |  |  | 1169 |  * @copyright  2009 The Open University
 | 
        
           |  |  | 1170 |  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | 
        
           |  |  | 1171 |  */
 | 
        
           |  |  | 1172 | interface question_usage_observer {
 | 
        
           |  |  | 1173 |     /** Called when a field of the question_usage_by_activity is changed. */
 | 
        
           |  |  | 1174 |     public function notify_modified();
 | 
        
           |  |  | 1175 |   | 
        
           |  |  | 1176 |     /**
 | 
        
           |  |  | 1177 |      * Called when a new question attempt is added to this usage.
 | 
        
           |  |  | 1178 |      * @param question_attempt $qa the newly added question attempt.
 | 
        
           |  |  | 1179 |      */
 | 
        
           |  |  | 1180 |     public function notify_attempt_added(question_attempt $qa);
 | 
        
           |  |  | 1181 |   | 
        
           |  |  | 1182 |     /**
 | 
        
           |  |  | 1183 |      * Called when the fields of a question attempt in this usage are modified.
 | 
        
           |  |  | 1184 |      * @param question_attempt $qa the newly added question attempt.
 | 
        
           |  |  | 1185 |      */
 | 
        
           |  |  | 1186 |     public function notify_attempt_modified(question_attempt $qa);
 | 
        
           |  |  | 1187 |   | 
        
           |  |  | 1188 |     /**
 | 
        
           |  |  | 1189 |      * Called when a question_attempt has been moved to a new slot.
 | 
        
           |  |  | 1190 |      * @param question_attempt $qa The question attempt that was moved.
 | 
        
           |  |  | 1191 |      * @param int $oldslot The previous slot number of that attempt.
 | 
        
           |  |  | 1192 |      */
 | 
        
           |  |  | 1193 |     public function notify_attempt_moved(question_attempt $qa, $oldslot);
 | 
        
           |  |  | 1194 |   | 
        
           |  |  | 1195 |     /**
 | 
        
           |  |  | 1196 |      * Called when a new step is added to a question attempt in this usage.
 | 
        
           |  |  | 1197 |      * @param question_attempt_step $step the new step.
 | 
        
           |  |  | 1198 |      * @param question_attempt $qa the usage it is being added to.
 | 
        
           |  |  | 1199 |      * @param int $seq the sequence number of the new step.
 | 
        
           |  |  | 1200 |      */
 | 
        
           |  |  | 1201 |     public function notify_step_added(question_attempt_step $step, question_attempt $qa, $seq);
 | 
        
           |  |  | 1202 |   | 
        
           |  |  | 1203 |     /**
 | 
        
           |  |  | 1204 |      * Called when a new step is updated in a question attempt in this usage.
 | 
        
           |  |  | 1205 |      * @param question_attempt_step $step the step that was updated.
 | 
        
           |  |  | 1206 |      * @param question_attempt $qa the usage it is being added to.
 | 
        
           |  |  | 1207 |      * @param int $seq the sequence number of the new step.
 | 
        
           |  |  | 1208 |      */
 | 
        
           |  |  | 1209 |     public function notify_step_modified(question_attempt_step $step, question_attempt $qa, $seq);
 | 
        
           |  |  | 1210 |   | 
        
           |  |  | 1211 |     /**
 | 
        
           |  |  | 1212 |      * Called when a new step is updated in a question attempt in this usage.
 | 
        
           |  |  | 1213 |      * @param question_attempt_step $step the step to delete.
 | 
        
           |  |  | 1214 |      * @param question_attempt $qa the usage it is being added to.
 | 
        
           |  |  | 1215 |      */
 | 
        
           |  |  | 1216 |     public function notify_step_deleted(question_attempt_step $step, question_attempt $qa);
 | 
        
           |  |  | 1217 |   | 
        
           |  |  | 1218 |     /**
 | 
        
           |  |  | 1219 |      * Called when a new metadata variable is set on a question attempt in this usage.
 | 
        
           |  |  | 1220 |      * @param question_attempt $qa the question attempt the metadata is being added to.
 | 
        
           |  |  | 1221 |      * @param int $name the name of the metadata variable added.
 | 
        
           |  |  | 1222 |      */
 | 
        
           |  |  | 1223 |     public function notify_metadata_added(question_attempt $qa, $name);
 | 
        
           |  |  | 1224 |   | 
        
           |  |  | 1225 |     /**
 | 
        
           |  |  | 1226 |      * Called when a metadata variable on a question attempt in this usage is updated.
 | 
        
           |  |  | 1227 |      * @param question_attempt $qa the question attempt where the metadata is being modified.
 | 
        
           |  |  | 1228 |      * @param int $name the name of the metadata variable modified.
 | 
        
           |  |  | 1229 |      */
 | 
        
           |  |  | 1230 |     public function notify_metadata_modified(question_attempt $qa, $name);
 | 
        
           |  |  | 1231 | }
 | 
        
           |  |  | 1232 |   | 
        
           |  |  | 1233 |   | 
        
           |  |  | 1234 | /**
 | 
        
           |  |  | 1235 |  * Null implmentation of the {@link question_usage_watcher} interface.
 | 
        
           |  |  | 1236 |  * Does nothing.
 | 
        
           |  |  | 1237 |  *
 | 
        
           |  |  | 1238 |  * @copyright  2009 The Open University
 | 
        
           |  |  | 1239 |  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | 
        
           |  |  | 1240 |  */
 | 
        
           |  |  | 1241 | class question_usage_null_observer implements question_usage_observer {
 | 
        
           |  |  | 1242 |     public function notify_modified() {
 | 
        
           |  |  | 1243 |     }
 | 
        
           |  |  | 1244 |     public function notify_attempt_added(question_attempt $qa) {
 | 
        
           |  |  | 1245 |     }
 | 
        
           |  |  | 1246 |     public function notify_attempt_modified(question_attempt $qa) {
 | 
        
           |  |  | 1247 |     }
 | 
        
           |  |  | 1248 |     public function notify_attempt_moved(question_attempt $qa, $oldslot) {
 | 
        
           |  |  | 1249 |     }
 | 
        
           |  |  | 1250 |     public function notify_step_added(question_attempt_step $step, question_attempt $qa, $seq) {
 | 
        
           |  |  | 1251 |     }
 | 
        
           |  |  | 1252 |     public function notify_step_modified(question_attempt_step $step, question_attempt $qa, $seq) {
 | 
        
           |  |  | 1253 |     }
 | 
        
           |  |  | 1254 |     public function notify_step_deleted(question_attempt_step $step, question_attempt $qa) {
 | 
        
           |  |  | 1255 |     }
 | 
        
           |  |  | 1256 |     public function notify_metadata_added(question_attempt $qa, $name) {
 | 
        
           |  |  | 1257 |     }
 | 
        
           |  |  | 1258 |     public function notify_metadata_modified(question_attempt $qa, $name) {
 | 
        
           |  |  | 1259 |     }
 | 
        
           |  |  | 1260 | }
 |