See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401]
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_defaults_for_new_questions(stdClass $fromform): void { 89 parent::save_defaults_for_new_questions($fromform); 90 $this->set_default_value('single', $fromform->single); 91 $this->set_default_value('shuffleanswers', $fromform->shuffleanswers); 92 $this->set_default_value('answernumbering', $fromform->answernumbering); 93 $this->set_default_value('showstandardinstruction', $fromform->showstandardinstruction); 94 } 95 96 public function save_question_options($question) { 97 global $DB; 98 $context = $question->context; 99 $result = new stdClass(); 100 101 $oldanswers = $DB->get_records('question_answers', 102 array('question' => $question->id), 'id ASC'); 103 104 // Following hack to check at least two answers exist. 105 $answercount = 0; 106 foreach ($question->answer as $key => $answer) { 107 if ($answer != '') { 108 $answercount++; 109 } 110 } 111 if ($answercount < 2) { // Check there are at lest 2 answers for multiple choice. 112 $result->error = get_string('notenoughanswers', 'qtype_multichoice', '2'); 113 return $result; 114 } 115 116 // Insert all the new answers. 117 $totalfraction = 0; 118 $maxfraction = -1; 119 foreach ($question->answer as $key => $answerdata) { 120 if (trim($answerdata['text']) == '') { 121 continue; 122 } 123 124 // Update an existing answer if possible. 125 $answer = array_shift($oldanswers); 126 if (!$answer) { 127 $answer = new stdClass(); 128 $answer->question = $question->id; 129 $answer->answer = ''; 130 $answer->feedback = ''; 131 $answer->id = $DB->insert_record('question_answers', $answer); 132 } 133 134 // Doing an import. 135 $answer->answer = $this->import_or_save_files($answerdata, 136 $context, 'question', 'answer', $answer->id); 137 $answer->answerformat = $answerdata['format']; 138 $answer->fraction = $question->fraction[$key]; 139 $answer->feedback = $this->import_or_save_files($question->feedback[$key], 140 $context, 'question', 'answerfeedback', $answer->id); 141 $answer->feedbackformat = $question->feedback[$key]['format']; 142 143 $DB->update_record('question_answers', $answer); 144 145 if ($question->fraction[$key] > 0) { 146 $totalfraction += $question->fraction[$key]; 147 } 148 if ($question->fraction[$key] > $maxfraction) { 149 $maxfraction = $question->fraction[$key]; 150 } 151 } 152 153 // Delete any left over old answer records. 154 $fs = get_file_storage(); 155 foreach ($oldanswers as $oldanswer) { 156 $fs->delete_area_files($context->id, 'question', 'answerfeedback', $oldanswer->id); 157 $DB->delete_records('question_answers', array('id' => $oldanswer->id)); 158 } 159 160 $options = $DB->get_record('qtype_multichoice_options', array('questionid' => $question->id)); 161 if (!$options) { 162 $options = new stdClass(); 163 $options->questionid = $question->id; 164 $options->correctfeedback = ''; 165 $options->partiallycorrectfeedback = ''; 166 $options->incorrectfeedback = ''; 167 $options->showstandardinstruction = 0; 168 $options->id = $DB->insert_record('qtype_multichoice_options', $options); 169 } 170 171 $options->single = $question->single; 172 if (isset($question->layout)) { 173 $options->layout = $question->layout; 174 } 175 $options->answernumbering = $question->answernumbering; 176 $options->shuffleanswers = $question->shuffleanswers; 177 $options->showstandardinstruction = !empty($question->showstandardinstruction); 178 $options = $this->save_combined_feedback_helper($options, $question, $context, true); 179 $DB->update_record('qtype_multichoice_options', $options); 180 181 $this->save_hints($question, true); 182 183 // Perform sanity checks on fractional grades. 184 if ($options->single) { 185 if ($maxfraction != 1) { 186 $result->noticeyesno = get_string('fractionsnomax', 'qtype_multichoice', 187 $maxfraction * 100); 188 return $result; 189 } 190 } else { 191 $totalfraction = round($totalfraction, 2); 192 if ($totalfraction != 1) { 193 $result->noticeyesno = get_string('fractionsaddwrong', 'qtype_multichoice', 194 $totalfraction * 100); 195 return $result; 196 } 197 } 198 } 199 200 protected function make_question_instance($questiondata) { 201 question_bank::load_question_definition_classes($this->name()); 202 if ($questiondata->options->single) { 203 $class = 'qtype_multichoice_single_question'; 204 } else { 205 $class = 'qtype_multichoice_multi_question'; 206 } 207 return new $class(); 208 } 209 210 protected function make_hint($hint) { 211 return question_hint_with_parts::load_from_record($hint); 212 } 213 214 protected function initialise_question_instance(question_definition $question, $questiondata) { 215 parent::initialise_question_instance($question, $questiondata); 216 $question->shuffleanswers = $questiondata->options->shuffleanswers; 217 $question->answernumbering = $questiondata->options->answernumbering; 218 $question->showstandardinstruction = $questiondata->options->showstandardinstruction; 219 if (!empty($questiondata->options->layout)) { 220 $question->layout = $questiondata->options->layout; 221 } else { 222 $question->layout = qtype_multichoice_single_question::LAYOUT_VERTICAL; 223 } 224 $this->initialise_combined_feedback($question, $questiondata, true); 225 226 $this->initialise_question_answers($question, $questiondata, false); 227 } 228 229 public function make_answer($answer) { 230 // Overridden just so we can make it public for use by question.php. 231 return parent::make_answer($answer); 232 } 233 234 public function delete_question($questionid, $contextid) { 235 global $DB; 236 $DB->delete_records('qtype_multichoice_options', array('questionid' => $questionid)); 237 238 parent::delete_question($questionid, $contextid); 239 } 240 241 public function get_random_guess_score($questiondata) { 242 if (!$questiondata->options->single) { 243 // Pretty much impossible to compute for _multi questions. Don't try. 244 return null; 245 } 246 247 if (empty($questiondata->options->answers)) { 248 // A multi-choice question with no choices is senseless, 249 // but, seemingly, it can happen (presumably as a side-effect of bugs). 250 // Therefore, ensure it does not lead to errors here. 251 return null; 252 } 253 254 // Single choice questions - average choice fraction. 255 $totalfraction = 0; 256 foreach ($questiondata->options->answers as $answer) { 257 $totalfraction += $answer->fraction; 258 } 259 return $totalfraction / count($questiondata->options->answers); 260 } 261 262 public function get_possible_responses($questiondata) { 263 if ($questiondata->options->single) { 264 $responses = array(); 265 266 foreach ($questiondata->options->answers as $aid => $answer) { 267 $responses[$aid] = new question_possible_response( 268 question_utils::to_plain_text($answer->answer, $answer->answerformat), 269 $answer->fraction); 270 } 271 272 $responses[null] = question_possible_response::no_response(); 273 return array($questiondata->id => $responses); 274 } else { 275 $parts = array(); 276 277 foreach ($questiondata->options->answers as $aid => $answer) { 278 $parts[$aid] = array($aid => new question_possible_response( 279 question_utils::to_plain_text($answer->answer, $answer->answerformat), 280 $answer->fraction)); 281 } 282 283 return $parts; 284 } 285 } 286 287 /** 288 * @return array of the numbering styles supported. For each one, there 289 * should be a lang string answernumberingxxx in teh qtype_multichoice 290 * language file, and a case in the switch statement in number_in_style, 291 * and it should be listed in the definition of this column in install.xml. 292 */ 293 public static function get_numbering_styles() { 294 $styles = array(); 295 foreach (array('abc', 'ABCD', '123', 'iii', 'IIII', 'none') as $numberingoption) { 296 $styles[$numberingoption] = 297 get_string('answernumbering' . $numberingoption, 'qtype_multichoice'); 298 } 299 return $styles; 300 } 301 302 public function move_files($questionid, $oldcontextid, $newcontextid) { 303 parent::move_files($questionid, $oldcontextid, $newcontextid); 304 $this->move_files_in_answers($questionid, $oldcontextid, $newcontextid, true); 305 $this->move_files_in_combined_feedback($questionid, $oldcontextid, $newcontextid); 306 $this->move_files_in_hints($questionid, $oldcontextid, $newcontextid); 307 } 308 309 protected function delete_files($questionid, $contextid) { 310 parent::delete_files($questionid, $contextid); 311 $this->delete_files_in_answers($questionid, $contextid, true); 312 $this->delete_files_in_combined_feedback($questionid, $contextid); 313 $this->delete_files_in_hints($questionid, $contextid); 314 } 315 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body