See Release Notes
Long Term Support Release
Differences Between: [Versions 39 and 310] [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 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 get_question_summary() { 82 $summary = $this->html_to_text($this->questiontext, $this->questiontextformat); 83 foreach ($this->subquestions as $i => $subq) { 84 switch ($subq->qtype->name()) { 85 case 'multichoice': 86 $choices = array(); 87 $dummyqa = new question_attempt($subq, $this->contextid); 88 foreach ($subq->get_order($dummyqa) as $ansid) { 89 $choices[] = $this->html_to_text($subq->answers[$ansid]->answer, 90 $subq->answers[$ansid]->answerformat); 91 } 92 $answerbit = '{' . implode('; ', $choices) . '}'; 93 break; 94 case 'numerical': 95 case 'shortanswer': 96 $answerbit = '_____'; 97 break; 98 default: 99 $answerbit = '{ERR unknown sub-question type}'; 100 } 101 $summary = str_replace('{#' . $i . '}', $answerbit, $summary); 102 } 103 return $summary; 104 } 105 106 public function get_min_fraction() { 107 $fractionsum = 0; 108 $fractionmax = 0; 109 foreach ($this->subquestions as $i => $subq) { 110 $fractionmax += $subq->defaultmark; 111 $fractionsum += $subq->defaultmark * $subq->get_min_fraction(); 112 } 113 return $fractionsum / $fractionmax; 114 } 115 116 public function get_max_fraction() { 117 $fractionsum = 0; 118 $fractionmax = 0; 119 foreach ($this->subquestions as $i => $subq) { 120 $fractionmax += $subq->defaultmark; 121 $fractionsum += $subq->defaultmark * $subq->get_max_fraction(); 122 } 123 return $fractionsum / $fractionmax; 124 } 125 126 public function get_expected_data() { 127 $expected = array(); 128 foreach ($this->subquestions as $i => $subq) { 129 $substep = $this->get_substep(null, $i); 130 foreach ($subq->get_expected_data() as $name => $type) { 131 if ($subq->qtype->name() == 'multichoice' && 132 $subq->layout == qtype_multichoice_base::LAYOUT_DROPDOWN) { 133 // Hack or MC inline does not work. 134 $expected[$substep->add_prefix($name)] = PARAM_RAW; 135 } else { 136 $expected[$substep->add_prefix($name)] = $type; 137 } 138 } 139 } 140 return $expected; 141 } 142 143 public function get_correct_response() { 144 $right = array(); 145 foreach ($this->subquestions as $i => $subq) { 146 $substep = $this->get_substep(null, $i); 147 foreach ($subq->get_correct_response() as $name => $type) { 148 $right[$substep->add_prefix($name)] = $type; 149 } 150 } 151 return $right; 152 } 153 154 public function prepare_simulated_post_data($simulatedresponse) { 155 $postdata = array(); 156 foreach ($this->subquestions as $i => $subq) { 157 $substep = $this->get_substep(null, $i); 158 foreach ($subq->prepare_simulated_post_data($simulatedresponse[$i]) as $name => $value) { 159 $postdata[$substep->add_prefix($name)] = $value; 160 } 161 } 162 return $postdata; 163 } 164 165 public function get_student_response_values_for_simulation($postdata) { 166 $simulatedresponse = array(); 167 foreach ($this->subquestions as $i => $subq) { 168 $substep = $this->get_substep(null, $i); 169 $subqpostdata = $substep->filter_array($postdata); 170 $subqsimulatedresponse = $subq->get_student_response_values_for_simulation($subqpostdata); 171 foreach ($subqsimulatedresponse as $subresponsekey => $responsevalue) { 172 $simulatedresponse[$i.'.'.$subresponsekey] = $responsevalue; 173 } 174 } 175 ksort($simulatedresponse); 176 return $simulatedresponse; 177 } 178 179 public function is_complete_response(array $response) { 180 foreach ($this->subquestions as $i => $subq) { 181 $substep = $this->get_substep(null, $i); 182 if (!$subq->is_complete_response($substep->filter_array($response))) { 183 return false; 184 } 185 } 186 return true; 187 } 188 189 public function is_gradable_response(array $response) { 190 foreach ($this->subquestions as $i => $subq) { 191 $substep = $this->get_substep(null, $i); 192 if ($subq->is_gradable_response($substep->filter_array($response))) { 193 return true; 194 } 195 } 196 return false; 197 } 198 199 public function is_same_response(array $prevresponse, array $newresponse) { 200 foreach ($this->subquestions as $i => $subq) { 201 $substep = $this->get_substep(null, $i); 202 if (!$subq->is_same_response($substep->filter_array($prevresponse), 203 $substep->filter_array($newresponse))) { 204 return false; 205 } 206 } 207 return true; 208 } 209 210 public function get_validation_error(array $response) { 211 if ($this->is_complete_response($response)) { 212 return ''; 213 } 214 return get_string('pleaseananswerallparts', 'qtype_multianswer'); 215 } 216 217 /** 218 * Used by grade_response to combine the states of the subquestions. 219 * The combined state is accumulates in $overallstate. That will be right 220 * if all the separate states are right; and wrong if all the separate states 221 * are wrong, otherwise, it will be partially right. 222 * @param question_state $overallstate the result so far. 223 * @param question_state $newstate the new state to add to the combination. 224 * @return question_state the new combined state. 225 */ 226 protected function combine_states($overallstate, $newstate) { 227 if (is_null($overallstate)) { 228 return $newstate; 229 } else if ($overallstate == question_state::$gaveup && 230 $newstate == question_state::$gaveup) { 231 return question_state::$gaveup; 232 } else if ($overallstate == question_state::$gaveup && 233 $newstate == question_state::$gradedwrong) { 234 return question_state::$gradedwrong; 235 } else if ($overallstate == question_state::$gradedwrong && 236 $newstate == question_state::$gaveup) { 237 return question_state::$gradedwrong; 238 } else if ($overallstate == question_state::$gradedwrong && 239 $newstate == question_state::$gradedwrong) { 240 return question_state::$gradedwrong; 241 } else if ($overallstate == question_state::$gradedright && 242 $newstate == question_state::$gradedright) { 243 return question_state::$gradedright; 244 } else { 245 return question_state::$gradedpartial; 246 } 247 } 248 249 public function grade_response(array $response) { 250 $overallstate = null; 251 $fractionsum = 0; 252 $fractionmax = 0; 253 foreach ($this->subquestions as $i => $subq) { 254 $fractionmax += $subq->defaultmark; 255 $substep = $this->get_substep(null, $i); 256 $subresp = $substep->filter_array($response); 257 if (!$subq->is_gradable_response($subresp)) { 258 $overallstate = $this->combine_states($overallstate, question_state::$gaveup); 259 } else { 260 list($subfraction, $newstate) = $subq->grade_response($subresp); 261 $fractionsum += $subfraction * $subq->defaultmark; 262 $overallstate = $this->combine_states($overallstate, $newstate); 263 } 264 } 265 return array($fractionsum / $fractionmax, $overallstate); 266 } 267 268 public function clear_wrong_from_response(array $response) { 269 foreach ($this->subquestions as $i => $subq) { 270 $substep = $this->get_substep(null, $i); 271 $subresp = $substep->filter_array($response); 272 list($subfraction, $newstate) = $subq->grade_response($subresp); 273 if ($newstate != question_state::$gradedright) { 274 foreach ($subresp as $ind => $resp) { 275 if ($subq->qtype == 'multichoice' && ($subq->layout == qtype_multichoice_base::LAYOUT_VERTICAL 276 || $subq->layout == qtype_multichoice_base::LAYOUT_HORIZONTAL)) { 277 $response[$substep->add_prefix($ind)] = '-1'; 278 } else { 279 $response[$substep->add_prefix($ind)] = ''; 280 } 281 } 282 } 283 } 284 return $response; 285 } 286 287 public function get_num_parts_right(array $response) { 288 $numright = 0; 289 foreach ($this->subquestions as $i => $subq) { 290 $substep = $this->get_substep(null, $i); 291 $subresp = $substep->filter_array($response); 292 list($subfraction, $newstate) = $subq->grade_response($subresp); 293 if ($newstate == question_state::$gradedright) { 294 $numright += 1; 295 } 296 } 297 return array($numright, count($this->subquestions)); 298 } 299 300 public function compute_final_grade($responses, $totaltries) { 301 $fractionsum = 0; 302 $fractionmax = 0; 303 foreach ($this->subquestions as $i => $subq) { 304 $fractionmax += $subq->defaultmark; 305 306 $lastresponse = array(); 307 $lastchange = 0; 308 $subfraction = 0; 309 foreach ($responses as $responseindex => $response) { 310 $substep = $this->get_substep(null, $i); 311 $subresp = $substep->filter_array($response); 312 if ($subq->is_same_response($lastresponse, $subresp)) { 313 continue; 314 } 315 $lastresponse = $subresp; 316 $lastchange = $responseindex; 317 list($subfraction, $newstate) = $subq->grade_response($subresp); 318 } 319 320 $fractionsum += $subq->defaultmark * max(0, $subfraction - $lastchange * $this->penalty); 321 } 322 323 return $fractionsum / $fractionmax; 324 } 325 326 public function summarise_response(array $response) { 327 $summary = array(); 328 foreach ($this->subquestions as $i => $subq) { 329 $substep = $this->get_substep(null, $i); 330 $a = new stdClass(); 331 $a->i = $i; 332 $a->response = $subq->summarise_response($substep->filter_array($response)); 333 $summary[] = get_string('subqresponse', 'qtype_multianswer', $a); 334 } 335 336 return implode('; ', $summary); 337 } 338 339 public function check_file_access($qa, $options, $component, $filearea, $args, $forcedownload) { 340 if ($component == 'question' && $filearea == 'answer') { 341 return true; 342 343 } else if ($component == 'question' && $filearea == 'answerfeedback') { 344 // Full logic to control which feedbacks a student can see is too complex. 345 // Just allow access to all images. There is a theoretical chance the 346 // students could see files they are not meant to see by guessing URLs, 347 // but it is remote. 348 return $options->feedback; 349 350 } else if ($component == 'question' && $filearea == 'hint') { 351 return $this->check_hint_file_access($qa, $options, $args); 352 353 } else { 354 return parent::check_file_access($qa, $options, $component, $filearea, 355 $args, $forcedownload); 356 } 357 } 358 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body