[ Index ]

PHP Cross Reference of Unnamed Project

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


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1