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