Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403]

   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 contains classes for handling the different question behaviours
  19   * during upgrade.
  20   *
  21   * @package    moodlecore
  22   * @subpackage questionengine
  23   * @copyright  2010 The Open University
  24   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  25   */
  26  
  27  
  28  defined('MOODLE_INTERNAL') || die();
  29  
  30  
  31  /**
  32   * Base class for managing the upgrade of a question using a particular behaviour.
  33   *
  34   * This class takes as input:
  35   * 1. Various backgroud data like $quiz, $attempt and $question.
  36   * 2. The data about the question session to upgrade $qsession and $qstates.
  37   * Working through that data, it builds up
  38   * 3. The equivalent new data $qa. This has roughly the same data as a
  39   * question_attempt object belonging to the new question engine would have, but
  40   * $this->qa is built up from stdClass objects.
  41   *
  42   * @copyright  2010 The Open University
  43   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  44   */
  45  abstract class question_behaviour_attempt_updater {
  46      /** @var question_qtype_attempt_updater */
  47      protected $qtypeupdater;
  48      /** @var question_engine_assumption_logger */
  49      protected $logger;
  50      /** @var question_engine_attempt_upgrader */
  51      protected $qeupdater;
  52  
  53      /**
  54       * @var object this is the data for the upgraded questions attempt that
  55       * we are building.
  56       */
  57      public $qa;
  58  
  59      /** @var object the quiz settings. */
  60      protected $quiz;
  61      /** @var object the quiz attempt data. */
  62      protected $attempt;
  63      /** @var object the question definition data. */
  64      protected $question;
  65      /** @var object the question session to be upgraded. */
  66      protected $qsession;
  67      /** @var array the question states for the session to be upgraded. */
  68      protected $qstates;
  69      /** @var stdClass */
  70      protected $startstate;
  71  
  72      /**
  73       * @var int counts the question_steps as they are converted to
  74       * question_attempt_steps.
  75       */
  76      protected $sequencenumber;
  77      /** @var object pointer to the state that has already finished this attempt. */
  78      protected $finishstate;
  79  
  80      public function __construct($quiz, $attempt, $question, $qsession, $qstates, $logger, $qeupdater) {
  81          $this->quiz = $quiz;
  82          $this->attempt = $attempt;
  83          $this->question = $question;
  84          $this->qsession = $qsession;
  85          $this->qstates = $qstates;
  86          $this->logger = $logger;
  87          $this->qeupdater = $qeupdater;
  88      }
  89  
  90      public function discard() {
  91          // Help the garbage collector, which seems to be struggling.
  92          $this->quiz = null;
  93          $this->attempt = null;
  94          $this->question = null;
  95          $this->qsession = null;
  96          $this->qstates = null;
  97          $this->qa = null;
  98          $this->qtypeupdater->discard();
  99          $this->qtypeupdater = null;
 100          $this->logger = null;
 101          $this->qeupdater = null;
 102      }
 103  
 104      protected abstract function behaviour_name();
 105  
 106      public function get_converted_qa() {
 107          $this->initialise_qa();
 108          $this->convert_steps();
 109          return $this->qa;
 110      }
 111  
 112      protected function create_missing_first_step() {
 113          $step = new stdClass();
 114          $step->state = 'todo';
 115          $step->data = array();
 116          $step->fraction = null;
 117          $step->timecreated = $this->attempt->timestart ? $this->attempt->timestart : time();
 118          $step->userid = $this->attempt->userid;
 119          $this->qtypeupdater->supply_missing_first_step_data($step->data);
 120          return $step;
 121      }
 122  
 123      public function supply_missing_qa() {
 124          $this->initialise_qa();
 125          $this->qa->timemodified = $this->attempt->timestart;
 126          $this->sequencenumber = 0;
 127          $this->add_step($this->create_missing_first_step());
 128          return $this->qa;
 129      }
 130  
 131      protected function initialise_qa() {
 132          $this->qtypeupdater = $this->make_qtype_updater();
 133  
 134          $qa = new stdClass();
 135          $qa->questionid = $this->question->id;
 136          $qa->variant = 1;
 137          $qa->behaviour = $this->behaviour_name();
 138          $qa->questionsummary = $this->qtypeupdater->question_summary($this->question);
 139          $qa->rightanswer = $this->qtypeupdater->right_answer($this->question);
 140          $qa->maxmark = $this->question->maxmark;
 141          $qa->minfraction = 0;
 142          $qa->maxfraction = 1;
 143          $qa->flagged = 0;
 144          $qa->responsesummary = '';
 145          $qa->timemodified = 0;
 146          $qa->steps = array();
 147  
 148          $this->qa = $qa;
 149      }
 150  
 151      protected function convert_steps() {
 152          $this->finishstate = null;
 153          $this->startstate = null;
 154          $this->sequencenumber = 0;
 155          foreach ($this->qstates as $state) {
 156              $this->process_state($state);
 157          }
 158          $this->finish_up();
 159      }
 160  
 161      protected function process_state($state) {
 162          $step = $this->make_step($state);
 163          $method = 'process' . $state->event;
 164          $this->$method($step, $state);
 165      }
 166  
 167      protected function finish_up() {
 168      }
 169  
 170      protected function add_step($step) {
 171          $step->sequencenumber = $this->sequencenumber;
 172          $this->qa->steps[] = $step;
 173          $this->sequencenumber++;
 174      }
 175  
 176      protected function discard_last_state() {
 177          array_pop($this->qa->steps);
 178          $this->sequencenumber--;
 179      }
 180  
 181      protected function unexpected_event($state) {
 182          throw new coding_exception("Unexpected event {$state->event} in state {$state->id} in question session {$this->qsession->id}.");
 183      }
 184  
 185      protected function process0($step, $state) {
 186          if ($this->startstate) {
 187              if ($state->answer == reset($this->qstates)->answer) {
 188                  return;
 189              } else if ($this->quiz->attemptonlast && $this->sequencenumber == 1) {
 190                  // There was a bug in attemptonlast in the past, which meant that
 191                  // it created two inconsistent open states, with the second taking
 192                  // priority. Simulate that be discarding the first open state, then
 193                  // continuing.
 194                  $this->logger->log_assumption("Ignoring bogus state in attempt at question {$state->question}");
 195                  $this->sequencenumber = 0;
 196                  $this->qa->steps = array();
 197              } else if ($this->qtypeupdater->is_blank_answer($state)) {
 198                  $this->logger->log_assumption("Ignoring second start state with blank answer in attempt at question {$state->question}");
 199                  return;
 200              } else {
 201                  throw new coding_exception("Two inconsistent open states for question session {$this->qsession->id}.");
 202              }
 203          }
 204          $step->state = 'todo';
 205          $this->startstate = $state;
 206          $this->add_step($step);
 207      }
 208  
 209      protected function process1($step, $state) {
 210          $this->unexpected_event($state);
 211      }
 212  
 213      protected function process2($step, $state) {
 214          if ($this->qtypeupdater->was_answered($state)) {
 215              $step->state = 'complete';
 216          } else {
 217              $step->state = 'todo';
 218          }
 219          $this->add_step($step);
 220      }
 221  
 222      protected function process3($step, $state) {
 223          return $this->process6($step, $state);
 224      }
 225  
 226      protected function process4($step, $state) {
 227          $this->unexpected_event($state);
 228      }
 229  
 230      protected function process5($step, $state) {
 231          $this->unexpected_event($state);
 232      }
 233  
 234      protected abstract function process6($step, $state);
 235      protected abstract function process7($step, $state);
 236  
 237      protected function process8($step, $state) {
 238          return $this->process6($step, $state);
 239      }
 240  
 241      protected function process9($step, $state) {
 242          if (!$this->finishstate) {
 243              $submitstate = clone($state);
 244              $submitstate->event = 8;
 245              $submitstate->grade = 0;
 246              $this->process_state($submitstate);
 247          }
 248  
 249          $step->data['-comment'] = $this->qsession->manualcomment;
 250          if ($this->question->maxmark > 0) {
 251              $step->fraction = $state->grade / $this->question->maxmark;
 252              $step->state = $this->manual_graded_state_for_fraction($step->fraction);
 253              $step->data['-mark'] = $state->grade;
 254              $step->data['-maxmark'] = $this->question->maxmark;
 255          } else {
 256              $step->state = 'manfinished';
 257          }
 258          unset($step->data['answer']);
 259          $step->userid = null;
 260          $this->add_step($step);
 261      }
 262  
 263      /**
 264       * @param object $question a question definition
 265       * @return qtype_updater
 266       */
 267      protected function make_qtype_updater() {
 268          global $CFG;
 269  
 270          if ($this->question->qtype == 'deleted') {
 271              return new question_deleted_question_attempt_updater(
 272                      $this, $this->question, $this->logger, $this->qeupdater);
 273          }
 274  
 275          $path = $CFG->dirroot . '/question/type/' . $this->question->qtype . '/db/upgradelib.php';
 276          if (!is_readable($path)) {
 277              throw new coding_exception("Question type {$this->question->qtype}
 278                      is missing important code (the file {$path})
 279                      required to run the upgrade to the new question engine.");
 280          }
 281          include_once($path);
 282          $class = 'qtype_' . $this->question->qtype . '_qe2_attempt_updater';
 283          if (!class_exists($class)) {
 284              throw new coding_exception("Question type {$this->question->qtype}
 285                      is missing important code (the class {$class})
 286                      required to run the upgrade to the new question engine.");
 287          }
 288          return new $class($this, $this->question, $this->logger, $this->qeupdater);
 289      }
 290  
 291      public function to_text($html) {
 292          return trim(html_to_text($html, 0, false));
 293      }
 294  
 295      protected function graded_state_for_fraction($fraction) {
 296          if ($fraction < 0.000001) {
 297              return 'gradedwrong';
 298          } else if ($fraction > 0.999999) {
 299              return 'gradedright';
 300          } else {
 301              return 'gradedpartial';
 302          }
 303      }
 304  
 305      protected function manual_graded_state_for_fraction($fraction) {
 306          if ($fraction < 0.000001) {
 307              return 'mangrwrong';
 308          } else if ($fraction > 0.999999) {
 309              return 'mangrright';
 310          } else {
 311              return 'mangrpartial';
 312          }
 313      }
 314  
 315      protected function make_step($state){
 316          $step = new stdClass();
 317          $step->data = array();
 318  
 319          if ($state->event == 0 || $this->sequencenumber == 0) {
 320              $this->qtypeupdater->set_first_step_data_elements($state, $step->data);
 321          } else {
 322              $this->qtypeupdater->set_data_elements_for_step($state, $step->data);
 323          }
 324  
 325          $step->fraction = null;
 326          $step->timecreated = $state->timestamp ? $state->timestamp : time();
 327          $step->userid = $this->attempt->userid;
 328  
 329          $summary = $this->qtypeupdater->response_summary($state);
 330          if (!is_null($summary)) {
 331              $this->qa->responsesummary = $summary;
 332          }
 333          $this->qa->timemodified = max($this->qa->timemodified, $state->timestamp);
 334  
 335          return $step;
 336      }
 337  }
 338  
 339  
 340  class qbehaviour_deferredfeedback_converter extends question_behaviour_attempt_updater {
 341  
 342      protected function behaviour_name() {
 343          return 'deferredfeedback';
 344      }
 345  
 346      protected function process6($step, $state) {
 347          if (!$this->startstate) {
 348              $this->logger->log_assumption("Ignoring bogus submit before open in attempt at question {$state->question}");
 349              // WTF, but this has happened a few times in our DB. It seems it is safe to ignore.
 350              return;
 351          }
 352  
 353          if ($this->finishstate) {
 354              if ($this->finishstate->answer != $state->answer ||
 355                      $this->finishstate->grade != $state->grade ||
 356                      $this->finishstate->raw_grade != $state->raw_grade ||
 357                      $this->finishstate->penalty != $state->penalty) {
 358                  $this->logger->log_assumption("Two inconsistent finish states found for question session {$this->qsession->id} in attempt at question {$state->question} keeping the later one.");
 359                  $this->discard_last_state();
 360              } else {
 361                  $this->logger->log_assumption("Ignoring extra finish states in attempt at question {$state->question}");
 362                  return;
 363              }
 364          }
 365  
 366          if ($this->question->maxmark > 0) {
 367              $step->fraction = $state->grade / $this->question->maxmark;
 368              $step->state = $this->graded_state_for_fraction($step->fraction);
 369          } else {
 370              $step->state = 'finished';
 371          }
 372          $step->data['-finish'] = '1';
 373          $this->finishstate = $state;
 374          $this->add_step($step);
 375      }
 376  
 377      protected function process7($step, $state) {
 378          $this->unexpected_event($state);
 379      }
 380  }
 381  
 382  
 383  class qbehaviour_manualgraded_converter extends question_behaviour_attempt_updater {
 384      protected function behaviour_name() {
 385          return 'manualgraded';
 386      }
 387  
 388      protected function process6($step, $state) {
 389          $step->state = 'needsgrading';
 390          if (!$this->finishstate) {
 391              $step->data['-finish'] = '1';
 392              $this->finishstate = $state;
 393          }
 394          $this->add_step($step);
 395      }
 396  
 397      protected function process7($step, $state) {
 398          return $this->process2($step, $state);
 399      }
 400  }
 401  
 402  
 403  class qbehaviour_informationitem_converter extends question_behaviour_attempt_updater {
 404  
 405      protected function behaviour_name() {
 406          return 'informationitem';
 407      }
 408  
 409      protected function process0($step, $state) {
 410          if ($this->startstate) {
 411              return;
 412          }
 413          $step->state = 'todo';
 414          $this->startstate = $state;
 415          $this->add_step($step);
 416      }
 417  
 418      protected function process2($step, $state) {
 419          $this->unexpected_event($state);
 420      }
 421  
 422      protected function process3($step, $state) {
 423          $this->unexpected_event($state);
 424      }
 425  
 426      protected function process6($step, $state) {
 427          if ($this->finishstate) {
 428              return;
 429          }
 430  
 431          $step->state = 'finished';
 432          $step->data['-finish'] = '1';
 433          $this->finishstate = $state;
 434          $this->add_step($step);
 435      }
 436  
 437      protected function process7($step, $state) {
 438          return $this->process6($step, $state);
 439      }
 440  
 441      protected function process8($step, $state) {
 442          return $this->process6($step, $state);
 443      }
 444  }
 445  
 446  
 447  class qbehaviour_adaptive_converter extends question_behaviour_attempt_updater {
 448      protected $try;
 449      protected $laststepwasatry = false;
 450      protected $finished = false;
 451      protected $bestrawgrade = 0;
 452  
 453      protected function behaviour_name() {
 454          return 'adaptive';
 455      }
 456  
 457      protected function finish_up() {
 458          parent::finish_up();
 459          if ($this->finishstate || !$this->attempt->timefinish) {
 460              return;
 461          }
 462  
 463          $state = end($this->qstates);
 464          $step = $this->make_step($state);
 465          $this->process6($step, $state);
 466      }
 467  
 468      protected function process0($step, $state) {
 469          $this->try = 1;
 470          $this->laststepwasatry = false;
 471          parent::process0($step, $state);
 472      }
 473  
 474      protected function process2($step, $state) {
 475          if ($this->finishstate) {
 476              $this->logger->log_assumption("Ignoring bogus save after submit in an " .
 477                      "adaptive attempt at question {$state->question} " .
 478                      "(question session {$this->qsession->id})");
 479              return;
 480          }
 481  
 482          if ($this->question->maxmark > 0) {
 483              $step->fraction = $state->grade / $this->question->maxmark;
 484          }
 485  
 486          $this->laststepwasatry = false;
 487          parent::process2($step, $state);
 488      }
 489  
 490      protected function process3($step, $state) {
 491          if ($this->question->maxmark > 0) {
 492              $step->fraction = $state->grade / $this->question->maxmark;
 493              if ($this->graded_state_for_fraction($step->fraction) == 'gradedright') {
 494                  $step->state = 'complete';
 495              } else {
 496                  $step->state = 'todo';
 497              }
 498          } else {
 499              $step->state = 'complete';
 500          }
 501  
 502          $this->bestrawgrade = max($state->raw_grade, $this->bestrawgrade);
 503  
 504          $step->data['-_try'] = $this->try;
 505          $this->try += 1;
 506          $this->laststepwasatry = true;
 507          if ($this->question->maxmark > 0) {
 508              $step->data['-_rawfraction'] = $state->raw_grade / $this->question->maxmark;
 509          } else {
 510              $step->data['-_rawfraction'] = 0;
 511          }
 512          $step->data['-submit'] = 1;
 513  
 514          $this->add_step($step);
 515      }
 516  
 517      protected function process6($step, $state) {
 518          if ($this->finishstate) {
 519              if (!$this->qtypeupdater->compare_answers($this->finishstate->answer, $state->answer) ||
 520                      $this->finishstate->grade != $state->grade ||
 521                      $this->finishstate->raw_grade != $state->raw_grade ||
 522                      $this->finishstate->penalty != $state->penalty) {
 523                  throw new coding_exception("Two inconsistent finish states found for question session {$this->qsession->id}.");
 524              } else {
 525                  $this->logger->log_assumption("Ignoring extra finish states in attempt at question {$state->question}");
 526                  return;
 527              }
 528          }
 529  
 530          $this->bestrawgrade = max($state->raw_grade, $this->bestrawgrade);
 531  
 532          if ($this->question->maxmark > 0) {
 533              $step->fraction = $state->grade / $this->question->maxmark;
 534              $step->state = $this->graded_state_for_fraction(
 535                      $this->bestrawgrade / $this->question->maxmark);
 536          } else {
 537              $step->state = 'finished';
 538          }
 539  
 540          $step->data['-finish'] = 1;
 541          if ($this->laststepwasatry) {
 542              $this->try -= 1;
 543          }
 544          $step->data['-_try'] = $this->try;
 545          if ($this->question->maxmark > 0) {
 546              $step->data['-_rawfraction'] = $state->raw_grade / $this->question->maxmark;
 547          } else {
 548              $step->data['-_rawfraction'] = 0;
 549          }
 550  
 551          $this->finishstate = $state;
 552          $this->add_step($step);
 553      }
 554  
 555      protected function process7($step, $state) {
 556          $this->unexpected_event($state);
 557      }
 558  }
 559  
 560  
 561  class qbehaviour_adaptivenopenalty_converter extends qbehaviour_adaptive_converter {
 562      protected function behaviour_name() {
 563          return 'adaptivenopenalty';
 564      }
 565  }