[ Index ]

PHP Cross Reference of moodle-2.8

title

Body

[close]

/question/engine/upgrade/ -> upgradelib.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 contains the code required to upgrade all the attempt data from
  19   * old versions of Moodle into the tables used by the new question engine.
  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  global $CFG;
  31  require_once($CFG->dirroot . '/question/engine/bank.php');
  32  require_once($CFG->dirroot . '/question/engine/upgrade/logger.php');
  33  require_once($CFG->dirroot . '/question/engine/upgrade/behaviourconverters.php');
  34  
  35  
  36  /**
  37   * This class manages upgrading all the question attempts from the old database
  38   * structure to the new question engine.
  39   *
  40   * @copyright  2010 The Open University
  41   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  42   */
  43  class question_engine_attempt_upgrader {
  44      /** @var question_engine_upgrade_question_loader */
  45      protected $questionloader;
  46      /** @var question_engine_assumption_logger */
  47      protected $logger;
  48  
  49      public function save_usage($preferredbehaviour, $attempt, $qas, $quizlayout) {
  50          $missing = array();
  51  
  52          $layout = explode(',', $attempt->layout);
  53          $questionkeys = array_combine(array_values($layout), array_keys($layout));
  54  
  55          $this->set_quba_preferred_behaviour($attempt->uniqueid, $preferredbehaviour);
  56  
  57          $i = 0;
  58          foreach (explode(',', $quizlayout) as $questionid) {
  59              if ($questionid == 0) {
  60                  continue;
  61              }
  62              $i++;
  63  
  64              if (!array_key_exists($questionid, $qas)) {
  65                  $missing[] = $questionid;
  66                  $layout[$questionkeys[$questionid]] = $questionid;
  67                  continue;
  68              }
  69  
  70              $qa = $qas[$questionid];
  71              $qa->questionusageid = $attempt->uniqueid;
  72              $qa->slot = $i;
  73              if (core_text::strlen($qa->questionsummary) > question_bank::MAX_SUMMARY_LENGTH) {
  74                  // It seems some people write very long quesions! MDL-30760
  75                  $qa->questionsummary = core_text::substr($qa->questionsummary,
  76                          0, question_bank::MAX_SUMMARY_LENGTH - 3) . '...';
  77              }
  78              $this->insert_record('question_attempts', $qa);
  79              $layout[$questionkeys[$questionid]] = $qa->slot;
  80  
  81              foreach ($qa->steps as $step) {
  82                  $step->questionattemptid = $qa->id;
  83                  $this->insert_record('question_attempt_steps', $step);
  84  
  85                  foreach ($step->data as $name => $value) {
  86                      $datum = new stdClass();
  87                      $datum->attemptstepid = $step->id;
  88                      $datum->name = $name;
  89                      $datum->value = $value;
  90                      $this->insert_record('question_attempt_step_data', $datum, false);
  91                  }
  92              }
  93          }
  94  
  95          $this->set_quiz_attempt_layout($attempt->uniqueid, implode(',', $layout));
  96  
  97          if ($missing) {
  98              notify("Question sessions for questions " .
  99                      implode(', ', $missing) .
 100                      " were missing when upgrading question usage {$attempt->uniqueid}.");
 101          }
 102      }
 103  
 104      protected function set_quba_preferred_behaviour($qubaid, $preferredbehaviour) {
 105          global $DB;
 106          $DB->set_field('question_usages', 'preferredbehaviour', $preferredbehaviour,
 107                  array('id' => $qubaid));
 108      }
 109  
 110      protected function set_quiz_attempt_layout($qubaid, $layout) {
 111          global $DB;
 112          $DB->set_field('quiz_attempts', 'layout', $layout, array('uniqueid' => $qubaid));
 113      }
 114  
 115      protected function delete_quiz_attempt($qubaid) {
 116          global $DB;
 117          $DB->delete_records('quiz_attempts', array('uniqueid' => $qubaid));
 118          $DB->delete_records('question_attempts', array('id' => $qubaid));
 119      }
 120  
 121      protected function insert_record($table, $record, $saveid = true) {
 122          global $DB;
 123          $newid = $DB->insert_record($table, $record, $saveid);
 124          if ($saveid) {
 125              $record->id = $newid;
 126          }
 127          return $newid;
 128      }
 129  
 130      public function load_question($questionid, $quizid = null) {
 131          return $this->questionloader->get_question($questionid, $quizid);
 132      }
 133  
 134      public function load_dataset($questionid, $selecteditem) {
 135          return $this->questionloader->load_dataset($questionid, $selecteditem);
 136      }
 137  
 138      public function get_next_question_session($attempt, moodle_recordset $questionsessionsrs) {
 139          if (!$questionsessionsrs->valid()) {
 140              return false;
 141          }
 142  
 143          $qsession = $questionsessionsrs->current();
 144          if ($qsession->attemptid != $attempt->uniqueid) {
 145              // No more question sessions belonging to this attempt.
 146              return false;
 147          }
 148  
 149          // Session found, move the pointer in the RS and return the record.
 150          $questionsessionsrs->next();
 151          return $qsession;
 152      }
 153  
 154      public function get_question_states($attempt, $question, moodle_recordset $questionsstatesrs) {
 155          $qstates = array();
 156  
 157          while ($questionsstatesrs->valid()) {
 158              $state = $questionsstatesrs->current();
 159              if ($state->attempt != $attempt->uniqueid ||
 160                      $state->question != $question->id) {
 161                  // We have found all the states for this attempt. Stop.
 162                  break;
 163              }
 164  
 165              // Add the new state to the array, and advance.
 166              $qstates[] = $state;
 167              $questionsstatesrs->next();
 168          }
 169  
 170          return $qstates;
 171      }
 172  
 173      protected function get_converter_class_name($question, $quiz, $qsessionid) {
 174          global $DB;
 175          if ($question->qtype == 'deleted') {
 176              $where = '(question = :questionid OR '.$DB->sql_like('answer', ':randomid').') AND event = 7';
 177              $params = array('questionid'=>$question->id, 'randomid'=>"random{$question->id}-%");
 178              if ($DB->record_exists_select('question_states', $where, $params)) {
 179                  $this->logger->log_assumption("Assuming that deleted question {$question->id} was manually graded.");
 180                  return 'qbehaviour_manualgraded_converter';
 181              }
 182          }
 183          $qtype = question_bank::get_qtype($question->qtype, false);
 184          if ($qtype->is_manual_graded()) {
 185              return 'qbehaviour_manualgraded_converter';
 186          } else if ($question->qtype == 'description') {
 187              return 'qbehaviour_informationitem_converter';
 188          } else if ($quiz->preferredbehaviour == 'deferredfeedback') {
 189              return 'qbehaviour_deferredfeedback_converter';
 190          } else if ($quiz->preferredbehaviour == 'adaptive') {
 191              return 'qbehaviour_adaptive_converter';
 192          } else if ($quiz->preferredbehaviour == 'adaptivenopenalty') {
 193              return 'qbehaviour_adaptivenopenalty_converter';
 194          } else {
 195              throw new coding_exception("Question session {$qsessionid}
 196                      has an unexpected preferred behaviour {$quiz->preferredbehaviour}.");
 197          }
 198      }
 199  
 200      public function supply_missing_question_attempt($quiz, $attempt, $question) {
 201          if ($question->qtype == 'random') {
 202              throw new coding_exception("Cannot supply a missing qsession for question
 203                      {$question->id} in attempt {$attempt->id}.");
 204          }
 205  
 206          $converterclass = $this->get_converter_class_name($question, $quiz, 'missing');
 207  
 208          $qbehaviourupdater = new $converterclass($quiz, $attempt, $question,
 209                  null, null, $this->logger, $this);
 210          $qa = $qbehaviourupdater->supply_missing_qa();
 211          $qbehaviourupdater->discard();
 212          return $qa;
 213      }
 214  
 215      public function convert_question_attempt($quiz, $attempt, $question, $qsession, $qstates) {
 216  
 217          if ($question->qtype == 'random') {
 218              list($question, $qstates) = $this->decode_random_attempt($qstates, $question->maxmark);
 219              $qsession->questionid = $question->id;
 220          }
 221  
 222          $converterclass = $this->get_converter_class_name($question, $quiz, $qsession->id);
 223  
 224          $qbehaviourupdater = new $converterclass($quiz, $attempt, $question, $qsession,
 225                  $qstates, $this->logger, $this);
 226          $qa = $qbehaviourupdater->get_converted_qa();
 227          $qbehaviourupdater->discard();
 228          return $qa;
 229      }
 230  
 231      protected function decode_random_attempt($qstates, $maxmark) {
 232          $realquestionid = null;
 233          foreach ($qstates as $i => $state) {
 234              if (strpos($state->answer, '-') < 6) {
 235                  // Broken state, skip it.
 236                  $this->logger->log_assumption("Had to skip brokes state {$state->id}
 237                          for question {$state->question}.");
 238                  unset($qstates[$i]);
 239                  continue;
 240              }
 241              list($randombit, $realanswer) = explode('-', $state->answer, 2);
 242              $newquestionid = substr($randombit, 6);
 243              if ($realquestionid && $realquestionid != $newquestionid) {
 244                  throw new coding_exception("Question session {$this->qsession->id}
 245                          for random question points to two different real questions
 246                          {$realquestionid} and {$newquestionid}.");
 247              }
 248              $qstates[$i]->answer = $realanswer;
 249          }
 250  
 251          if (empty($newquestionid)) {
 252              // This attempt only had broken states. Set a fake $newquestionid to
 253              // prevent a null DB error later.
 254              $newquestionid = 0;
 255          }
 256  
 257          $newquestion = $this->load_question($newquestionid);
 258          $newquestion->maxmark = $maxmark;
 259          return array($newquestion, $qstates);
 260      }
 261  
 262      public function prepare_to_restore() {
 263          $this->logger = new dummy_question_engine_assumption_logger();
 264          $this->questionloader = new question_engine_upgrade_question_loader($this->logger);
 265      }
 266  }
 267  
 268  
 269  /**
 270   * This class deals with loading (and caching) question definitions during the
 271   * question engine upgrade.
 272   *
 273   * @copyright  2010 The Open University
 274   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 275   */
 276  class question_engine_upgrade_question_loader {
 277      protected $cache = array();
 278      protected $datasetcache = array();
 279  
 280      public function __construct($logger) {
 281          $this->logger = $logger;
 282      }
 283  
 284      protected function load_question($questionid, $quizid) {
 285          global $DB;
 286  
 287          if ($quizid) {
 288              $question = $DB->get_record_sql("
 289                  SELECT q.*, slot.maxmark
 290                  FROM {question} q
 291                  JOIN {quiz_slots} slot ON slot.questionid = q.id
 292                  WHERE q.id = ? AND slot.quizid = ?", array($questionid, $quizid));
 293          } else {
 294              $question = $DB->get_record('question', array('id' => $questionid));
 295          }
 296  
 297          if (!$question) {
 298              return null;
 299          }
 300  
 301          if (empty($question->defaultmark)) {
 302              if (!empty($question->defaultgrade)) {
 303                  $question->defaultmark = $question->defaultgrade;
 304              } else {
 305                  $question->defaultmark = 0;
 306              }
 307              unset($question->defaultgrade);
 308          }
 309  
 310          $qtype = question_bank::get_qtype($question->qtype, false);
 311          if ($qtype->name() === 'missingtype') {
 312              $this->logger->log_assumption("Dealing with question id {$question->id}
 313                      that is of an unknown type {$question->qtype}.");
 314              $question->questiontext = '<p>' . get_string('warningmissingtype', 'quiz') .
 315                      '</p>' . $question->questiontext;
 316          }
 317  
 318          $qtype->get_question_options($question);
 319  
 320          return $question;
 321      }
 322  
 323      public function get_question($questionid, $quizid) {
 324          if (isset($this->cache[$questionid])) {
 325              return $this->cache[$questionid];
 326          }
 327  
 328          $question = $this->load_question($questionid, $quizid);
 329  
 330          if (!$question) {
 331              $this->logger->log_assumption("Dealing with question id {$questionid}
 332                      that was missing from the database.");
 333              $question = new stdClass();
 334              $question->id = $questionid;
 335              $question->qtype = 'deleted';
 336              $question->maxmark = 1; // Guess, but that is all we can do.
 337              $question->questiontext = get_string('deletedquestiontext', 'qtype_missingtype');
 338          }
 339  
 340          $this->cache[$questionid] = $question;
 341          return $this->cache[$questionid];
 342      }
 343  
 344      public function load_dataset($questionid, $selecteditem) {
 345          global $DB;
 346  
 347          if (isset($this->datasetcache[$questionid][$selecteditem])) {
 348              return $this->datasetcache[$questionid][$selecteditem];
 349          }
 350  
 351          $this->datasetcache[$questionid][$selecteditem] = $DB->get_records_sql_menu('
 352                  SELECT qdd.name, qdi.value
 353                    FROM {question_dataset_items} qdi
 354                    JOIN {question_dataset_definitions} qdd ON qdd.id = qdi.definition
 355                    JOIN {question_datasets} qd ON qdd.id = qd.datasetdefinition
 356                   WHERE qd.question = ?
 357                     AND qdi.itemnumber = ?
 358                  ', array($questionid, $selecteditem));
 359          return $this->datasetcache[$questionid][$selecteditem];
 360      }
 361  }
 362  
 363  
 364  /**
 365   * Base class for the classes that convert the question-type specific bits of
 366   * the attempt data.
 367   *
 368   * @copyright  2010 The Open University
 369   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 370   */
 371  abstract class question_qtype_attempt_updater {
 372      /** @var object the question definition data. */
 373      protected $question;
 374      /** @var question_behaviour_attempt_updater */
 375      protected $updater;
 376      /** @var question_engine_assumption_logger */
 377      protected $logger;
 378      /** @var question_engine_attempt_upgrader */
 379      protected $qeupdater;
 380  
 381      public function __construct($updater, $question, $logger, $qeupdater) {
 382          $this->updater = $updater;
 383          $this->question = $question;
 384          $this->logger = $logger;
 385          $this->qeupdater = $qeupdater;
 386      }
 387  
 388      public function discard() {
 389          // Help the garbage collector, which seems to be struggling.
 390          $this->updater = null;
 391          $this->question = null;
 392          $this->logger = null;
 393          $this->qeupdater = null;
 394      }
 395  
 396      protected function to_text($html) {
 397          return $this->updater->to_text($html);
 398      }
 399  
 400      public function question_summary() {
 401          return $this->to_text($this->question->questiontext);
 402      }
 403  
 404      public function compare_answers($answer1, $answer2) {
 405          return $answer1 == $answer2;
 406      }
 407  
 408      public function is_blank_answer($state) {
 409          return $state->answer == '';
 410      }
 411  
 412      public abstract function right_answer();
 413      public abstract function response_summary($state);
 414      public abstract function was_answered($state);
 415      public abstract function set_first_step_data_elements($state, &$data);
 416      public abstract function set_data_elements_for_step($state, &$data);
 417      public abstract function supply_missing_first_step_data(&$data);
 418  }
 419  
 420  
 421  class question_deleted_question_attempt_updater extends question_qtype_attempt_updater {
 422      public function right_answer() {
 423          return '';
 424      }
 425  
 426      public function response_summary($state) {
 427          return $state->answer;
 428      }
 429  
 430      public function was_answered($state) {
 431          return !empty($state->answer);
 432      }
 433  
 434      public function set_first_step_data_elements($state, &$data) {
 435          $data['upgradedfromdeletedquestion'] = $state->answer;
 436      }
 437  
 438      public function supply_missing_first_step_data(&$data) {
 439      }
 440  
 441      public function set_data_elements_for_step($state, &$data) {
 442          $data['upgradedfromdeletedquestion'] = $state->answer;
 443      }
 444  }
 445  
 446  /**
 447   * This check verifies that all quiz attempts were upgraded since following
 448   * the question engine upgrade in Moodle 2.1.
 449   *
 450   * @param environment_results object to update, if relevant.
 451   * @return environment_results updated results object, or null if this test is not relevant.
 452   */
 453  function quiz_attempts_upgraded(environment_results $result) {
 454      global $DB;
 455  
 456      $dbman = $DB->get_manager();
 457      $table = new xmldb_table('quiz_attempts');
 458      $field = new xmldb_field('needsupgradetonewqe');
 459  
 460      if (!$dbman->table_exists($table) || !$dbman->field_exists($table, $field)) {
 461          // DB already upgraded. This test is no longer relevant.
 462          return null;
 463      }
 464  
 465      if (!$DB->record_exists('quiz_attempts', array('needsupgradetonewqe' => 1))) {
 466          // No 1s present in that column means there are no problems.
 467          return null;
 468      }
 469  
 470      // Only display anything if the admins need to be aware of the problem.
 471      $result->setStatus(false);
 472      return $result;
 473  }


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