Differences Between: [Versions 310 and 400] [Versions 311 and 400] [Versions 39 and 400] [Versions 400 and 401] [Versions 400 and 402] [Versions 400 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 /** 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 return $fractionsum / (!empty($this->subquestions) ? $fractionmax : 1); 151 } 152 153 public function get_max_fraction() { 154 $fractionsum = 0; 155 $fractionmax = 0; 156 foreach ($this->subquestions as $i => $subq) { 157 $fractionmax += $subq->defaultmark; 158 $fractionsum += $subq->defaultmark * $subq->get_max_fraction(); 159 } 160 return $fractionsum / (!empty($this->subquestions) ? $fractionmax : 1); 161 } 162 163 public function get_expected_data() { 164 $expected = array(); 165 foreach ($this->subquestions as $i => $subq) { 166 $substep = $this->get_substep(null, $i); 167 foreach ($subq->get_expected_data() as $name => $type) { 168 if ($subq->qtype->name() == 'multichoice' && 169 $subq->layout == qtype_multichoice_base::LAYOUT_DROPDOWN) { 170 // Hack or MC inline does not work. 171 $expected[$substep->add_prefix($name)] = PARAM_RAW; 172 } else { 173 $expected[$substep->add_prefix($name)] = $type; 174 } 175 } 176 } 177 return $expected; 178 } 179 180 public function get_correct_response() { 181 $right = array(); 182 foreach ($this->subquestions as $i => $subq) { 183 $substep = $this->get_substep(null, $i); 184 foreach ($subq->get_correct_response() as $name => $type) { 185 $right[$substep->add_prefix($name)] = $type; 186 } 187 } 188 return $right; 189 } 190 191 public function prepare_simulated_post_data($simulatedresponse) { 192 $postdata = array(); 193 foreach ($this->subquestions as $i => $subq) { 194 $substep = $this->get_substep(null, $i); 195 foreach ($subq->prepare_simulated_post_data($simulatedresponse[$i]) as $name => $value) { 196 $postdata[$substep->add_prefix($name)] = $value; 197 } 198 } 199 return $postdata; 200 } 201 202 public function get_student_response_values_for_simulation($postdata) { 203 $simulatedresponse = array(); 204 foreach ($this->subquestions as $i => $subq) { 205 $substep = $this->get_substep(null, $i); 206 $subqpostdata = $substep->filter_array($postdata); 207 $subqsimulatedresponse = $subq->get_student_response_values_for_simulation($subqpostdata); 208 foreach ($subqsimulatedresponse as $subresponsekey => $responsevalue) { 209 $simulatedresponse[$i.'.'.$subresponsekey] = $responsevalue; 210 } 211 } 212 ksort($simulatedresponse); 213 return $simulatedresponse; 214 } 215 216 public function is_complete_response(array $response) { 217 foreach ($this->subquestions as $i => $subq) { 218 $substep = $this->get_substep(null, $i); 219 if (!$subq->is_complete_response($substep->filter_array($response))) { 220 return false; 221 } 222 } 223 return true; 224 } 225 226 public function is_gradable_response(array $response) { 227 foreach ($this->subquestions as $i => $subq) { 228 $substep = $this->get_substep(null, $i); 229 if ($subq->is_gradable_response($substep->filter_array($response))) { 230 return true; 231 } 232 } 233 return false; 234 } 235 236 public function is_same_response(array $prevresponse, array $newresponse) { 237 foreach ($this->subquestions as $i => $subq) { 238 $substep = $this->get_substep(null, $i); 239 if (!$subq->is_same_response($substep->filter_array($prevresponse), 240 $substep->filter_array($newresponse))) { 241 return false; 242 } 243 } 244 return true; 245 } 246 247 public function get_validation_error(array $response) { 248 if ($this->is_complete_response($response)) { 249 return ''; 250 } 251 return get_string('pleaseananswerallparts', 'qtype_multianswer'); 252 } 253 254 /** 255 * Used by grade_response to combine the states of the subquestions. 256 * The combined state is accumulates in $overallstate. That will be right 257 * if all the separate states are right; and wrong if all the separate states 258 * are wrong, otherwise, it will be partially right. 259 * @param question_state $overallstate the result so far. 260 * @param question_state $newstate the new state to add to the combination. 261 * @return question_state the new combined state. 262 */ 263 protected function combine_states($overallstate, $newstate) { 264 if (is_null($overallstate)) { 265 return $newstate; 266 } else if ($overallstate == question_state::$gaveup && 267 $newstate == question_state::$gaveup) { 268 return question_state::$gaveup; 269 } else if ($overallstate == question_state::$gaveup && 270 $newstate == question_state::$gradedwrong) { 271 return question_state::$gradedwrong; 272 } else if ($overallstate == question_state::$gradedwrong && 273 $newstate == question_state::$gaveup) { 274 return question_state::$gradedwrong; 275 } else if ($overallstate == question_state::$gradedwrong && 276 $newstate == question_state::$gradedwrong) { 277 return question_state::$gradedwrong; 278 } else if ($overallstate == question_state::$gradedright && 279 $newstate == question_state::$gradedright) { 280 return question_state::$gradedright; 281 } else { 282 return question_state::$gradedpartial; 283 } 284 } 285 286 public function grade_response(array $response) { 287 $overallstate = null; 288 $fractionsum = 0; 289 $fractionmax = 0; 290 foreach ($this->subquestions as $i => $subq) { 291 $fractionmax += $subq->defaultmark; 292 $substep = $this->get_substep(null, $i); 293 $subresp = $substep->filter_array($response); 294 if (!$subq->is_gradable_response($subresp)) { 295 $overallstate = $this->combine_states($overallstate, question_state::$gaveup); 296 } else { 297 list($subfraction, $newstate) = $subq->grade_response($subresp); 298 $fractionsum += $subfraction * $subq->defaultmark; 299 $overallstate = $this->combine_states($overallstate, $newstate); 300 } 301 } 302 return array($fractionsum / $fractionmax, $overallstate); 303 } 304 305 public function clear_wrong_from_response(array $response) { 306 foreach ($this->subquestions as $i => $subq) { 307 $substep = $this->get_substep(null, $i); 308 $subresp = $substep->filter_array($response); 309 list($subfraction, $newstate) = $subq->grade_response($subresp); 310 if ($newstate != question_state::$gradedright) { 311 foreach ($subresp as $ind => $resp) { 312 if ($subq->qtype == 'multichoice' && ($subq->layout == qtype_multichoice_base::LAYOUT_VERTICAL 313 || $subq->layout == qtype_multichoice_base::LAYOUT_HORIZONTAL)) { 314 $response[$substep->add_prefix($ind)] = '-1'; 315 } else { 316 $response[$substep->add_prefix($ind)] = ''; 317 } 318 } 319 } 320 } 321 return $response; 322 } 323 324 public function get_num_parts_right(array $response) { 325 $numright = 0; 326 foreach ($this->subquestions as $i => $subq) { 327 $substep = $this->get_substep(null, $i); 328 $subresp = $substep->filter_array($response); 329 list($subfraction, $newstate) = $subq->grade_response($subresp); 330 if ($newstate == question_state::$gradedright) { 331 $numright += 1; 332 } 333 } 334 return array($numright, count($this->subquestions)); 335 } 336 337 public function compute_final_grade($responses, $totaltries) { 338 $fractionsum = 0; 339 $fractionmax = 0; 340 foreach ($this->subquestions as $i => $subq) { 341 $fractionmax += $subq->defaultmark; 342 343 $lastresponse = array(); 344 $lastchange = 0; 345 $subfraction = 0; 346 foreach ($responses as $responseindex => $response) { 347 $substep = $this->get_substep(null, $i); 348 $subresp = $substep->filter_array($response); 349 if ($subq->is_same_response($lastresponse, $subresp)) { 350 continue; 351 } 352 $lastresponse = $subresp; 353 $lastchange = $responseindex; 354 list($subfraction, $newstate) = $subq->grade_response($subresp); 355 } 356 357 $fractionsum += $subq->defaultmark * max(0, $subfraction - $lastchange * $this->penalty); 358 } 359 360 return $fractionsum / $fractionmax; 361 } 362 363 public function summarise_response(array $response) { 364 $summary = array(); 365 foreach ($this->subquestions as $i => $subq) { 366 $substep = $this->get_substep(null, $i); 367 $a = new stdClass(); 368 $a->i = $i; 369 $a->response = $subq->summarise_response($substep->filter_array($response)); 370 $summary[] = get_string('subqresponse', 'qtype_multianswer', $a); 371 } 372 373 return implode('; ', $summary); 374 } 375 376 public function check_file_access($qa, $options, $component, $filearea, $args, $forcedownload) { 377 if ($component == 'question' && $filearea == 'answer') { 378 return true; 379 380 } else if ($component == 'question' && $filearea == 'answerfeedback') { 381 // Full logic to control which feedbacks a student can see is too complex. 382 // Just allow access to all images. There is a theoretical chance the 383 // students could see files they are not meant to see by guessing URLs, 384 // but it is remote. 385 return $options->feedback; 386 387 } else if ($component == 'question' && $filearea == 'hint') { 388 return $this->check_hint_file_access($qa, $options, $args); 389 390 } else { 391 return parent::check_file_access($qa, $options, $component, $filearea, 392 $args, $forcedownload); 393 } 394 } 395 396 /** 397 * Return the question settings that define this question as structured data. 398 * 399 * @param question_attempt $qa the current attempt for which we are exporting the settings. 400 * @param question_display_options $options the question display options which say which aspects of the question 401 * should be visible. 402 * @return mixed structure representing the question settings. In web services, this will be JSON-encoded. 403 */ 404 public function get_question_definition_for_external_rendering(question_attempt $qa, question_display_options $options) { 405 // Empty implementation for now in order to avoid debugging in core questions (generated in the parent class), 406 // ideally, we should return as much as settings as possible (depending on the state and display options). 407 408 return null; 409 } 410 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body