Differences Between: [Versions 310 and 403] [Versions 311 and 403] [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 * Definition class for embedded element in question text question. 19 * 20 * Used by gap-select, drag and drop and possibly others. 21 * 22 * @package qtype_gapselect 23 * @copyright 2011 The Open University 24 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 25 */ 26 27 28 defined('MOODLE_INTERNAL') || die(); 29 30 require_once($CFG->dirroot . '/question/type/questionbase.php'); 31 32 /** 33 * Represents embedded element in question text question. 34 * 35 * Parent of drag and drop and select from drop down list and others. 36 * 37 * @copyright 2011 The Open University 38 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 39 */ 40 abstract class qtype_gapselect_question_base extends question_graded_automatically_with_countback { 41 /** @var boolean Whether the question stems should be shuffled. */ 42 public $shufflechoices; 43 44 /** @var string Feedback for any correct response. */ 45 public $correctfeedback; 46 /** @var int format of $correctfeedback. */ 47 public $correctfeedbackformat; 48 /** @var string Feedback for any partially correct response. */ 49 public $partiallycorrectfeedback; 50 /** @var int format of $partiallycorrectfeedback. */ 51 public $partiallycorrectfeedbackformat; 52 /** @var string Feedback for any incorrect response. */ 53 public $incorrectfeedback; 54 /** @var int format of $incorrectfeedback. */ 55 public $incorrectfeedbackformat; 56 57 /** 58 * @var array of arrays. The outer keys are the choice group numbers. 59 * The inner keys for most question types number sequentialy from 1. However 60 * for ddimageortext questions it is strange (and difficult to change now). 61 * the first item in each group gets numbered 1, and the other items get numbered 62 * $choice->no. Be careful! 63 * The values are arrays of qtype_gapselect_choice objects (or a subclass). 64 */ 65 public $choices; 66 67 /** 68 * @var array place number => group number of the places in the question 69 * text where choices can be put. Places are numbered from 1. 70 */ 71 public $places; 72 73 /** 74 * @var array of strings, one longer than $places, which is achieved by 75 * indexing from 0. The bits of question text that go between the placeholders. 76 */ 77 public $textfragments; 78 79 /** @var array index of the right choice for each stem. */ 80 public $rightchoices; 81 82 /** @var array shuffled choice indexes. */ 83 protected $choiceorder; 84 85 public function start_attempt(question_attempt_step $step, $variant) { 86 foreach ($this->choices as $group => $choices) { 87 $choiceorder = array_keys($choices); 88 if ($this->shufflechoices) { 89 shuffle($choiceorder); 90 } 91 $step->set_qt_var('_choiceorder' . $group, implode(',', $choiceorder)); 92 $this->set_choiceorder($group, $choiceorder); 93 } 94 } 95 96 public function apply_attempt_state(question_attempt_step $step) { 97 foreach ($this->choices as $group => $choices) { 98 $this->set_choiceorder($group, explode(',', 99 $step->get_qt_var('_choiceorder' . $group))); 100 } 101 } 102 103 /** 104 * Helper method used by both {@link start_attempt()} and 105 * {@link apply_attempt_state()}. 106 * @param int $group the group number. 107 * @param array $choiceorder the choices, in order. 108 */ 109 protected function set_choiceorder($group, $choiceorder) { 110 foreach ($choiceorder as $key => $value) { 111 $this->choiceorder[$group][$key + 1] = $value; 112 } 113 } 114 115 public function validate_can_regrade_with_other_version(question_definition $otherversion): ?string { 116 $basemessage = parent::validate_can_regrade_with_other_version($otherversion); 117 if ($basemessage) { 118 return $basemessage; 119 } 120 121 if (count($this->choices) != count($otherversion->choices)) { 122 return get_string('regradeissuenumgroupsschanged', 'qtype_gapselect'); 123 } 124 125 foreach ($this->choices as $group => $choices) { 126 if (count($this->choices[$group]) != count($otherversion->choices[$group])) { 127 return get_string('regradeissuenumchoiceschanged', 'qtype_gapselect', $group); 128 } 129 } 130 131 return null; 132 } 133 134 public function get_question_summary() { 135 $question = $this->html_to_text($this->questiontext, $this->questiontextformat); 136 $groups = array(); 137 foreach ($this->choices as $group => $choices) { 138 $cs = array(); 139 foreach ($choices as $choice) { 140 $cs[] = html_to_text($choice->text, 0, false); 141 } 142 $groups[] = '[[' . $group . ']] -> {' . implode(' / ', $cs) . '}'; 143 } 144 return $question . '; ' . implode('; ', $groups); 145 } 146 147 protected function get_selected_choice($group, $shuffledchoicenumber) { 148 $choiceno = $this->choiceorder[$group][$shuffledchoicenumber]; 149 return isset($this->choices[$group][$choiceno]) ? $this->choices[$group][$choiceno] : null; 150 } 151 152 public function summarise_response(array $response) { 153 $matches = array(); 154 $allblank = true; 155 foreach ($this->places as $place => $group) { 156 if (array_key_exists($this->field($place), $response) && 157 $response[$this->field($place)]) { 158 $choices[] = '{' . $this->summarise_choice( 159 $this->get_selected_choice($group, $response[$this->field($place)])) . '}'; 160 $allblank = false; 161 } else { 162 $choices[] = '{}'; 163 } 164 } 165 if ($allblank) { 166 return null; 167 } 168 return implode(' ', $choices); 169 } 170 171 /** 172 * Convert a choice to plain text. 173 * @param qtype_gapselect_choice $choice one of the choices for a place. 174 * @return a plain text summary of the choice. 175 */ 176 public function summarise_choice($choice) { 177 return $this->html_to_text($choice->text, FORMAT_PLAIN); 178 } 179 180 public function get_random_guess_score() { 181 $accum = 0; 182 183 foreach ($this->places as $placegroup) { 184 $accum += 1 / count($this->choices[$placegroup]); 185 } 186 187 return $accum / count($this->places); 188 } 189 190 public function clear_wrong_from_response(array $response) { 191 foreach ($this->places as $place => $notused) { 192 if (array_key_exists($this->field($place), $response) && 193 $response[$this->field($place)] != $this->get_right_choice_for($place)) { 194 $response[$this->field($place)] = '0'; 195 } 196 } 197 return $response; 198 } 199 200 public function get_num_parts_right(array $response) { 201 $numright = 0; 202 foreach ($this->places as $place => $notused) { 203 if (!array_key_exists($this->field($place), $response)) { 204 continue; 205 } 206 if ($response[$this->field($place)] == $this->get_right_choice_for($place)) { 207 $numright += 1; 208 } 209 } 210 return array($numright, count($this->places)); 211 } 212 213 /** 214 * Get the field name corresponding to a given place. 215 * @param int $place stem number 216 * @return string the question-type variable name. 217 */ 218 public function field($place) { 219 return 'p' . $place; 220 } 221 222 public function get_expected_data() { 223 $vars = array(); 224 foreach ($this->places as $place => $notused) { 225 $vars[$this->field($place)] = PARAM_INTEGER; 226 } 227 return $vars; 228 } 229 230 public function get_correct_response() { 231 $response = array(); 232 foreach ($this->places as $place => $notused) { 233 $response[$this->field($place)] = $this->get_right_choice_for($place); 234 } 235 return $response; 236 } 237 238 public function get_right_choice_for($place) { 239 $group = $this->places[$place]; 240 foreach ($this->choiceorder[$group] as $choicekey => $choiceid) { 241 if ($this->rightchoices[$place] == $choiceid) { 242 return $choicekey; 243 } 244 } 245 } 246 247 public function get_ordered_choices($group) { 248 $choices = array(); 249 foreach ($this->choiceorder[$group] as $choicekey => $choiceid) { 250 $choices[$choicekey] = $this->choices[$group][$choiceid]; 251 } 252 return $choices; 253 } 254 255 public function is_complete_response(array $response) { 256 $complete = true; 257 foreach ($this->places as $place => $notused) { 258 $complete = $complete && !empty($response[$this->field($place)]); 259 } 260 return $complete; 261 } 262 263 public function is_gradable_response(array $response) { 264 foreach ($this->places as $place => $notused) { 265 if (!empty($response[$this->field($place)])) { 266 return true; 267 } 268 } 269 return false; 270 } 271 272 public function is_same_response(array $prevresponse, array $newresponse) { 273 foreach ($this->places as $place => $notused) { 274 $fieldname = $this->field($place); 275 if (!question_utils::arrays_same_at_key_integer( 276 $prevresponse, $newresponse, $fieldname)) { 277 return false; 278 } 279 } 280 return true; 281 } 282 283 public function get_validation_error(array $response) { 284 if ($this->is_complete_response($response)) { 285 return ''; 286 } 287 return get_string('pleaseputananswerineachbox', 'qtype_gapselect'); 288 } 289 290 public function grade_response(array $response) { 291 list($right, $total) = $this->get_num_parts_right($response); 292 $fraction = $right / $total; 293 return array($fraction, question_state::graded_state_for_fraction($fraction)); 294 } 295 296 public function compute_final_grade($responses, $totaltries) { 297 $totalscore = 0; 298 foreach ($this->places as $place => $notused) { 299 $fieldname = $this->field($place); 300 301 $lastwrongindex = -1; 302 $finallyright = false; 303 foreach ($responses as $i => $response) { 304 if (!array_key_exists($fieldname, $response) || 305 $response[$fieldname] != $this->get_right_choice_for($place)) { 306 $lastwrongindex = $i; 307 $finallyright = false; 308 } else { 309 $finallyright = true; 310 } 311 } 312 313 if ($finallyright) { 314 $totalscore += max(0, 1 - ($lastwrongindex + 1) * $this->penalty); 315 } 316 } 317 318 return $totalscore / count($this->places); 319 } 320 321 public function classify_response(array $response) { 322 $parts = array(); 323 foreach ($this->places as $place => $group) { 324 if (!array_key_exists($this->field($place), $response) || 325 !$response[$this->field($place)]) { 326 $parts[$place] = question_classified_response::no_response(); 327 continue; 328 } 329 330 $fieldname = $this->field($place); 331 $choiceno = $this->choiceorder[$group][$response[$fieldname]]; 332 $choice = $this->choices[$group][$choiceno]; 333 $parts[$place] = new question_classified_response( 334 $choiceno, html_to_text($choice->text, 0, false), 335 ($this->get_right_choice_for($place) == $response[$fieldname]) / count($this->places)); 336 } 337 return $parts; 338 } 339 340 public function check_file_access($qa, $options, $component, $filearea, $args, $forcedownload) { 341 if ($component == 'question' && in_array($filearea, 342 array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback'))) { 343 return $this->check_combined_feedback_file_access($qa, $options, $filearea, $args); 344 345 } else if ($component == 'question' && $filearea == 'hint') { 346 return $this->check_hint_file_access($qa, $options, $args); 347 348 } else { 349 return parent::check_file_access($qa, $options, $component, $filearea, 350 $args, $forcedownload); 351 } 352 } 353 354 /** 355 * Return the question settings that define this question as structured data. 356 * 357 * @param question_attempt $qa the current attempt for which we are exporting the settings. 358 * @param question_display_options $options the question display options which say which aspects of the question 359 * should be visible. 360 * @return mixed structure representing the question settings. In web services, this will be JSON-encoded. 361 */ 362 public function get_question_definition_for_external_rendering(question_attempt $qa, question_display_options $options) { 363 // This is a partial implementation, returning only the most relevant question settings for now, 364 // ideally, we should return as much as settings as possible (depending on the state and display options). 365 366 return [ 367 'shufflechoices' => $this->shufflechoices, 368 ]; 369 } 370 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body