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 /** 19 * Multianswer question definition class. 20 * 21 * @package qtype 22 * @subpackage multianswer 23 * @copyright 2010 Pierre Pichet 24 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 25 */ 26 27 require_once($CFG->dirroot . '/question/type/questionbase.php'); 28 require_once($CFG->dirroot . '/question/type/shortanswer/question.php'); 29 require_once($CFG->dirroot . '/question/type/numerical/question.php'); 30 require_once($CFG->dirroot . '/question/type/multichoice/question.php'); 31 32 33 /** 34 * Represents a multianswer question. 35 * 36 * A multi-answer question is made of of several subquestions of various types. 37 * You can think of it as an application of the composite pattern to qusetion 38 * types. 39 * 40 * @copyright 2010 Pierre Pichet 41 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 42 */ 43 class qtype_multianswer_question extends question_graded_automatically_with_countback { 44 /** @var array of question_graded_automatically. */ 45 public $subquestions = array(); 46 47 /** 48 * @var array place number => insex in the $subquestions array. Places are 49 * numbered from 1. 50 */ 51 public $places; 52 53 /** 54 * @var array of strings, one longer than $places, which is achieved by 55 * indexing from 0. The bits of question text that go between the subquestions. 56 */ 57 public $textfragments; 58 59 /** 60 * Get a question_attempt_step_subquestion_adapter 61 * @param question_attempt_step $step the step to adapt. 62 * @param int $i the subquestion index. 63 * @return question_attempt_step_subquestion_adapter. 64 */ 65 protected function get_substep($step, $i) { 66 return new question_attempt_step_subquestion_adapter($step, 'sub' . $i . '_'); 67 } 68 69 public function start_attempt(question_attempt_step $step, $variant) { 70 foreach ($this->subquestions as $i => $subq) { 71 $subq->start_attempt($this->get_substep($step, $i), $variant); 72 } 73 } 74 75 public function apply_attempt_state(question_attempt_step $step) { 76 foreach ($this->subquestions as $i => $subq) { 77 $subq->apply_attempt_state($this->get_substep($step, $i)); 78 } 79 } 80 81 public function validate_can_regrade_with_other_version(question_definition $otherversion): ?string { 82 $basemessage = parent::validate_can_regrade_with_other_version($otherversion); 83 if ($basemessage) { 84 return $basemessage; 85 } 86 87 if (count($this->subquestions) != count($otherversion->subquestions)) { 88 return get_string('regradeissuenumsubquestionschanged', 'qtype_multianswer'); 89 } 90 91 foreach ($this->subquestions as $i => $subq) { 92 $subqmessage = $subq->validate_can_regrade_with_other_version($otherversion->subquestions[$i]); 93 if ($subqmessage) { 94 return $subqmessage; 95 } 96 } 97 98 return null; 99 } 100 101 public function update_attempt_state_data_for_new_version( 102 question_attempt_step $oldstep, question_definition $oldquestion) { 103 parent::update_attempt_state_data_for_new_version($oldstep, $oldquestion); 104 105 $result = []; 106 foreach ($this->subquestions as $i => $subq) { 107 $substep = $this->get_substep($oldstep, $i); 108 $statedata = $subq->update_attempt_state_data_for_new_version( 109 $substep, $oldquestion->subquestions[$i]); 110 foreach ($statedata as $name => $value) { 111 $result[$substep->add_prefix($name)] = $value; 112 } 113 } 114 115 return $result; 116 } 117 118 public function get_question_summary() { 119 $summary = $this->html_to_text($this->questiontext, $this->questiontextformat); 120 foreach ($this->subquestions as $i => $subq) { 121 switch ($subq->qtype->name()) { 122 case 'multichoice': 123 $choices = array(); 124 $dummyqa = new question_attempt($subq, $this->contextid); 125 foreach ($subq->get_order($dummyqa) as $ansid) { 126 $choices[] = $this->html_to_text($subq->answers[$ansid]->answer, 127 $subq->answers[$ansid]->answerformat); 128 } 129 $answerbit = '{' . implode('; ', $choices) . '}'; 130 break; 131 case 'numerical': 132 case 'shortanswer': 133 $answerbit = '_____'; 134 break; 135 default: 136 $answerbit = '{ERR unknown sub-question type}'; 137 } 138 $summary = str_replace('{#' . $i . '}', $answerbit, $summary); 139 } 140 return $summary; 141 } 142 143 public function get_min_fraction() { 144 $fractionsum = 0; 145 $fractionmax = 0; 146 foreach ($this->subquestions as $i => $subq) { 147 $fractionmax += $subq->defaultmark; 148 $fractionsum += $subq->defaultmark * $subq->get_min_fraction(); 149 } 150 if (empty($fractionsum)) { 151 return 0; 152 } 153 return $fractionsum / (!empty($this->subquestions) ? $fractionmax : 1); 154 } 155 156 public function get_max_fraction() { 157 $fractionsum = 0; 158 $fractionmax = 0; 159 foreach ($this->subquestions as $i => $subq) { 160 $fractionmax += $subq->defaultmark; 161 $fractionsum += $subq->defaultmark * $subq->get_max_fraction(); 162 } 163 if (empty($fractionsum)) { 164 return 1; 165 } 166 return $fractionsum / (!empty($this->subquestions) ? $fractionmax : 1); 167 } 168 169 public function get_expected_data() { 170 $expected = array(); 171 foreach ($this->subquestions as $i => $subq) { 172 $substep = $this->get_substep(null, $i); 173 foreach ($subq->get_expected_data() as $name => $type) { 174 if ($subq->qtype->name() == 'multichoice' && 175 $subq->layout == qtype_multichoice_base::LAYOUT_DROPDOWN) { 176 // Hack or MC inline does not work. 177 $expected[$substep->add_prefix($name)] = PARAM_RAW; 178 } else { 179 $expected[$substep->add_prefix($name)] = $type; 180 } 181 } 182 } 183 return $expected; 184 } 185 186 public function get_correct_response() { 187 $right = array(); 188 foreach ($this->subquestions as $i => $subq) { 189 $substep = $this->get_substep(null, $i); 190 foreach ($subq->get_correct_response() as $name => $type) { 191 $right[$substep->add_prefix($name)] = $type; 192 } 193 } 194 return $right; 195 } 196 197 public function prepare_simulated_post_data($simulatedresponse) { 198 $postdata = array(); 199 foreach ($this->subquestions as $i => $subq) { 200 $substep = $this->get_substep(null, $i); 201 foreach ($subq->prepare_simulated_post_data($simulatedresponse[$i]) as $name => $value) { 202 $postdata[$substep->add_prefix($name)] = $value; 203 } 204 } 205 return $postdata; 206 } 207 208 public function get_student_response_values_for_simulation($postdata) { 209 $simulatedresponse = array(); 210 foreach ($this->subquestions as $i => $subq) { 211 $substep = $this->get_substep(null, $i); 212 $subqpostdata = $substep->filter_array($postdata); 213 $subqsimulatedresponse = $subq->get_student_response_values_for_simulation($subqpostdata); 214 foreach ($subqsimulatedresponse as $subresponsekey => $responsevalue) { 215 $simulatedresponse[$i.'.'.$subresponsekey] = $responsevalue; 216 } 217 } 218 ksort($simulatedresponse); 219 return $simulatedresponse; 220 } 221 222 public function is_complete_response(array $response) { 223 foreach ($this->subquestions as $i => $subq) { 224 $substep = $this->get_substep(null, $i); 225 if (!$subq->is_complete_response($substep->filter_array($response))) { 226 return false; 227 } 228 } 229 return true; 230 } 231 232 public function is_gradable_response(array $response) { 233 foreach ($this->subquestions as $i => $subq) { 234 $substep = $this->get_substep(null, $i); 235 if ($subq->is_gradable_response($substep->filter_array($response))) { 236 return true; 237 } 238 } 239 return false; 240 } 241 242 public function is_same_response(array $prevresponse, array $newresponse) { 243 foreach ($this->subquestions as $i => $subq) { 244 $substep = $this->get_substep(null, $i); 245 if (!$subq->is_same_response($substep->filter_array($prevresponse), 246 $substep->filter_array($newresponse))) { 247 return false; 248 } 249 } 250 return true; 251 } 252 253 public function get_validation_error(array $response) { 254 if ($this->is_complete_response($response)) { 255 return ''; 256 } 257 return get_string('pleaseananswerallparts', 'qtype_multianswer'); 258 } 259 260 /** 261 * Used by grade_response to combine the states of the subquestions. 262 * The combined state is accumulates in $overallstate. That will be right 263 * if all the separate states are right; and wrong if all the separate states 264 * are wrong, otherwise, it will be partially right. 265 * @param question_state $overallstate the result so far. 266 * @param question_state $newstate the new state to add to the combination. 267 * @return question_state the new combined state. 268 */ 269 protected function combine_states($overallstate, $newstate) { 270 if (is_null($overallstate)) { 271 return $newstate; 272 } else if ($overallstate == question_state::$gaveup && 273 $newstate == question_state::$gaveup) { 274 return question_state::$gaveup; 275 } else if ($overallstate == question_state::$gaveup && 276 $newstate == question_state::$gradedwrong) { 277 return question_state::$gradedwrong; 278 } else if ($overallstate == question_state::$gradedwrong && 279 $newstate == question_state::$gaveup) { 280 return question_state::$gradedwrong; 281 } else if ($overallstate == question_state::$gradedwrong && 282 $newstate == question_state::$gradedwrong) { 283 return question_state::$gradedwrong; 284 } else if ($overallstate == question_state::$gradedright && 285 $newstate == question_state::$gradedright) { 286 return question_state::$gradedright; 287 } else { 288 return question_state::$gradedpartial; 289 } 290 } 291 292 public function grade_response(array $response) { 293 $overallstate = null; 294 $fractionsum = 0; 295 $fractionmax = 0; 296 foreach ($this->subquestions as $i => $subq) { 297 $fractionmax += $subq->defaultmark; 298 $substep = $this->get_substep(null, $i); 299 $subresp = $substep->filter_array($response); 300 if (!$subq->is_gradable_response($subresp)) { 301 $overallstate = $this->combine_states($overallstate, question_state::$gaveup); 302 } else { 303 list($subfraction, $newstate) = $subq->grade_response($subresp); 304 $fractionsum += $subfraction * $subq->defaultmark; 305 $overallstate = $this->combine_states($overallstate, $newstate); 306 } 307 } 308 if (empty($fractionmax)) { 309 return array(null, $overallstate ?? question_state::$finished); 310 } 311 return array($fractionsum / $fractionmax, $overallstate); 312 } 313 314 public function clear_wrong_from_response(array $response) { 315 foreach ($this->subquestions as $i => $subq) { 316 $substep = $this->get_substep(null, $i); 317 $subresp = $substep->filter_array($response); 318 list($subfraction, $newstate) = $subq->grade_response($subresp); 319 if ($newstate != question_state::$gradedright) { 320 foreach ($subresp as $ind => $resp) { 321 if ($subq->qtype == 'multichoice' && ($subq->layout == qtype_multichoice_base::LAYOUT_VERTICAL 322 || $subq->layout == qtype_multichoice_base::LAYOUT_HORIZONTAL)) { 323 $response[$substep->add_prefix($ind)] = '-1'; 324 } else { 325 $response[$substep->add_prefix($ind)] = ''; 326 } 327 } 328 } 329 } 330 return $response; 331 } 332 333 public function get_num_parts_right(array $response) { 334 $numright = 0; 335 foreach ($this->subquestions as $i => $subq) { 336 $substep = $this->get_substep(null, $i); 337 $subresp = $substep->filter_array($response); 338 list($subfraction, $newstate) = $subq->grade_response($subresp); 339 if ($newstate == question_state::$gradedright) { 340 $numright += 1; 341 } 342 } 343 return array($numright, count($this->subquestions)); 344 } 345 346 public function compute_final_grade($responses, $totaltries) { 347 $fractionsum = 0; 348 $fractionmax = 0; 349 foreach ($this->subquestions as $i => $subq) { 350 $fractionmax += $subq->defaultmark; 351 352 $lastresponse = array(); 353 $lastchange = 0; 354 $subfraction = 0; 355 foreach ($responses as $responseindex => $response) { 356 $substep = $this->get_substep(null, $i); 357 $subresp = $substep->filter_array($response); 358 if ($subq->is_same_response($lastresponse, $subresp)) { 359 continue; 360 } 361 $lastresponse = $subresp; 362 $lastchange = $responseindex; 363 list($subfraction, $newstate) = $subq->grade_response($subresp); 364 } 365 366 $fractionsum += $subq->defaultmark * max(0, $subfraction - $lastchange * $this->penalty); 367 } 368 369 return $fractionsum / $fractionmax; 370 } 371 372 public function summarise_response(array $response) { 373 $summary = array(); 374 foreach ($this->subquestions as $i => $subq) { 375 $substep = $this->get_substep(null, $i); 376 $a = new stdClass(); 377 $a->i = $i; 378 $a->response = $subq->summarise_response($substep->filter_array($response)); 379 $summary[] = get_string('subqresponse', 'qtype_multianswer', $a); 380 } 381 382 return implode('; ', $summary); 383 } 384 385 public function check_file_access($qa, $options, $component, $filearea, $args, $forcedownload) { 386 if ($component == 'question' && $filearea == 'answer') { 387 return true; 388 389 } else if ($component == 'question' && $filearea == 'answerfeedback') { 390 // Full logic to control which feedbacks a student can see is too complex. 391 // Just allow access to all images. There is a theoretical chance the 392 // students could see files they are not meant to see by guessing URLs, 393 // but it is remote. 394 return $options->feedback; 395 396 } else if ($component == 'question' && $filearea == 'hint') { 397 return $this->check_hint_file_access($qa, $options, $args); 398 399 } else { 400 return parent::check_file_access($qa, $options, $component, $filearea, 401 $args, $forcedownload); 402 } 403 } 404 405 /** 406 * Return the question settings that define this question as structured data. 407 * 408 * @param question_attempt $qa the current attempt for which we are exporting the settings. 409 * @param question_display_options $options the question display options which say which aspects of the question 410 * should be visible. 411 * @return mixed structure representing the question settings. In web services, this will be JSON-encoded. 412 */ 413 public function get_question_definition_for_external_rendering(question_attempt $qa, question_display_options $options) { 414 // Empty implementation for now in order to avoid debugging in core questions (generated in the parent class), 415 // ideally, we should return as much as settings as possible (depending on the state and display options). 416 417 return null; 418 } 419 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body