[ Index ]

PHP Cross Reference of moodle-2.8

title

Body

[close]

/question/engine/ -> questionattempt.php (source)

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


Generated: Fri Nov 28 20:29:05 2014 Cross-referenced by PHPXref 0.7.1