Differences Between: [Versions 311 and 400] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]
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 * Multiple choice question definition classes. 19 * 20 * @package qtype 21 * @subpackage multichoice 22 * @copyright 2009 The Open University 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 */ 25 26 27 defined('MOODLE_INTERNAL') || die(); 28 29 require_once($CFG->dirroot . '/question/type/questionbase.php'); 30 31 /** 32 * Base class for multiple choice questions. The parts that are common to 33 * single select and multiple select. 34 * 35 * @copyright 2009 The Open University 36 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 37 */ 38 abstract class qtype_multichoice_base extends question_graded_automatically { 39 const LAYOUT_DROPDOWN = 0; 40 const LAYOUT_VERTICAL = 1; 41 const LAYOUT_HORIZONTAL = 2; 42 43 public $answers; 44 45 public $shuffleanswers; 46 public $answernumbering; 47 /** 48 * @var int standard instruction to be displayed if enabled. 49 */ 50 public $showstandardinstruction = 0; 51 public $layout = self::LAYOUT_VERTICAL; 52 53 public $correctfeedback; 54 public $correctfeedbackformat; 55 public $partiallycorrectfeedback; 56 public $partiallycorrectfeedbackformat; 57 public $incorrectfeedback; 58 public $incorrectfeedbackformat; 59 60 protected $order = null; 61 62 public function start_attempt(question_attempt_step $step, $variant) { 63 $this->order = array_keys($this->answers); 64 if ($this->shuffleanswers) { 65 shuffle($this->order); 66 } 67 $step->set_qt_var('_order', implode(',', $this->order)); 68 } 69 70 public function apply_attempt_state(question_attempt_step $step) { 71 $this->order = explode(',', $step->get_qt_var('_order')); 72 73 // Add any missing answers. Sometimes people edit questions after they 74 // have been attempted which breaks things. 75 foreach ($this->order as $ansid) { 76 if (isset($this->answers[$ansid])) { 77 continue; 78 } 79 $a = new stdClass(); 80 $a->id = 0; 81 $a->answer = html_writer::span(get_string('deletedchoice', 'qtype_multichoice'), 82 'notifyproblem'); 83 $a->answerformat = FORMAT_HTML; 84 $a->fraction = 0; 85 $a->feedback = ''; 86 $a->feedbackformat = FORMAT_HTML; 87 $this->answers[$ansid] = $this->qtype->make_answer($a); 88 $this->answers[$ansid]->answerformat = FORMAT_HTML; 89 } 90 } 91 92 public function get_question_summary() { 93 $question = $this->html_to_text($this->questiontext, $this->questiontextformat); 94 $choices = array(); 95 foreach ($this->order as $ansid) { 96 $choices[] = $this->html_to_text($this->answers[$ansid]->answer, 97 $this->answers[$ansid]->answerformat); 98 } 99 return $question . ': ' . implode('; ', $choices); 100 } 101 102 public function get_order(question_attempt $qa) { 103 $this->init_order($qa); 104 return $this->order; 105 } 106 107 protected function init_order(question_attempt $qa) { 108 if (is_null($this->order)) { 109 $this->order = explode(',', $qa->get_step(0)->get_qt_var('_order')); 110 } 111 } 112 113 public abstract function get_response(question_attempt $qa); 114 115 public abstract function is_choice_selected($response, $value); 116 117 public function check_file_access($qa, $options, $component, $filearea, $args, $forcedownload) { 118 if ($component == 'question' && in_array($filearea, 119 array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback'))) { 120 return $this->check_combined_feedback_file_access($qa, $options, $filearea, $args); 121 122 } else if ($component == 'question' && $filearea == 'answer') { 123 $answerid = reset($args); // Itemid is answer id. 124 return in_array($answerid, $this->order); 125 126 } else if ($component == 'question' && $filearea == 'answerfeedback') { 127 $answerid = reset($args); // Itemid is answer id. 128 $response = $this->get_response($qa); 129 $isselected = false; 130 foreach ($this->order as $value => $ansid) { 131 if ($ansid == $answerid) { 132 $isselected = $this->is_choice_selected($response, $value); 133 break; 134 } 135 } 136 // Param $options->suppresschoicefeedback is a hack specific to the 137 // oumultiresponse question type. It would be good to refactor to 138 // avoid refering to it here. 139 return $options->feedback && empty($options->suppresschoicefeedback) && 140 $isselected; 141 142 } else if ($component == 'question' && $filearea == 'hint') { 143 return $this->check_hint_file_access($qa, $options, $args); 144 145 } else { 146 return parent::check_file_access($qa, $options, $component, $filearea, 147 $args, $forcedownload); 148 } 149 } 150 151 /** 152 * Return the question settings that define this question as structured data. 153 * 154 * @param question_attempt $qa the current attempt for which we are exporting the settings. 155 * @param question_display_options $options the question display options which say which aspects of the question 156 * should be visible. 157 * @return mixed structure representing the question settings. In web services, this will be JSON-encoded. 158 */ 159 public function get_question_definition_for_external_rendering(question_attempt $qa, question_display_options $options) { 160 // This is a partial implementation, returning only the most relevant question settings for now, 161 // ideally, we should return as much as settings as possible (depending on the state and display options). 162 163 return [ 164 'shuffleanswers' => $this->shuffleanswers, 165 'answernumbering' => $this->answernumbering, 166 'showstandardinstruction' => $this->showstandardinstruction, 167 'layout' => $this->layout, 168 ]; 169 } 170 } 171 172 173 /** 174 * Represents a multiple choice question where only one choice should be selected. 175 * 176 * @copyright 2009 The Open University 177 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 178 */ 179 class qtype_multichoice_single_question extends qtype_multichoice_base { 180 public function get_renderer(moodle_page $page) { 181 return $page->get_renderer('qtype_multichoice', 'single'); 182 } 183 184 public function get_min_fraction() { 185 $minfraction = 0; 186 foreach ($this->answers as $ans) { 187 $minfraction = min($minfraction, $ans->fraction); 188 } 189 return $minfraction; 190 } 191 192 /** 193 * Return an array of the question type variables that could be submitted 194 * as part of a question of this type, with their types, so they can be 195 * properly cleaned. 196 * @return array variable name => PARAM_... constant. 197 */ 198 public function get_expected_data() { 199 return array('answer' => PARAM_INT); 200 } 201 202 public function summarise_response(array $response) { 203 if (!$this->is_complete_response($response)) { 204 return null; 205 } 206 $ansid = $this->order[$response['answer']]; 207 return $this->html_to_text($this->answers[$ansid]->answer, 208 $this->answers[$ansid]->answerformat); 209 } 210 211 public function classify_response(array $response) { 212 if (!$this->is_complete_response($response)) { 213 return array($this->id => question_classified_response::no_response()); 214 } 215 $choiceid = $this->order[$response['answer']]; 216 $ans = $this->answers[$choiceid]; 217 return array($this->id => new question_classified_response($choiceid, 218 $this->html_to_text($ans->answer, $ans->answerformat), $ans->fraction)); 219 } 220 221 public function get_correct_response() { 222 foreach ($this->order as $key => $answerid) { 223 if (question_state::graded_state_for_fraction( 224 $this->answers[$answerid]->fraction)->is_correct()) { 225 return array('answer' => $key); 226 } 227 } 228 return array(); 229 } 230 231 public function prepare_simulated_post_data($simulatedresponse) { 232 $ansid = 0; 233 foreach ($this->answers as $answer) { 234 if (clean_param($answer->answer, PARAM_NOTAGS) == $simulatedresponse['answer']) { 235 $ansid = $answer->id; 236 } 237 } 238 if ($ansid) { 239 return array('answer' => array_search($ansid, $this->order)); 240 } else { 241 return array(); 242 } 243 } 244 245 public function get_student_response_values_for_simulation($postdata) { 246 if (!isset($postdata['answer'])) { 247 return array(); 248 } else { 249 $answer = $this->answers[$this->order[$postdata['answer']]]; 250 return array('answer' => clean_param($answer->answer, PARAM_NOTAGS)); 251 } 252 } 253 254 public function is_same_response(array $prevresponse, array $newresponse) { 255 if (!$this->is_complete_response($prevresponse)) { 256 $prevresponse = []; 257 } 258 if (!$this->is_complete_response($newresponse)) { 259 $newresponse = []; 260 } 261 return question_utils::arrays_same_at_key($prevresponse, $newresponse, 'answer'); 262 } 263 264 public function is_complete_response(array $response) { 265 return array_key_exists('answer', $response) && $response['answer'] !== '' 266 && (string) $response['answer'] !== '-1'; 267 } 268 269 public function is_gradable_response(array $response) { 270 return $this->is_complete_response($response); 271 } 272 273 public function grade_response(array $response) { 274 if (array_key_exists('answer', $response) && 275 array_key_exists($response['answer'], $this->order)) { 276 $fraction = $this->answers[$this->order[$response['answer']]]->fraction; 277 } else { 278 $fraction = 0; 279 } 280 return array($fraction, question_state::graded_state_for_fraction($fraction)); 281 } 282 283 public function get_validation_error(array $response) { 284 if ($this->is_gradable_response($response)) { 285 return ''; 286 } 287 return get_string('pleaseselectananswer', 'qtype_multichoice'); 288 } 289 290 public function get_response(question_attempt $qa) { 291 return $qa->get_last_qt_var('answer', -1); 292 } 293 294 public function is_choice_selected($response, $value) { 295 return (string) $response === (string) $value; 296 } 297 } 298 299 300 /** 301 * Represents a multiple choice question where multiple choices can be selected. 302 * 303 * @copyright 2009 The Open University 304 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 305 */ 306 class qtype_multichoice_multi_question extends qtype_multichoice_base { 307 public function get_renderer(moodle_page $page) { 308 return $page->get_renderer('qtype_multichoice', 'multi'); 309 } 310 311 public function get_min_fraction() { 312 return 0; 313 } 314 315 public function clear_wrong_from_response(array $response) { 316 foreach ($this->order as $key => $ans) { 317 if (array_key_exists($this->field($key), $response) && 318 question_state::graded_state_for_fraction( 319 $this->answers[$ans]->fraction)->is_incorrect()) { 320 $response[$this->field($key)] = 0; 321 } 322 } 323 return $response; 324 } 325 326 public function get_num_parts_right(array $response) { 327 $numright = 0; 328 foreach ($this->order as $key => $ans) { 329 $fieldname = $this->field($key); 330 if (!array_key_exists($fieldname, $response) || !$response[$fieldname]) { 331 continue; 332 } 333 334 if (!question_state::graded_state_for_fraction( 335 $this->answers[$ans]->fraction)->is_incorrect()) { 336 $numright += 1; 337 } 338 } 339 return array($numright, count($this->order)); 340 } 341 342 /** 343 * @param int $key choice number 344 * @return string the question-type variable name. 345 */ 346 protected function field($key) { 347 return 'choice' . $key; 348 } 349 350 public function get_expected_data() { 351 $expected = array(); 352 foreach ($this->order as $key => $notused) { 353 $expected[$this->field($key)] = PARAM_BOOL; 354 } 355 return $expected; 356 } 357 358 public function summarise_response(array $response) { 359 $selectedchoices = array(); 360 foreach ($this->order as $key => $ans) { 361 $fieldname = $this->field($key); 362 if (array_key_exists($fieldname, $response) && $response[$fieldname]) { 363 $selectedchoices[] = $this->html_to_text($this->answers[$ans]->answer, 364 $this->answers[$ans]->answerformat); 365 } 366 } 367 if (empty($selectedchoices)) { 368 return null; 369 } 370 return implode('; ', $selectedchoices); 371 } 372 373 public function classify_response(array $response) { 374 $selectedchoices = array(); 375 foreach ($this->order as $key => $ansid) { 376 $fieldname = $this->field($key); 377 if (array_key_exists($fieldname, $response) && $response[$fieldname]) { 378 $selectedchoices[$ansid] = 1; 379 } 380 } 381 $choices = array(); 382 foreach ($this->answers as $ansid => $ans) { 383 if (isset($selectedchoices[$ansid])) { 384 $choices[$ansid] = new question_classified_response($ansid, 385 $this->html_to_text($ans->answer, $ans->answerformat), $ans->fraction); 386 } 387 } 388 return $choices; 389 } 390 391 public function get_correct_response() { 392 $response = array(); 393 foreach ($this->order as $key => $ans) { 394 if (!question_state::graded_state_for_fraction( 395 $this->answers[$ans]->fraction)->is_incorrect()) { 396 $response[$this->field($key)] = 1; 397 } 398 } 399 return $response; 400 } 401 402 public function prepare_simulated_post_data($simulatedresponse) { 403 $postdata = array(); 404 foreach ($simulatedresponse as $ans => $checked) { 405 foreach ($this->answers as $ansid => $answer) { 406 if (clean_param($answer->answer, PARAM_NOTAGS) == $ans) { 407 $fieldno = array_search($ansid, $this->order); 408 $postdata[$this->field($fieldno)] = $checked; 409 break; 410 } 411 } 412 } 413 return $postdata; 414 } 415 416 public function get_student_response_values_for_simulation($postdata) { 417 $simulatedresponse = array(); 418 foreach ($this->order as $fieldno => $ansid) { 419 if (isset($postdata[$this->field($fieldno)])) { 420 $checked = $postdata[$this->field($fieldno)]; 421 $simulatedresponse[clean_param($this->answers[$ansid]->answer, PARAM_NOTAGS)] = $checked; 422 } 423 } 424 ksort($simulatedresponse); 425 return $simulatedresponse; 426 } 427 428 public function is_same_response(array $prevresponse, array $newresponse) { 429 foreach ($this->order as $key => $notused) { 430 $fieldname = $this->field($key); 431 if (!question_utils::arrays_same_at_key_integer($prevresponse, $newresponse, $fieldname)) { 432 return false; 433 } 434 } 435 return true; 436 } 437 438 public function is_complete_response(array $response) { 439 foreach ($this->order as $key => $notused) { 440 if (!empty($response[$this->field($key)])) { 441 return true; 442 } 443 } 444 return false; 445 } 446 447 public function is_gradable_response(array $response) { 448 return $this->is_complete_response($response); 449 } 450 451 /** 452 * @param array $response responses, as returned by 453 * {@link question_attempt_step::get_qt_data()}. 454 * @return int the number of choices that were selected. in this response. 455 */ 456 public function get_num_selected_choices(array $response) { 457 $numselected = 0; 458 foreach ($response as $key => $value) { 459 // Response keys starting with _ are internal values like _order, so ignore them. 460 if (!empty($value) && $key[0] != '_') { 461 $numselected += 1; 462 } 463 } 464 return $numselected; 465 } 466 467 /** 468 * @return int the number of choices that are correct. 469 */ 470 public function get_num_correct_choices() { 471 $numcorrect = 0; 472 foreach ($this->answers as $ans) { 473 if (!question_state::graded_state_for_fraction($ans->fraction)->is_incorrect()) { 474 $numcorrect += 1; 475 } 476 } 477 return $numcorrect; 478 } 479 480 public function grade_response(array $response) { 481 $fraction = 0; 482 foreach ($this->order as $key => $ansid) { 483 if (!empty($response[$this->field($key)])) { 484 $fraction += $this->answers[$ansid]->fraction; 485 } 486 } 487 $fraction = min(max(0, $fraction), 1.0); 488 return array($fraction, question_state::graded_state_for_fraction($fraction)); 489 } 490 491 public function get_validation_error(array $response) { 492 if ($this->is_gradable_response($response)) { 493 return ''; 494 } 495 return get_string('pleaseselectatleastoneanswer', 'qtype_multichoice'); 496 } 497 498 /** 499 * Disable those hint settings that we don't want when the student has selected 500 * more choices than the number of right choices. This avoids giving the game away. 501 * @param question_hint_with_parts $hint a hint. 502 */ 503 protected function disable_hint_settings_when_too_many_selected( 504 question_hint_with_parts $hint) { 505 $hint->clearwrong = false; 506 } 507 508 public function get_hint($hintnumber, question_attempt $qa) { 509 $hint = parent::get_hint($hintnumber, $qa); 510 if (is_null($hint)) { 511 return $hint; 512 } 513 514 if ($this->get_num_selected_choices($qa->get_last_qt_data()) > 515 $this->get_num_correct_choices()) { 516 $hint = clone($hint); 517 $this->disable_hint_settings_when_too_many_selected($hint); 518 } 519 return $hint; 520 } 521 522 public function get_response(question_attempt $qa) { 523 return $qa->get_last_qt_data(); 524 } 525 526 public function is_choice_selected($response, $value) { 527 return !empty($response['choice' . $value]); 528 } 529 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body