Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

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