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]

   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   * Question type class for the embedded element in question text question types.
  19   *
  20   * @package    qtype_gapselect
  21   * @copyright  2011 The Open University
  22   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  defined('MOODLE_INTERNAL') || die();
  26  
  27  require_once($CFG->libdir . '/questionlib.php');
  28  require_once($CFG->dirroot . '/question/engine/lib.php');
  29  require_once($CFG->dirroot . '/question/format/xml/format.php');
  30  
  31  
  32  /**
  33   * The embedded element in question text question type class.
  34   *
  35   * @copyright  2011 The Open University
  36   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  37   */
  38  abstract class qtype_gapselect_base extends question_type {
  39      /**
  40       * Choices are stored in the question_answers table, and any options need to
  41       * be put into the feedback field somehow. This method is responsible for
  42       * converting all the options to a single string for this purpose. It is used
  43       * by {@link save_question_options()}.
  44       * @param array $choice the form data relating to this choice.
  45       * @return string ready to store in the database.
  46       */
  47      protected abstract function choice_options_to_feedback($choice);
  48  
  49      public function save_question_options($question) {
  50          global $DB;
  51          $context = $question->context;
  52  
  53          // This question type needs the choices to be consecutively numbered, but
  54          // there is no reason why the question author should have done that,
  55          // so renumber if necessary.
  56          // Insert all the new answers.
  57          $nonblankchoices = [];
  58          $questiontext = $question->questiontext;
  59          $newkey = 0;
  60          foreach ($question->choices as $key => $choice) {
  61              if (trim($choice['answer']) == '') {
  62                  continue;
  63              }
  64  
  65              $nonblankchoices[] = $choice;
  66              if ($newkey != $key) {
  67                  // Safe to do this in this order, because we will always be replacing
  68                  // a bigger number with a smaller number that is not present.
  69                  // Numbers in the question text always one bigger than the array index.
  70                  $questiontext = str_replace('[[' . ($key + 1) . ']]', '[[' . ($newkey + 1) . ']]',
  71                          $questiontext);
  72              }
  73              $newkey += 1;
  74          }
  75          $question->choices = $nonblankchoices;
  76          if ($questiontext !== $question->questiontext) {
  77              $DB->set_field('question', 'questiontext', $questiontext,
  78                      ['id' => $question->id]);
  79              $question->questiontext = $questiontext;
  80          }
  81  
  82          $oldanswers = $DB->get_records('question_answers',
  83                  array('question' => $question->id), 'id ASC');
  84  
  85          // Insert all the new answers.
  86          foreach ($question->choices as $key => $choice) {
  87  
  88              // Answer guaranteed to be non-blank. See above.
  89  
  90              $feedback = $this->choice_options_to_feedback($choice);
  91  
  92              if ($answer = array_shift($oldanswers)) {
  93                  $answer->answer = trim($choice['answer']);
  94                  $answer->feedback = $feedback;
  95                  $DB->update_record('question_answers', $answer);
  96  
  97              } else {
  98                  $answer = new stdClass();
  99                  $answer->question = $question->id;
 100                  $answer->answer = $choice['answer'];
 101                  $answer->answerformat = FORMAT_HTML;
 102                  $answer->fraction = 0;
 103                  $answer->feedback = $feedback;
 104                  $answer->feedbackformat = 0;
 105                  $DB->insert_record('question_answers', $answer);
 106              }
 107          }
 108  
 109          // Delete old answer records.
 110          foreach ($oldanswers as $oa) {
 111              $DB->delete_records('question_answers', array('id' => $oa->id));
 112          }
 113  
 114          $options = $DB->get_record('question_' . $this->name(),
 115                  array('questionid' => $question->id));
 116          if (!$options) {
 117              $options = new stdClass();
 118              $options->questionid = $question->id;
 119              $options->correctfeedback = '';
 120              $options->partiallycorrectfeedback = '';
 121              $options->incorrectfeedback = '';
 122              $options->id = $DB->insert_record('question_' . $this->name(), $options);
 123          }
 124  
 125          $options->shuffleanswers = !empty($question->shuffleanswers);
 126          $options = $this->save_combined_feedback_helper($options, $question, $context, true);
 127          $DB->update_record('question_' . $this->name(), $options);
 128  
 129          $this->save_hints($question, true);
 130      }
 131  
 132      public function get_question_options($question) {
 133          global $DB;
 134          $question->options = $DB->get_record('question_'.$this->name(),
 135                  array('questionid' => $question->id), '*', MUST_EXIST);
 136          parent::get_question_options($question);
 137      }
 138  
 139      public function delete_question($questionid, $contextid) {
 140          global $DB;
 141          $DB->delete_records('question_'.$this->name(), array('questionid' => $questionid));
 142          return parent::delete_question($questionid, $contextid);
 143      }
 144  
 145      /**
 146       * Used by {@link initialise_question_instance()} to set up the choice-specific data.
 147       * @param object $choicedata as loaded from the question_answers table.
 148       * @return object an appropriate object for representing the choice.
 149       */
 150      protected abstract function make_choice($choicedata);
 151  
 152      protected function initialise_question_instance(question_definition $question, $questiondata) {
 153          parent::initialise_question_instance($question, $questiondata);
 154  
 155          $question->shufflechoices = $questiondata->options->shuffleanswers;
 156  
 157          $this->initialise_combined_feedback($question, $questiondata, true);
 158  
 159          $question->choices = array();
 160          $choiceindexmap = array();
 161  
 162          // Store the choices in arrays by group.
 163          $i = 1;
 164          foreach ($questiondata->options->answers as $choicedata) {
 165              $choice = $this->make_choice($choicedata);
 166  
 167              if (array_key_exists($choice->choice_group(), $question->choices)) {
 168                  $question->choices[$choice->choice_group()][] = $choice;
 169              } else {
 170                  $question->choices[$choice->choice_group()][1] = $choice;
 171              }
 172  
 173              end($question->choices[$choice->choice_group()]);
 174              $choiceindexmap[$i] = array($choice->choice_group(),
 175                      key($question->choices[$choice->choice_group()]));
 176              $i += 1;
 177          }
 178  
 179          $question->places = array();
 180          $question->textfragments = array();
 181          $question->rightchoices = array();
 182          // Break up the question text, and store the fragments, places and right answers.
 183  
 184          $bits = preg_split('/\[\[(\d+)]]/', $question->questiontext,
 185                  -1, PREG_SPLIT_DELIM_CAPTURE);
 186          $question->textfragments[0] = array_shift($bits);
 187          $i = 1;
 188  
 189          while (!empty($bits)) {
 190              $choice = array_shift($bits);
 191  
 192              list($group, $choiceindex) = $choiceindexmap[$choice];
 193              $question->places[$i] = $group;
 194              $question->rightchoices[$i] = $choiceindex;
 195  
 196              $question->textfragments[$i] = array_shift($bits);
 197              $i += 1;
 198          }
 199      }
 200  
 201      protected function make_hint($hint) {
 202          return question_hint_with_parts::load_from_record($hint);
 203      }
 204  
 205      public function get_random_guess_score($questiondata) {
 206          $question = $this->make_question($questiondata);
 207          return $question->get_random_guess_score();
 208      }
 209  
 210      /**
 211       * This function should reverse {@link choice_options_to_feedback()}.
 212       * @param string $feedback the data loaded from the database.
 213       * @return array the choice options.
 214       */
 215      protected abstract function feedback_to_choice_options($feedback);
 216  
 217      /**
 218       * This method gets the choices (answers)
 219       * in a 2 dimentional array.
 220       *
 221       * @param object $question
 222       * @return array of groups
 223       */
 224      protected function get_array_of_choices($question) {
 225          $subquestions = $question->options->answers;
 226          $count = 0;
 227          foreach ($subquestions as $key => $subquestion) {
 228              $answers[$count]['id'] = $subquestion->id;
 229              $answers[$count]['answer'] = $subquestion->answer;
 230              $answers[$count]['fraction'] = $subquestion->fraction;
 231              $answers[$count] += $this->feedback_to_choice_options($subquestion->feedback);
 232              $answers[$count]['choice'] = $count + 1;
 233              ++$count;
 234          }
 235          return $answers;
 236      }
 237  
 238      /**
 239       * This method gets the choices (answers) and sort them by groups
 240       * in a 2 dimentional array.
 241       *
 242       * @param object $question
 243       * @param object $state Question state object
 244       * @return array of groups
 245       */
 246      protected function get_array_of_groups($question, $state) {
 247          $answers = $this->get_array_of_choices($question);
 248          $arr = array();
 249          for ($group = 1; $group < count($answers); $group++) {
 250              $players = $this->get_group_of_players($question, $state, $answers, $group);
 251              if ($players) {
 252                  $arr[$group] = $players;
 253              }
 254          }
 255          return $arr;
 256      }
 257  
 258      /**
 259       * This method gets the correct answers in a 2 dimentional array.
 260       *
 261       * @param object $question
 262       * @return array of groups
 263       */
 264      protected function get_correct_answers($question) {
 265          $arrayofchoices = $this->get_array_of_choices($question);
 266          $arrayofplaceholdeers = $this->get_array_of_placeholders($question);
 267  
 268          $correctplayers = array();
 269          foreach ($arrayofplaceholdeers as $ph) {
 270              foreach ($arrayofchoices as $key => $choice) {
 271                  if ($key + 1 == $ph) {
 272                      $correctplayers[] = $choice;
 273                  }
 274              }
 275          }
 276          return $correctplayers;
 277      }
 278  
 279      /**
 280       * Return the list of groups used in a question.
 281       * @param stdClass $question the question data.
 282       * @return array the groups used, or false if an error occurs.
 283       */
 284      protected function get_array_of_placeholders($question) {
 285          $qtext = $question->questiontext;
 286          $error = '<b> ERROR</b>: Please check the form for this question. ';
 287          if (!$qtext) {
 288              echo $error . 'The question text is empty!';
 289              return false;
 290          }
 291  
 292          // Get the slots.
 293          $slots = $this->getEmbeddedTextArray($question);
 294  
 295          if (!$slots) {
 296              echo $error . 'The question text is not in the correct format!';
 297              return false;
 298          }
 299  
 300          $output = array();
 301          foreach ($slots as $slot) {
 302              $output[] = substr($slot, 2, strlen($slot) - 4); // 2 is for '[[' and 4 is for '[[]]'.
 303          }
 304          return $output;
 305      }
 306  
 307      protected function get_group_of_players($question, $state, $subquestions, $group) {
 308          $goupofanswers = array();
 309          foreach ($subquestions as $key => $subquestion) {
 310              if ($subquestion[$this->choice_group_key()] == $group) {
 311                  $goupofanswers[] = $subquestion;
 312              }
 313          }
 314  
 315          // Shuffle answers within this group.
 316          if ($question->options->shuffleanswers == 1) {
 317              shuffle($goupofanswers);
 318          }
 319          return $goupofanswers;
 320      }
 321  
 322      public function save_defaults_for_new_questions(stdClass $fromform): void {
 323          parent::save_defaults_for_new_questions($fromform);
 324          $this->set_default_value('shuffleanswers', $fromform->shuffleanswers ?? 0);
 325      }
 326  
 327      public function get_possible_responses($questiondata) {
 328          $question = $this->make_question($questiondata);
 329  
 330          $parts = array();
 331          foreach ($question->places as $place => $group) {
 332              $choices = array();
 333  
 334              foreach ($question->choices[$group] as $i => $choice) {
 335                  $choices[$i] = new question_possible_response(
 336                          html_to_text($choice->text, 0, false),
 337                          ($question->rightchoices[$place] == $i) / count($question->places));
 338              }
 339              $choices[null] = question_possible_response::no_response();
 340  
 341              $parts[$place] = $choices;
 342          }
 343  
 344          return $parts;
 345      }
 346  
 347      public function move_files($questionid, $oldcontextid, $newcontextid) {
 348          parent::move_files($questionid, $oldcontextid, $newcontextid);
 349          $this->move_files_in_combined_feedback($questionid, $oldcontextid, $newcontextid);
 350          $this->move_files_in_hints($questionid, $oldcontextid, $newcontextid);
 351      }
 352  
 353      protected function delete_files($questionid, $contextid) {
 354          parent::delete_files($questionid, $contextid);
 355          $this->delete_files_in_combined_feedback($questionid, $contextid);
 356          $this->delete_files_in_hints($questionid, $contextid);
 357      }
 358  }