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 * Matching question definition class. 19 * 20 * @package qtype_match 21 * @copyright 2009 The Open University 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 25 26 defined('MOODLE_INTERNAL') || die(); 27 28 require_once($CFG->dirroot . '/question/type/questionbase.php'); 29 30 /** 31 * Represents a matching question. 32 * 33 * @copyright 2009 The Open University 34 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 35 */ 36 class qtype_match_question extends question_graded_automatically_with_countback { 37 /** @var boolean Whether the question stems should be shuffled. */ 38 public $shufflestems; 39 40 public $correctfeedback; 41 public $correctfeedbackformat; 42 public $partiallycorrectfeedback; 43 public $partiallycorrectfeedbackformat; 44 public $incorrectfeedback; 45 public $incorrectfeedbackformat; 46 47 /** @var array of question stems. */ 48 public $stems; 49 /** @var int[] FORMAT_... type for each stem. */ 50 public $stemformat; 51 /** @var array of choices that can be matched to each stem. */ 52 public $choices; 53 /** @var array index of the right choice for each stem. */ 54 public $right; 55 56 /** @var array shuffled stem indexes. */ 57 protected $stemorder; 58 /** @var array shuffled choice indexes. */ 59 protected $choiceorder; 60 61 public function start_attempt(question_attempt_step $step, $variant) { 62 $this->stemorder = array_keys($this->stems); 63 if ($this->shufflestems) { 64 shuffle($this->stemorder); 65 } 66 $step->set_qt_var('_stemorder', implode(',', $this->stemorder)); 67 68 $choiceorder = array_keys($this->choices); 69 shuffle($choiceorder); 70 $step->set_qt_var('_choiceorder', implode(',', $choiceorder)); 71 $this->set_choiceorder($choiceorder); 72 } 73 74 public function apply_attempt_state(question_attempt_step $step) { 75 $this->stemorder = explode(',', $step->get_qt_var('_stemorder')); 76 $this->set_choiceorder(explode(',', $step->get_qt_var('_choiceorder'))); 77 78 // Add any missing subquestions. Sometimes people edit questions after they 79 // have been attempted which breaks things. 80 foreach ($this->stemorder as $stemid) { 81 if (!isset($this->stems[$stemid])) { 82 $this->stems[$stemid] = html_writer::span( 83 get_string('deletedsubquestion', 'qtype_match'), 'notifyproblem'); 84 $this->stemformat[$stemid] = FORMAT_HTML; 85 $this->right[$stemid] = 0; 86 } 87 } 88 89 // Add any missing choices. Sometimes people edit questions after they 90 // have been attempted which breaks things. 91 foreach ($this->choiceorder as $choiceid) { 92 if (!isset($this->choices[$choiceid])) { 93 $this->choices[$choiceid] = get_string('deletedchoice', 'qtype_match'); 94 } 95 } 96 } 97 98 /** 99 * Helper method used by both {@link start_attempt()} and 100 * {@link apply_attempt_state()}. 101 * @param array $choiceorder the choices, in order. 102 */ 103 protected function set_choiceorder($choiceorder) { 104 $this->choiceorder = array(); 105 foreach ($choiceorder as $key => $choiceid) { 106 $this->choiceorder[$key + 1] = $choiceid; 107 } 108 } 109 110 public function get_question_summary() { 111 $question = $this->html_to_text($this->questiontext, $this->questiontextformat); 112 $stems = array(); 113 foreach ($this->stemorder as $stemid) { 114 $stems[] = $this->html_to_text($this->stems[$stemid], $this->stemformat[$stemid]); 115 } 116 $choices = array(); 117 foreach ($this->choiceorder as $choiceid) { 118 $choices[] = $this->choices[$choiceid]; 119 } 120 return $question . ' {' . implode('; ', $stems) . '} -> {' . 121 implode('; ', $choices) . '}'; 122 } 123 124 public function summarise_response(array $response) { 125 $matches = array(); 126 foreach ($this->stemorder as $key => $stemid) { 127 if (array_key_exists($this->field($key), $response) && $response[$this->field($key)]) { 128 $matches[] = $this->html_to_text($this->stems[$stemid], 129 $this->stemformat[$stemid]) . ' -> ' . 130 $this->choices[$this->choiceorder[$response[$this->field($key)]]]; 131 } 132 } 133 if (empty($matches)) { 134 return null; 135 } 136 return implode('; ', $matches); 137 } 138 139 public function classify_response(array $response) { 140 $selectedchoicekeys = array(); 141 foreach ($this->stemorder as $key => $stemid) { 142 if (array_key_exists($this->field($key), $response) && $response[$this->field($key)]) { 143 $selectedchoicekeys[$stemid] = $this->choiceorder[$response[$this->field($key)]]; 144 } else { 145 $selectedchoicekeys[$stemid] = 0; 146 } 147 } 148 149 $parts = array(); 150 foreach ($this->stems as $stemid => $stem) { 151 if ($this->right[$stemid] == 0 || !isset($selectedchoicekeys[$stemid])) { 152 // Choice for a deleted subquestion, ignore. (See apply_attempt_state.) 153 continue; 154 } 155 $selectedchoicekey = $selectedchoicekeys[$stemid]; 156 if (empty($selectedchoicekey)) { 157 $parts[$stemid] = question_classified_response::no_response(); 158 continue; 159 } 160 $choice = $this->choices[$selectedchoicekey]; 161 if ($choice == get_string('deletedchoice', 'qtype_match')) { 162 // Deleted choice, ignore. (See apply_attempt_state.) 163 continue; 164 } 165 $parts[$stemid] = new question_classified_response( 166 $selectedchoicekey, $choice, 167 ($selectedchoicekey == $this->right[$stemid]) / count($this->stems)); 168 } 169 return $parts; 170 } 171 172 public function clear_wrong_from_response(array $response) { 173 foreach ($this->stemorder as $key => $stemid) { 174 if (!array_key_exists($this->field($key), $response) || 175 $response[$this->field($key)] != $this->get_right_choice_for($stemid)) { 176 $response[$this->field($key)] = 0; 177 } 178 } 179 return $response; 180 } 181 182 public function get_num_parts_right(array $response) { 183 $numright = 0; 184 foreach ($this->stemorder as $key => $stemid) { 185 $fieldname = $this->field($key); 186 if (!array_key_exists($fieldname, $response)) { 187 continue; 188 } 189 190 $choice = $response[$fieldname]; 191 if ($choice && $this->choiceorder[$choice] == $this->right[$stemid]) { 192 $numright += 1; 193 } 194 } 195 return array($numright, count($this->stemorder)); 196 } 197 198 /** 199 * @param int $key stem number 200 * @return string the question-type variable name. 201 */ 202 protected function field($key) { 203 return 'sub' . $key; 204 } 205 206 public function get_expected_data() { 207 $vars = array(); 208 foreach ($this->stemorder as $key => $notused) { 209 $vars[$this->field($key)] = PARAM_INT; 210 } 211 return $vars; 212 } 213 214 public function get_correct_response() { 215 $response = array(); 216 foreach ($this->stemorder as $key => $stemid) { 217 $response[$this->field($key)] = $this->get_right_choice_for($stemid); 218 } 219 return $response; 220 } 221 222 public function prepare_simulated_post_data($simulatedresponse) { 223 $postdata = array(); 224 $stemtostemids = array_flip(clean_param_array($this->stems, PARAM_NOTAGS)); 225 $choicetochoiceno = array_flip($this->choices); 226 $choicenotochoiceselectvalue = array_flip($this->choiceorder); 227 foreach ($simulatedresponse as $stem => $choice) { 228 $choice = clean_param($choice, PARAM_NOTAGS); 229 $stemid = $stemtostemids[$stem]; 230 $shuffledstemno = array_search($stemid, $this->stemorder); 231 if (empty($choice)) { 232 $choiceselectvalue = 0; 233 } else if ($choicetochoiceno[$choice]) { 234 $choiceselectvalue = $choicenotochoiceselectvalue[$choicetochoiceno[$choice]]; 235 } else { 236 throw new coding_exception("Unknown choice {$choice} in matching question - {$this->name}."); 237 } 238 $postdata[$this->field($shuffledstemno)] = $choiceselectvalue; 239 } 240 return $postdata; 241 } 242 243 public function get_student_response_values_for_simulation($postdata) { 244 $simulatedresponse = array(); 245 foreach ($this->stemorder as $shuffledstemno => $stemid) { 246 if (!empty($postdata[$this->field($shuffledstemno)])) { 247 $choiceselectvalue = $postdata[$this->field($shuffledstemno)]; 248 $choiceno = $this->choiceorder[$choiceselectvalue]; 249 $choice = clean_param($this->choices[$choiceno], PARAM_NOTAGS); 250 $stem = clean_param($this->stems[$stemid], PARAM_NOTAGS); 251 $simulatedresponse[$stem] = $choice; 252 } 253 } 254 ksort($simulatedresponse); 255 return $simulatedresponse; 256 } 257 258 public function get_right_choice_for($stemid) { 259 foreach ($this->choiceorder as $choicekey => $choiceid) { 260 if ($this->right[$stemid] == $choiceid) { 261 return $choicekey; 262 } 263 } 264 } 265 266 public function is_complete_response(array $response) { 267 $complete = true; 268 foreach ($this->stemorder as $key => $stemid) { 269 $complete = $complete && !empty($response[$this->field($key)]); 270 } 271 return $complete; 272 } 273 274 public function is_gradable_response(array $response) { 275 foreach ($this->stemorder as $key => $stemid) { 276 if (!empty($response[$this->field($key)])) { 277 return true; 278 } 279 } 280 return false; 281 } 282 283 public function get_validation_error(array $response) { 284 if ($this->is_complete_response($response)) { 285 return ''; 286 } 287 return get_string('pleaseananswerallparts', 'qtype_match'); 288 } 289 290 public function is_same_response(array $prevresponse, array $newresponse) { 291 foreach ($this->stemorder as $key => $notused) { 292 $fieldname = $this->field($key); 293 if (!question_utils::arrays_same_at_key_integer( 294 $prevresponse, $newresponse, $fieldname)) { 295 return false; 296 } 297 } 298 return true; 299 } 300 301 public function grade_response(array $response) { 302 list($right, $total) = $this->get_num_parts_right($response); 303 $fraction = $right / $total; 304 return array($fraction, question_state::graded_state_for_fraction($fraction)); 305 } 306 307 public function compute_final_grade($responses, $totaltries) { 308 $totalstemscore = 0; 309 foreach ($this->stemorder as $key => $stemid) { 310 $fieldname = $this->field($key); 311 312 $lastwrongindex = -1; 313 $finallyright = false; 314 foreach ($responses as $i => $response) { 315 if (!array_key_exists($fieldname, $response) || !$response[$fieldname] || 316 $this->choiceorder[$response[$fieldname]] != $this->right[$stemid]) { 317 $lastwrongindex = $i; 318 $finallyright = false; 319 } else { 320 $finallyright = true; 321 } 322 } 323 324 if ($finallyright) { 325 $totalstemscore += max(0, 1 - ($lastwrongindex + 1) * $this->penalty); 326 } 327 } 328 329 return $totalstemscore / count($this->stemorder); 330 } 331 332 public function get_stem_order() { 333 return $this->stemorder; 334 } 335 336 public function get_choice_order() { 337 return $this->choiceorder; 338 } 339 340 public function check_file_access($qa, $options, $component, $filearea, $args, $forcedownload) { 341 if ($component == 'qtype_match' && $filearea == 'subquestion') { 342 $subqid = reset($args); // Itemid is sub question id. 343 return array_key_exists($subqid, $this->stems); 344 345 } else if ($component == 'question' && in_array($filearea, 346 array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback'))) { 347 return $this->check_combined_feedback_file_access($qa, $options, $filearea, $args); 348 349 } else if ($component == 'question' && $filearea == 'hint') { 350 return $this->check_hint_file_access($qa, $options, $args); 351 352 } else { 353 return parent::check_file_access($qa, $options, $component, $filearea, 354 $args, $forcedownload); 355 } 356 } 357 358 /** 359 * Return the question settings that define this question as structured data. 360 * 361 * @param question_attempt $qa the current attempt for which we are exporting the settings. 362 * @param question_display_options $options the question display options which say which aspects of the question 363 * should be visible. 364 * @return mixed structure representing the question settings. In web services, this will be JSON-encoded. 365 */ 366 public function get_question_definition_for_external_rendering(question_attempt $qa, question_display_options $options) { 367 // This is a partial implementation, returning only the most relevant question settings for now, 368 // ideally, we should return as much as settings as possible (depending on the state and display options). 369 370 return [ 371 'shufflestems' => $this->shufflestems, 372 ]; 373 } 374 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body