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