Differences Between: [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403] [Versions 39 and 310]
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 get_question_summary() { 116 $question = $this->html_to_text($this->questiontext, $this->questiontextformat); 117 $groups = array(); 118 foreach ($this->choices as $group => $choices) { 119 $cs = array(); 120 foreach ($choices as $choice) { 121 $cs[] = html_to_text($choice->text, 0, false); 122 } 123 $groups[] = '[[' . $group . ']] -> {' . implode(' / ', $cs) . '}'; 124 } 125 return $question . '; ' . implode('; ', $groups); 126 } 127 128 protected function get_selected_choice($group, $shuffledchoicenumber) { 129 $choiceno = $this->choiceorder[$group][$shuffledchoicenumber]; 130 return isset($this->choices[$group][$choiceno]) ? $this->choices[$group][$choiceno] : null; 131 } 132 133 public function summarise_response(array $response) { 134 $matches = array(); 135 $allblank = true; 136 foreach ($this->places as $place => $group) { 137 if (array_key_exists($this->field($place), $response) && 138 $response[$this->field($place)]) { 139 $choices[] = '{' . $this->summarise_choice( 140 $this->get_selected_choice($group, $response[$this->field($place)])) . '}'; 141 $allblank = false; 142 } else { 143 $choices[] = '{}'; 144 } 145 } 146 if ($allblank) { 147 return null; 148 } 149 return implode(' ', $choices); 150 } 151 152 /** 153 * Convert a choice to plain text. 154 * @param qtype_gapselect_choice $choice one of the choices for a place. 155 * @return a plain text summary of the choice. 156 */ 157 public function summarise_choice($choice) { 158 return $this->html_to_text($choice->text, FORMAT_PLAIN); 159 } 160 161 public function get_random_guess_score() { 162 $accum = 0; 163 164 foreach ($this->places as $placegroup) { 165 $accum += 1 / count($this->choices[$placegroup]); 166 } 167 168 return $accum / count($this->places); 169 } 170 171 public function clear_wrong_from_response(array $response) { 172 foreach ($this->places as $place => $notused) { 173 if (array_key_exists($this->field($place), $response) && 174 $response[$this->field($place)] != $this->get_right_choice_for($place)) { 175 $response[$this->field($place)] = '0'; 176 } 177 } 178 return $response; 179 } 180 181 public function get_num_parts_right(array $response) { 182 $numright = 0; 183 foreach ($this->places as $place => $notused) { 184 if (!array_key_exists($this->field($place), $response)) { 185 continue; 186 } 187 if ($response[$this->field($place)] == $this->get_right_choice_for($place)) { 188 $numright += 1; 189 } 190 } 191 return array($numright, count($this->places)); 192 } 193 194 /** 195 * Get the field name corresponding to a given place. 196 * @param int $place stem number 197 * @return string the question-type variable name. 198 */ 199 public function field($place) { 200 return 'p' . $place; 201 } 202 203 public function get_expected_data() { 204 $vars = array(); 205 foreach ($this->places as $place => $notused) { 206 $vars[$this->field($place)] = PARAM_INTEGER; 207 } 208 return $vars; 209 } 210 211 public function get_correct_response() { 212 $response = array(); 213 foreach ($this->places as $place => $notused) { 214 $response[$this->field($place)] = $this->get_right_choice_for($place); 215 } 216 return $response; 217 } 218 219 public function get_right_choice_for($place) { 220 $group = $this->places[$place]; 221 foreach ($this->choiceorder[$group] as $choicekey => $choiceid) { 222 if ($this->rightchoices[$place] == $choiceid) { 223 return $choicekey; 224 } 225 } 226 } 227 228 public function get_ordered_choices($group) { 229 $choices = array(); 230 foreach ($this->choiceorder[$group] as $choicekey => $choiceid) { 231 $choices[$choicekey] = $this->choices[$group][$choiceid]; 232 } 233 return $choices; 234 } 235 236 public function is_complete_response(array $response) { 237 $complete = true; 238 foreach ($this->places as $place => $notused) { 239 $complete = $complete && !empty($response[$this->field($place)]); 240 } 241 return $complete; 242 } 243 244 public function is_gradable_response(array $response) { 245 foreach ($this->places as $place => $notused) { 246 if (!empty($response[$this->field($place)])) { 247 return true; 248 } 249 } 250 return false; 251 } 252 253 public function is_same_response(array $prevresponse, array $newresponse) { 254 foreach ($this->places as $place => $notused) { 255 $fieldname = $this->field($place); 256 if (!question_utils::arrays_same_at_key_integer( 257 $prevresponse, $newresponse, $fieldname)) { 258 return false; 259 } 260 } 261 return true; 262 } 263 264 public function get_validation_error(array $response) { 265 if ($this->is_complete_response($response)) { 266 return ''; 267 } 268 return get_string('pleaseputananswerineachbox', 'qtype_gapselect'); 269 } 270 271 public function grade_response(array $response) { 272 list($right, $total) = $this->get_num_parts_right($response); 273 $fraction = $right / $total; 274 return array($fraction, question_state::graded_state_for_fraction($fraction)); 275 } 276 277 public function compute_final_grade($responses, $totaltries) { 278 $totalscore = 0; 279 foreach ($this->places as $place => $notused) { 280 $fieldname = $this->field($place); 281 282 $lastwrongindex = -1; 283 $finallyright = false; 284 foreach ($responses as $i => $response) { 285 if (!array_key_exists($fieldname, $response) || 286 $response[$fieldname] != $this->get_right_choice_for($place)) { 287 $lastwrongindex = $i; 288 $finallyright = false; 289 } else { 290 $finallyright = true; 291 } 292 } 293 294 if ($finallyright) { 295 $totalscore += max(0, 1 - ($lastwrongindex + 1) * $this->penalty); 296 } 297 } 298 299 return $totalscore / count($this->places); 300 } 301 302 public function classify_response(array $response) { 303 $parts = array(); 304 foreach ($this->places as $place => $group) { 305 if (!array_key_exists($this->field($place), $response) || 306 !$response[$this->field($place)]) { 307 $parts[$place] = question_classified_response::no_response(); 308 continue; 309 } 310 311 $fieldname = $this->field($place); 312 $choiceno = $this->choiceorder[$group][$response[$fieldname]]; 313 $choice = $this->choices[$group][$choiceno]; 314 $parts[$place] = new question_classified_response( 315 $choiceno, html_to_text($choice->text, 0, false), 316 ($this->get_right_choice_for($place) == $response[$fieldname]) / count($this->places)); 317 } 318 return $parts; 319 } 320 321 public function check_file_access($qa, $options, $component, $filearea, $args, $forcedownload) { 322 if ($component == 'question' && in_array($filearea, 323 array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback'))) { 324 return $this->check_combined_feedback_file_access($qa, $options, $filearea, $args); 325 326 } else if ($component == 'question' && $filearea == 'hint') { 327 return $this->check_hint_file_access($qa, $options, $args); 328 329 } else { 330 return parent::check_file_access($qa, $options, $component, $filearea, 331 $args, $forcedownload); 332 } 333 } 334 335 /** 336 * Return the question settings that define this question as structured data. 337 * 338 * @param question_attempt $qa the current attempt for which we are exporting the settings. 339 * @param question_display_options $options the question display options which say which aspects of the question 340 * should be visible. 341 * @return mixed structure representing the question settings. In web services, this will be JSON-encoded. 342 */ 343 public function get_question_definition_for_external_rendering(question_attempt $qa, question_display_options $options) { 344 // This is a partial implementation, returning only the most relevant question settings for now, 345 // ideally, we should return as much as settings as possible (depending on the state and display options). 346 347 return [ 348 'shufflechoices' => $this->shufflechoices, 349 ]; 350 } 351 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body