Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.

Differences Between: [Versions 310 and 400] [Versions 39 and 400]

   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 matching question type.
  19   *
  20   * @package   qtype_match
  21   * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
  22   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  
  26  defined('MOODLE_INTERNAL') || die();
  27  
  28  require_once($CFG->libdir . '/questionlib.php');
  29  require_once($CFG->dirroot . '/question/engine/lib.php');
  30  
  31  
  32  /**
  33   * The matching question type class.
  34   *
  35   * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
  36   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  37   */
  38  class qtype_match extends question_type {
  39  
  40      public function get_question_options($question) {
  41          global $DB;
  42          parent::get_question_options($question);
  43          $question->options = $DB->get_record('qtype_match_options',
  44                  array('questionid' => $question->id));
  45          $question->options->subquestions = $DB->get_records('qtype_match_subquestions',
  46                  array('questionid' => $question->id), 'id ASC');
  47          return true;
  48      }
  49  
  50      public function save_defaults_for_new_questions(stdClass $fromform): void {
  51          parent::save_defaults_for_new_questions($fromform);
  52          $this->set_default_value('shuffleanswers', $fromform->shuffleanswers);
  53      }
  54  
  55      public function save_question_options($question) {
  56          global $DB;
  57          $context = $question->context;
  58          $result = new stdClass();
  59  
  60          $oldsubquestions = $DB->get_records('qtype_match_subquestions',
  61                  array('questionid' => $question->id), 'id ASC');
  62  
  63          // Insert all the new question & answer pairs.
  64          foreach ($question->subquestions as $key => $questiontext) {
  65              if ($questiontext['text'] == '' && trim($question->subanswers[$key]) == '') {
  66                  continue;
  67              }
  68              if ($questiontext['text'] != '' && trim($question->subanswers[$key]) == '') {
  69                  $result->notice = get_string('nomatchinganswer', 'qtype_match', $questiontext);
  70              }
  71  
  72              // Update an existing subquestion if possible.
  73              $subquestion = array_shift($oldsubquestions);
  74              if (!$subquestion) {
  75                  $subquestion = new stdClass();
  76                  $subquestion->questionid = $question->id;
  77                  $subquestion->questiontext = '';
  78                  $subquestion->answertext = '';
  79                  $subquestion->id = $DB->insert_record('qtype_match_subquestions', $subquestion);
  80              }
  81  
  82              $subquestion->questiontext = $this->import_or_save_files($questiontext,
  83                      $context, 'qtype_match', 'subquestion', $subquestion->id);
  84              $subquestion->questiontextformat = $questiontext['format'];
  85              $subquestion->answertext = trim($question->subanswers[$key]);
  86  
  87              $DB->update_record('qtype_match_subquestions', $subquestion);
  88          }
  89  
  90          // Delete old subquestions records.
  91          $fs = get_file_storage();
  92          foreach ($oldsubquestions as $oldsub) {
  93              $fs->delete_area_files($context->id, 'qtype_match', 'subquestion', $oldsub->id);
  94              $DB->delete_records('qtype_match_subquestions', array('id' => $oldsub->id));
  95          }
  96  
  97          // Save the question options.
  98          $options = $DB->get_record('qtype_match_options', array('questionid' => $question->id));
  99          if (!$options) {
 100              $options = new stdClass();
 101              $options->questionid = $question->id;
 102              $options->correctfeedback = '';
 103              $options->partiallycorrectfeedback = '';
 104              $options->incorrectfeedback = '';
 105              $options->id = $DB->insert_record('qtype_match_options', $options);
 106          }
 107  
 108          $options->shuffleanswers = $question->shuffleanswers;
 109          $options = $this->save_combined_feedback_helper($options, $question, $context, true);
 110          $DB->update_record('qtype_match_options', $options);
 111  
 112          $this->save_hints($question, true);
 113  
 114          if (!empty($result->notice)) {
 115              return $result;
 116          }
 117  
 118          return true;
 119      }
 120  
 121      protected function initialise_question_instance(question_definition $question, $questiondata) {
 122          parent::initialise_question_instance($question, $questiondata);
 123  
 124          $question->shufflestems = $questiondata->options->shuffleanswers;
 125          $this->initialise_combined_feedback($question, $questiondata, true);
 126  
 127          $question->stems = array();
 128          $question->choices = array();
 129          $question->right = array();
 130  
 131          foreach ($questiondata->options->subquestions as $matchsub) {
 132              $key = array_search($matchsub->answertext, $question->choices, true);
 133              if ($key === false) {
 134                  $key = $matchsub->id;
 135                  $question->choices[$key] = $matchsub->answertext;
 136              }
 137  
 138              if ($matchsub->questiontext !== '') {
 139                  $question->stems[$matchsub->id] = $matchsub->questiontext;
 140                  $question->stemformat[$matchsub->id] = $matchsub->questiontextformat;
 141                  $question->right[$matchsub->id] = $key;
 142              }
 143          }
 144      }
 145  
 146      protected function make_hint($hint) {
 147          return question_hint_with_parts::load_from_record($hint);
 148      }
 149  
 150      public function delete_question($questionid, $contextid) {
 151          global $DB;
 152          $DB->delete_records('qtype_match_options', array('questionid' => $questionid));
 153          $DB->delete_records('qtype_match_subquestions', array('questionid' => $questionid));
 154  
 155          parent::delete_question($questionid, $contextid);
 156      }
 157  
 158      public function get_random_guess_score($questiondata) {
 159          $q = $this->make_question($questiondata);
 160          return 1 / count($q->choices);
 161      }
 162  
 163      public function get_possible_responses($questiondata) {
 164          $subqs = array();
 165  
 166          $q = $this->make_question($questiondata);
 167  
 168          foreach ($q->stems as $stemid => $stem) {
 169  
 170              $responses = array();
 171              foreach ($q->choices as $choiceid => $choice) {
 172                  $responses[$choiceid] = new question_possible_response(
 173                          $q->html_to_text($stem, $q->stemformat[$stemid]) . ': ' . $choice,
 174                          ($choiceid == $q->right[$stemid]) / count($q->stems));
 175              }
 176              $responses[null] = question_possible_response::no_response();
 177  
 178              $subqs[$stemid] = $responses;
 179          }
 180  
 181          return $subqs;
 182      }
 183  
 184      public function move_files($questionid, $oldcontextid, $newcontextid) {
 185          global $DB;
 186          $fs = get_file_storage();
 187  
 188          parent::move_files($questionid, $oldcontextid, $newcontextid);
 189  
 190          $subquestionids = $DB->get_records_menu('qtype_match_subquestions',
 191                  array('questionid' => $questionid), 'id', 'id,1');
 192          foreach ($subquestionids as $subquestionid => $notused) {
 193              $fs->move_area_files_to_new_context($oldcontextid,
 194                      $newcontextid, 'qtype_match', 'subquestion', $subquestionid);
 195          }
 196  
 197          $this->move_files_in_combined_feedback($questionid, $oldcontextid, $newcontextid);
 198          $this->move_files_in_hints($questionid, $oldcontextid, $newcontextid);
 199      }
 200  
 201      protected function delete_files($questionid, $contextid) {
 202          global $DB;
 203          $fs = get_file_storage();
 204  
 205          parent::delete_files($questionid, $contextid);
 206  
 207          $subquestionids = $DB->get_records_menu('qtype_match_subquestions',
 208                  array('questionid' => $questionid), 'id', 'id,1');
 209          foreach ($subquestionids as $subquestionid => $notused) {
 210              $fs->delete_area_files($contextid, 'qtype_match', 'subquestion', $subquestionid);
 211          }
 212  
 213          $this->delete_files_in_combined_feedback($questionid, $contextid);
 214          $this->delete_files_in_hints($questionid, $contextid);
 215      }
 216  }