Search moodle.org's
Developer Documentation

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.
  •    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  }