Search moodle.org's
Developer Documentation

See Release Notes

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

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

   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   * Upgrade library code for the randomsamatch question type.
  19   *
  20   * @package   qtype_randomsamatch
  21   * @copyright 2013 Jean-Michel Vedrine
  22   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  
  26  defined('MOODLE_INTERNAL') || die();
  27  
  28  
  29  /**
  30   * Class for converting attempt data for randomsamatch questions when upgrading
  31   * attempts to the new question engine.
  32   *
  33   * This class is used by the code in question/engine/upgrade/upgradelib.php.
  34   *
  35   * @copyright 2013 Jean-Michel Vedrine
  36   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  37   */
  38  class qtype_randomsamatch_qe2_attempt_updater extends question_qtype_attempt_updater {
  39      /** @var array of question stems. */
  40      protected $stems;
  41      /** @var array of question stems format. */
  42      protected $stemformat;
  43      /** @var array of choices that can be matched to each stem. */
  44      protected $choices;
  45      /** @var array index of the right choice for each stem. */
  46      protected $right;
  47      /** @var array id of the right answer for each stem (used by {@link lookup_choice}). */
  48      protected $rightanswerid;
  49      /** @var array shuffled stem indexes. */
  50      protected $stemorder;
  51      /** @var array shuffled choice indexes. */
  52      protected $choiceorder;
  53      /** @var array flipped version of the choiceorder array. */
  54      protected $flippedchoiceorder;
  55      /** @var array of right answer for each stem. */
  56      protected $rightanswer;
  57  
  58      public function question_summary() {
  59          return ''; // Done later, after we know which shortanswer questions are used.
  60      }
  61  
  62      public function right_answer() {
  63          return ''; // Done later, after we know which shortanswer questions are used.
  64      }
  65  
  66      /**
  67       * Explode the answer saved as a string in state
  68       *
  69       * @param string $answer comma separated list of dash separated pairs
  70       * @return array
  71       */
  72      protected function explode_answer($answer) {
  73          if (!$answer) {
  74              return array();
  75          }
  76          $bits = explode(',', $answer);
  77          $selections = array();
  78          foreach ($bits as $bit) {
  79              list($stem, $choice) = explode('-', $bit);
  80              $selections[$stem] = $choice;
  81          }
  82          return $selections;
  83      }
  84  
  85      protected function make_summary($pairs) {
  86          $bits = array();
  87          foreach ($pairs as $stem => $answer) {
  88              $bits[] = $stem . ' -> ' . $answer;
  89          }
  90          return implode('; ', $bits);
  91      }
  92  
  93      /**
  94       * Find the index corresponding to a choice
  95       *
  96       * @param integer $choice
  97       * @return integer
  98       */
  99      protected function lookup_choice($choice) {
 100          if (array_key_exists($choice, $this->choices)) {
 101              // Easy case: choice is a key in the choices array.
 102              return $choice;
 103          } else {
 104              // But choice can also be the id of a shortanser correct answer
 105              // without been a key of the choices array, in that case we need
 106              // to first find the shortanswer id, then find the choices index
 107              // associated to it.
 108              $questionid = array_search($choice, $this->rightanswerid);
 109              if ($questionid) {
 110                  return $this->right[$questionid];
 111              }
 112          }
 113          return null;
 114      }
 115  
 116      public function response_summary($state) {
 117          $choices = $this->explode_answer($state->answer);
 118          if (empty($choices)) {
 119              return null;
 120          }
 121  
 122          $pairs = array();
 123          foreach ($choices as $stemid => $choicekey) {
 124              if (array_key_exists($stemid, $this->stems) && $choices[$stemid]) {
 125                  $choiceid = $this->lookup_choice($choicekey);
 126                  if ($choiceid) {
 127                      $pairs[$this->stems[$stemid]] = $this->choices[$choiceid];
 128                  } else {
 129                      $this->logger->log_assumption("Dealing with a place where the
 130                              student selected a choice that was later deleted for
 131                              randomsamatch question {$this->question->id}");
 132                      $pairs[$this->stems[$stemid]] = '[CHOICE THAT WAS LATER DELETED]';
 133                  }
 134              }
 135          }
 136  
 137          if ($pairs) {
 138              return $this->make_summary($pairs);
 139          } else {
 140              return '';
 141          }
 142      }
 143  
 144      public function was_answered($state) {
 145          $choices = $this->explode_answer($state->answer);
 146          foreach ($choices as $choice) {
 147              if ($choice) {
 148                  return true;
 149              }
 150          }
 151          return false;
 152      }
 153  
 154      public function set_first_step_data_elements($state, &$data) {
 155          $this->stems = array();
 156          $this->stemformat = array();
 157          $this->choices = array();
 158          $this->right = array();
 159          $this->rightanswer = array();
 160          $choices = $this->explode_answer($state->answer);
 161          $this->stemorder = array();
 162          foreach ($choices as $key => $notused) {
 163              $this->stemorder[] = $key;
 164          }
 165          $wrappedquestions = array();
 166          // TODO test what happen when some questions are missing.
 167          foreach ($this->stemorder as $questionid) {
 168              $wrappedquestions[] = $this->load_question($questionid);
 169          }
 170          foreach ($wrappedquestions as $wrappedquestion) {
 171  
 172              // We only take into account the first correct answer.
 173              $foundcorrect = false;
 174              foreach ($wrappedquestion->options->answers as $answer) {
 175                  if ($foundcorrect || $answer->fraction != 1.0) {
 176                      unset($wrappedquestion->options->answers[$answer->id]);
 177                  } else if (!$foundcorrect) {
 178                      $foundcorrect = true;
 179                      // Store right answer id, so we can use it later in lookup_choice.
 180                      $this->rightanswerid[$wrappedquestion->id] = $answer->id;
 181                      $key = array_search($answer->answer, $this->choices);
 182                      if ($key === false) {
 183                          $key = $answer->id;
 184                          $this->choices[$key] = $answer->answer;
 185                          $data['_choice_' . $key] = $answer->answer;
 186                      }
 187                      $this->stems[$wrappedquestion->id] = $wrappedquestion->questiontext;
 188                      $this->stemformat[$wrappedquestion->id] = $wrappedquestion->questiontextformat;
 189                      $this->right[$wrappedquestion->id] = $key;
 190                      $this->rightanswer[$wrappedquestion->id] = $answer->answer;
 191  
 192                      $data['_stem_' . $wrappedquestion->id] = $wrappedquestion->questiontext;
 193                      $data['_stemformat_' . $wrappedquestion->id] = $wrappedquestion->questiontextformat;
 194                      $data['_right_' . $wrappedquestion->id] = $key;
 195  
 196                  }
 197              }
 198          }
 199          $this->choiceorder = array_keys($this->choices);
 200          // We don't shuffle the choices as that seems unnecessary for old upgraded attempts.
 201          $this->flippedchoiceorder = array_combine(
 202                  array_values($this->choiceorder), array_keys($this->choiceorder));
 203  
 204          $data['_stemorder'] = implode(',', $this->stemorder);
 205          $data['_choiceorder'] = implode(',', $this->choiceorder);
 206  
 207          $this->updater->qa->questionsummary = $this->to_text($this->question->questiontext) . ' {' .
 208                  implode('; ', $this->stems) . '} -> {' . implode('; ', $this->choices) . '}';
 209  
 210          $answer = array();
 211          foreach ($this->stems as $key => $stem) {
 212              $answer[$stem] = $this->choices[$this->right[$key]];
 213          }
 214          $this->updater->qa->rightanswer = $this->make_summary($answer);
 215      }
 216  
 217      public function supply_missing_first_step_data(&$data) {
 218          throw new coding_exception('qtype_randomsamatch_updater::supply_missing_first_step_data ' .
 219                  'not tested');
 220          $data['_stemorder'] = array();
 221          $data['_choiceorder'] = array();
 222      }
 223  
 224      public function set_data_elements_for_step($state, &$data) {
 225          $choices = $this->explode_answer($state->answer);
 226  
 227          foreach ($this->stemorder as $i => $key) {
 228              if (empty($choices[$key])) {
 229                  $data['sub' . $i] = 0;
 230                  continue;
 231              }
 232              $choice = $this->lookup_choice($choices[$key]);
 233  
 234              if (array_key_exists($choice, $this->flippedchoiceorder)) {
 235                  $data['sub' . $i] = $this->flippedchoiceorder[$choice] + 1;
 236              } else {
 237                  $data['sub' . $i] = 0;
 238              }
 239          }
 240      }
 241  
 242      public function load_question($questionid) {
 243          return $this->qeupdater->load_question($questionid);
 244      }
 245  }