MDL-47494 gapselect: Fix unit tests for 3.0 compatibility.
[moodle.git] / question / type / gapselect / questionbase.php
CommitLineData
0d24b17a 1<?php
0d24b17a
TH
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
0d24b17a 17/**
647d9373
TH
18 * Definition class for embedded element in question text question.
19 *
20 * Used by gap-select, drag and drop and possibly others.
0d24b17a 21 *
8d6fb0c6 22 * @package qtype_gapselect
9df0480d
TH
23 * @copyright 2011 The Open University
24 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
0d24b17a
TH
25 */
26
27
b28ad86a
TH
28defined('MOODLE_INTERNAL') || die();
29
30
0d24b17a 31/**
647d9373
TH
32 * Represents embedded element in question text question.
33 *
34 * Parent of drag and drop and select from drop down list and others.
0d24b17a 35 *
9df0480d
TH
36 * @copyright 2011 The Open University
37 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
0d24b17a 38 */
43df6cac 39abstract class qtype_gapselect_question_base extends question_graded_automatically_with_countback {
0d24b17a
TH
40 /** @var boolean Whether the question stems should be shuffled. */
41 public $shufflechoices;
42
647d9373 43 /** @var string Feedback for any correct response. */
0d24b17a 44 public $correctfeedback;
647d9373
TH
45 /** @var int format of $correctfeedback. */
46 public $correctfeedbackformat;
47 /** @var string Feedback for any partially correct response. */
0d24b17a 48 public $partiallycorrectfeedback;
647d9373
TH
49 /** @var int format of $partiallycorrectfeedback. */
50 public $partiallycorrectfeedbackformat;
51 /** @var string Feedback for any incorrect response. */
0d24b17a 52 public $incorrectfeedback;
647d9373
TH
53 /** @var int format of $incorrectfeedback. */
54 public $incorrectfeedbackformat;
4dc37d69 55
0d24b17a
TH
56 /** @var array of arrays. The keys are the choice group numbers. The values
57 * are arrays of qtype_gapselect_choice objects. */
58 public $choices;
59
60 /**
61 * @var array place number => group number of the places in the question
62 * text where choices can be put. Places are numbered from 1.
63 */
64 public $places;
65
66 /**
67 * @var array of strings, one longer than $places, which is achieved by
68 * indexing from 0. The bits of question text that go between the placeholders.
69 */
70 public $textfragments;
71
72 /** @var array index of the right choice for each stem. */
73 public $rightchoices;
74
75 /** @var array shuffled choice indexes. */
76 protected $choiceorder;
77
a2d6ea7a 78 public function start_attempt(question_attempt_step $step, $variant) {
0d24b17a 79 foreach ($this->choices as $group => $choices) {
6ec101ac
TH
80 $choiceorder = array_keys($choices);
81 if ($this->shufflechoices) {
82 shuffle($choiceorder);
0d24b17a 83 }
6ec101ac
TH
84 $step->set_qt_var('_choiceorder' . $group, implode(',', $choiceorder));
85 $this->set_choiceorder($group, $choiceorder);
86 }
87 }
0d24b17a 88
6ec101ac
TH
89 public function apply_attempt_state(question_attempt_step $step) {
90 foreach ($this->choices as $group => $choices) {
91 $this->set_choiceorder($group, explode(',',
92 $step->get_qt_var('_choiceorder' . $group)));
93 }
94 }
0d24b17a 95
6ec101ac
TH
96 /**
97 * Helper method used by both {@link start_attempt()} and
98 * {@link apply_attempt_state()}.
99 * @param int $group the group number.
100 * @param array $choiceorder the choices, in order.
101 */
102 protected function set_choiceorder($group, $choiceorder) {
103 foreach ($choiceorder as $key => $value) {
104 $this->choiceorder[$group][$key + 1] = $value;
0d24b17a
TH
105 }
106 }
107
108 public function get_question_summary() {
4ffe7ac4 109 $question = $this->html_to_text($this->questiontext, $this->questiontextformat);
0d24b17a
TH
110 $groups = array();
111 foreach ($this->choices as $group => $choices) {
112 $cs = array();
113 foreach ($choices as $choice) {
4ffe7ac4 114 $cs[] = html_to_text($choice->text, 0, false);
0d24b17a
TH
115 }
116 $groups[] = '[[' . $group . ']] -> {' . implode(' / ', $cs) . '}';
117 }
118 return $question . '; ' . implode('; ', $groups);
119 }
120
121 protected function get_selected_choice($group, $shuffledchoicenumber) {
122 $choiceno = $this->choiceorder[$group][$shuffledchoicenumber];
123 return $this->choices[$group][$choiceno];
124 }
125
126 public function summarise_response(array $response) {
127 $matches = array();
128 $allblank = true;
129 foreach ($this->places as $place => $group) {
130 if (array_key_exists($this->field($place), $response) &&
131 $response[$this->field($place)]) {
a719f4d2
TH
132 $choices[] = '{' . $this->summarise_choice(
133 $this->get_selected_choice($group, $response[$this->field($place)])) . '}';
0d24b17a
TH
134 $allblank = false;
135 } else {
136 $choices[] = '{}';
137 }
138 }
139 if ($allblank) {
140 return null;
141 }
142 return implode(' ', $choices);
143 }
144
a719f4d2
TH
145 /**
146 * Convert a choice to plain text.
147 * @param qtype_gapselect_choice $choice one of the choices for a place.
148 * @return a plain text summary of the choice.
149 */
150 public function summarise_choice($choice) {
151 return $this->html_to_text($choice->text, FORMAT_PLAIN);
152 }
153
0d24b17a
TH
154 public function get_random_guess_score() {
155 $accum = 0;
156
157 foreach ($this->places as $placegroup) {
158 $accum += 1 / count($this->choices[$placegroup]);
159 }
160
161 return $accum / count($this->places);
162 }
163
164 public function clear_wrong_from_response(array $response) {
165 foreach ($this->places as $place => $notused) {
166 if (array_key_exists($this->field($place), $response) &&
167 $response[$this->field($place)] != $this->get_right_choice_for($place)) {
168 $response[$this->field($place)] = '0';
169 }
170 }
171 return $response;
172 }
173
174 public function get_num_parts_right(array $response) {
175 $numright = 0;
176 foreach ($this->places as $place => $notused) {
177 if (!array_key_exists($this->field($place), $response)) {
178 continue;
179 }
180 if ($response[$this->field($place)] == $this->get_right_choice_for($place)) {
181 $numright += 1;
182 }
183 }
184 return array($numright, count($this->places));
185 }
186
187 /**
647d9373
TH
188 * Get the field name corresponding to a given place.
189 * @param int $place stem number
0d24b17a
TH
190 * @return string the question-type variable name.
191 */
192 public function field($place) {
193 return 'p' . $place;
194 }
195
196 public function get_expected_data() {
197 $vars = array();
198 foreach ($this->places as $place => $notused) {
199 $vars[$this->field($place)] = PARAM_INTEGER;
200 }
201 return $vars;
202 }
203
204 public function get_correct_response() {
205 $response = array();
206 foreach ($this->places as $place => $notused) {
207 $response[$this->field($place)] = $this->get_right_choice_for($place);
208 }
209 return $response;
210 }
211
212 public function get_right_choice_for($place) {
213 $group = $this->places[$place];
214 foreach ($this->choiceorder[$group] as $choicekey => $choiceid) {
215 if ($this->rightchoices[$place] == $choiceid) {
216 return $choicekey;
217 }
218 }
219 }
220
221 public function get_ordered_choices($group) {
222 $choices = array();
223 foreach ($this->choiceorder[$group] as $choicekey => $choiceid) {
224 $choices[$choicekey] = $this->choices[$group][$choiceid];
225 }
226 return $choices;
227 }
228
229 public function is_complete_response(array $response) {
230 $complete = true;
231 foreach ($this->places as $place => $notused) {
232 $complete = $complete && !empty($response[$this->field($place)]);
233 }
234 return $complete;
235 }
236
237 public function is_gradable_response(array $response) {
238 foreach ($this->places as $place => $notused) {
239 if (!empty($response[$this->field($place)])) {
240 return true;
241 }
242 }
243 return false;
244 }
245
246 public function is_same_response(array $prevresponse, array $newresponse) {
247 foreach ($this->places as $place => $notused) {
248 $fieldname = $this->field($place);
249 if (!question_utils::arrays_same_at_key_integer(
250 $prevresponse, $newresponse, $fieldname)) {
251 return false;
252 }
253 }
254 return true;
255 }
256
257 public function get_validation_error(array $response) {
258 if ($this->is_complete_response($response)) {
259 return '';
260 }
261 return get_string('pleaseputananswerineachbox', 'qtype_gapselect');
262 }
263
264 public function grade_response(array $response) {
265 list($right, $total) = $this->get_num_parts_right($response);
266 $fraction = $right / $total;
267 return array($fraction, question_state::graded_state_for_fraction($fraction));
268 }
269
270 public function compute_final_grade($responses, $totaltries) {
271 $totalscore = 0;
272 foreach ($this->places as $place => $notused) {
273 $fieldname = $this->field($place);
274
275 $lastwrongindex = -1;
276 $finallyright = false;
277 foreach ($responses as $i => $response) {
278 if (!array_key_exists($fieldname, $response) ||
279 $response[$fieldname] != $this->get_right_choice_for($place)) {
280 $lastwrongindex = $i;
281 $finallyright = false;
282 } else {
283 $finallyright = true;
284 }
285 }
286
287 if ($finallyright) {
288 $totalscore += max(0, 1 - ($lastwrongindex + 1) * $this->penalty);
289 }
290 }
291
292 return $totalscore / count($this->places);
293 }
294
295 public function classify_response(array $response) {
296 $parts = array();
297 foreach ($this->places as $place => $group) {
298 if (!array_key_exists($this->field($place), $response) ||
299 !$response[$this->field($place)]) {
300 $parts[$place] = question_classified_response::no_response();
301 continue;
302 }
303
304 $fieldname = $this->field($place);
305 $choiceno = $this->choiceorder[$group][$response[$fieldname]];
306 $choice = $this->choices[$group][$choiceno];
307 $parts[$place] = new question_classified_response(
4ffe7ac4 308 $choiceno, html_to_text($choice->text, 0, false),
647d9373 309 ($this->get_right_choice_for($place) == $response[$fieldname]) / count($this->places));
0d24b17a
TH
310 }
311 return $parts;
312 }
e45d37bb
TH
313
314 public function check_file_access($qa, $options, $component, $filearea, $args, $forcedownload) {
315 if ($component == 'question' && in_array($filearea,
316 array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback'))) {
317 return $this->check_combined_feedback_file_access($qa, $options, $filearea);
318
319 } else if ($component == 'question' && $filearea == 'hint') {
320 return $this->check_hint_file_access($qa, $options, $args);
321
322 } else {
060e0294
TH
323 return parent::check_file_access($qa, $options, $component, $filearea,
324 $args, $forcedownload);
e45d37bb
TH
325 }
326 }
0d24b17a 327}