Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 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   * The questiontype class for the multiple choice question type.
  19   *
  20   * @package    qtype
  21   * @subpackage multichoice
  22   * @copyright  1999 onwards Martin Dougiamas  {@link http://moodle.com}
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  
  27  defined('MOODLE_INTERNAL') || die();
  28  
  29  global $CFG;
  30  require_once($CFG->libdir . '/questionlib.php');
  31  
  32  
  33  /**
  34   * The multiple choice question type.
  35   *
  36   * @copyright  1999 onwards Martin Dougiamas  {@link http://moodle.com}
  37   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  38   */
  39  class qtype_multichoice extends question_type {
  40      public function get_question_options($question) {
  41          global $DB, $OUTPUT;
  42  
  43          $question->options = $DB->get_record('qtype_multichoice_options', ['questionid' => $question->id]);
  44  
  45          if ($question->options === false) {
  46              // If this has happened, then we have a problem.
  47              // For the user to be able to edit or delete this question, we need options.
  48              debugging("Question ID {$question->id} was missing an options record. Using default.", DEBUG_DEVELOPER);
  49  
  50              $question->options = $this->create_default_options($question);
  51          }
  52  
  53          parent::get_question_options($question);
  54      }
  55  
  56      /**
  57       * Create a default options object for the provided question.
  58       *
  59       * @param object $question The queston we are working with.
  60       * @return object The options object.
  61       */
  62      protected function create_default_options($question) {
  63          // Create a default question options record.
  64          $options = new stdClass();
  65          $options->questionid = $question->id;
  66  
  67          // Get the default strings and just set the format.
  68          $options->correctfeedback = get_string('correctfeedbackdefault', 'question');
  69          $options->correctfeedbackformat = FORMAT_HTML;
  70          $options->partiallycorrectfeedback = get_string('partiallycorrectfeedbackdefault', 'question');;
  71          $options->partiallycorrectfeedbackformat = FORMAT_HTML;
  72          $options->incorrectfeedback = get_string('incorrectfeedbackdefault', 'question');
  73          $options->incorrectfeedbackformat = FORMAT_HTML;
  74  
  75          $config = get_config('qtype_multichoice');
  76          $options->single = $config->answerhowmany;
  77          if (isset($question->layout)) {
  78              $options->layout = $question->layout;
  79          }
  80          $options->answernumbering = $config->answernumbering;
  81          $options->shuffleanswers = $config->shuffleanswers;
  82          $options->showstandardinstruction = 0;
  83          $options->shownumcorrect = 1;
  84  
  85          return $options;
  86      }
  87  
  88      public function save_question_options($question) {
  89          global $DB;
  90          $context = $question->context;
  91          $result = new stdClass();
  92  
  93          $oldanswers = $DB->get_records('question_answers',
  94                  array('question' => $question->id), 'id ASC');
  95  
  96          // Following hack to check at least two answers exist.
  97          $answercount = 0;
  98          foreach ($question->answer as $key => $answer) {
  99              if ($answer != '') {
 100                  $answercount++;
 101              }
 102          }
 103          if ($answercount < 2) { // Check there are at lest 2 answers for multiple choice.
 104              $result->error = get_string('notenoughanswers', 'qtype_multichoice', '2');
 105              return $result;
 106          }
 107  
 108          // Insert all the new answers.
 109          $totalfraction = 0;
 110          $maxfraction = -1;
 111          foreach ($question->answer as $key => $answerdata) {
 112              if (trim($answerdata['text']) == '') {
 113                  continue;
 114              }
 115  
 116              // Update an existing answer if possible.
 117              $answer = array_shift($oldanswers);
 118              if (!$answer) {
 119                  $answer = new stdClass();
 120                  $answer->question = $question->id;
 121                  $answer->answer = '';
 122                  $answer->feedback = '';
 123                  $answer->id = $DB->insert_record('question_answers', $answer);
 124              }
 125  
 126              // Doing an import.
 127              $answer->answer = $this->import_or_save_files($answerdata,
 128                      $context, 'question', 'answer', $answer->id);
 129              $answer->answerformat = $answerdata['format'];
 130              $answer->fraction = $question->fraction[$key];
 131              $answer->feedback = $this->import_or_save_files($question->feedback[$key],
 132                      $context, 'question', 'answerfeedback', $answer->id);
 133              $answer->feedbackformat = $question->feedback[$key]['format'];
 134  
 135              $DB->update_record('question_answers', $answer);
 136  
 137              if ($question->fraction[$key] > 0) {
 138                  $totalfraction += $question->fraction[$key];
 139              }
 140              if ($question->fraction[$key] > $maxfraction) {
 141                  $maxfraction = $question->fraction[$key];
 142              }
 143          }
 144  
 145          // Delete any left over old answer records.
 146          $fs = get_file_storage();
 147          foreach ($oldanswers as $oldanswer) {
 148              $fs->delete_area_files($context->id, 'question', 'answerfeedback', $oldanswer->id);
 149              $DB->delete_records('question_answers', array('id' => $oldanswer->id));
 150          }
 151  
 152          $options = $DB->get_record('qtype_multichoice_options', array('questionid' => $question->id));
 153          if (!$options) {
 154              $options = new stdClass();
 155              $options->questionid = $question->id;
 156              $options->correctfeedback = '';
 157              $options->partiallycorrectfeedback = '';
 158              $options->incorrectfeedback = '';
 159              $options->showstandardinstruction = 0;
 160              $options->id = $DB->insert_record('qtype_multichoice_options', $options);
 161          }
 162  
 163          $options->single = $question->single;
 164          if (isset($question->layout)) {
 165              $options->layout = $question->layout;
 166          }
 167          $options->answernumbering = $question->answernumbering;
 168          $options->shuffleanswers = $question->shuffleanswers;
 169          $options->showstandardinstruction = !empty($question->showstandardinstruction);
 170          $options = $this->save_combined_feedback_helper($options, $question, $context, true);
 171          $DB->update_record('qtype_multichoice_options', $options);
 172  
 173          $this->save_hints($question, true);
 174  
 175          // Perform sanity checks on fractional grades.
 176          if ($options->single) {
 177              if ($maxfraction != 1) {
 178                  $result->noticeyesno = get_string('fractionsnomax', 'qtype_multichoice',
 179                          $maxfraction * 100);
 180                  return $result;
 181              }
 182          } else {
 183              $totalfraction = round($totalfraction, 2);
 184              if ($totalfraction != 1) {
 185                  $result->noticeyesno = get_string('fractionsaddwrong', 'qtype_multichoice',
 186                          $totalfraction * 100);
 187                  return $result;
 188              }
 189          }
 190      }
 191  
 192      protected function make_question_instance($questiondata) {
 193          question_bank::load_question_definition_classes($this->name());
 194          if ($questiondata->options->single) {
 195              $class = 'qtype_multichoice_single_question';
 196          } else {
 197              $class = 'qtype_multichoice_multi_question';
 198          }
 199          return new $class();
 200      }
 201  
 202      protected function make_hint($hint) {
 203          return question_hint_with_parts::load_from_record($hint);
 204      }
 205  
 206      protected function initialise_question_instance(question_definition $question, $questiondata) {
 207          parent::initialise_question_instance($question, $questiondata);
 208          $question->shuffleanswers = $questiondata->options->shuffleanswers;
 209          $question->answernumbering = $questiondata->options->answernumbering;
 210          $question->showstandardinstruction = $questiondata->options->showstandardinstruction;
 211          if (!empty($questiondata->options->layout)) {
 212              $question->layout = $questiondata->options->layout;
 213          } else {
 214              $question->layout = qtype_multichoice_single_question::LAYOUT_VERTICAL;
 215          }
 216          $this->initialise_combined_feedback($question, $questiondata, true);
 217  
 218          $this->initialise_question_answers($question, $questiondata, false);
 219      }
 220  
 221      public function make_answer($answer) {
 222          // Overridden just so we can make it public for use by question.php.
 223          return parent::make_answer($answer);
 224      }
 225  
 226      public function delete_question($questionid, $contextid) {
 227          global $DB;
 228          $DB->delete_records('qtype_multichoice_options', array('questionid' => $questionid));
 229  
 230          parent::delete_question($questionid, $contextid);
 231      }
 232  
 233      public function get_random_guess_score($questiondata) {
 234          if (!$questiondata->options->single) {
 235              // Pretty much impossible to compute for _multi questions. Don't try.
 236              return null;
 237          }
 238  
 239          // Single choice questions - average choice fraction.
 240          $totalfraction = 0;
 241          foreach ($questiondata->options->answers as $answer) {
 242              $totalfraction += $answer->fraction;
 243          }
 244          return $totalfraction / count($questiondata->options->answers);
 245      }
 246  
 247      public function get_possible_responses($questiondata) {
 248          if ($questiondata->options->single) {
 249              $responses = array();
 250  
 251              foreach ($questiondata->options->answers as $aid => $answer) {
 252                  $responses[$aid] = new question_possible_response(
 253                          question_utils::to_plain_text($answer->answer, $answer->answerformat),
 254                          $answer->fraction);
 255              }
 256  
 257              $responses[null] = question_possible_response::no_response();
 258              return array($questiondata->id => $responses);
 259          } else {
 260              $parts = array();
 261  
 262              foreach ($questiondata->options->answers as $aid => $answer) {
 263                  $parts[$aid] = array($aid => new question_possible_response(
 264                          question_utils::to_plain_text($answer->answer, $answer->answerformat),
 265                          $answer->fraction));
 266              }
 267  
 268              return $parts;
 269          }
 270      }
 271  
 272      /**
 273       * @return array of the numbering styles supported. For each one, there
 274       *      should be a lang string answernumberingxxx in teh qtype_multichoice
 275       *      language file, and a case in the switch statement in number_in_style,
 276       *      and it should be listed in the definition of this column in install.xml.
 277       */
 278      public static function get_numbering_styles() {
 279          $styles = array();
 280          foreach (array('abc', 'ABCD', '123', 'iii', 'IIII', 'none') as $numberingoption) {
 281              $styles[$numberingoption] =
 282                      get_string('answernumbering' . $numberingoption, 'qtype_multichoice');
 283          }
 284          return $styles;
 285      }
 286  
 287      public function move_files($questionid, $oldcontextid, $newcontextid) {
 288          parent::move_files($questionid, $oldcontextid, $newcontextid);
 289          $this->move_files_in_answers($questionid, $oldcontextid, $newcontextid, true);
 290          $this->move_files_in_combined_feedback($questionid, $oldcontextid, $newcontextid);
 291          $this->move_files_in_hints($questionid, $oldcontextid, $newcontextid);
 292      }
 293  
 294      protected function delete_files($questionid, $contextid) {
 295          parent::delete_files($questionid, $contextid);
 296          $this->delete_files_in_answers($questionid, $contextid, true);
 297          $this->delete_files_in_combined_feedback($questionid, $contextid);
 298          $this->delete_files_in_hints($questionid, $contextid);
 299      }
 300  }