MDL-20636 Mostly working conversion of the multichoice question type.
[moodle.git] / question / type / multichoice / question.php
CommitLineData
c9c989a0
TH
1<?php
2
3// This file is part of Moodle - http://moodle.org/
4//
5// Moodle is free software: you can redistribute it and/or modify
6// it under the terms of the GNU General Public License as published by
7// the Free Software Foundation, either version 3 of the License, or
8// (at your option) any later version.
9//
10// Moodle is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13// GNU General Public License for more details.
14//
15// You should have received a copy of the GNU General Public License
16// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17
18
19/**
20 * Multiple choice question definition classes.
21 *
22 * @package qtype_multichoice
23 * @copyright 2009 The Open University
24 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25 */
26
27
28/**
29 * Base class for multiple choice questions. The parts that are common to
30 * single select and multiple select.
31 *
32 * @copyright 2009 The Open University
33 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
34 */
35abstract class qtype_multichoice_base extends question_graded_automatically {
36 const LAYOUT_DROPDOWN = 0;
37 const LAYOUT_VERTICAL = 1;
38 const LAYOUT_HORIZONTAL = 2;
39
40 public $answers;
41
42 public $shuffleanswers;
43 public $answernumbering;
44 public $layout = self::LAYOUT_VERTICAL;
45 public $correctfeedback;
46 public $partiallycorrectfeedback;
47 public $incorrectfeedback;
48
49 protected $order = null;
50
51 public function init_first_step(question_attempt_step $step) {
52 if ($step->has_qt_var('_order')) {
53 $this->order = explode(',', $step->get_qt_var('_order'));
54 } else {
55 $this->order = array_keys($this->answers);
56 if ($this->shuffleanswers) {
57 shuffle($this->order);
58 }
59 $step->set_qt_var('_order', implode(',', $this->order));
60 }
61 }
62
63 public function get_question_summary() {
64 $question = $this->html_to_text($this->questiontext);
65 $choices = array();
66 foreach ($this->order as $ansid) {
67 $choices[] = $this->html_to_text($this->answers[$ansid]->answer);
68 }
69 return $question . ': ' . implode('; ', $choices);
70 }
71
72 public function get_order(question_attempt $qa) {
73 $this->init_order($qa);
74 return $this->order;
75 }
76
77 protected function init_order(question_attempt $qa) {
78 if (is_null($this->order)) {
79 $this->order = explode(',', $qa->get_step(0)->get_qt_var('_order'));
80 }
81 }
82
83 abstract public function get_response(question_attempt $qa);
84
85 abstract public function is_choice_selected($response, $value);
86
87 function check_file_access($qa, $options, $component, $filearea, $args, $forcedownload) {
88 $itemid = reset($args);
89
90 if ($component == 'qtype_multichoice' && in_array($filearea,
91 array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback'))) {
92 $state = $qa->get_state();
93
94 if (!$state->is_finished()) {
95 $response = $qa->get_last_qt_data();
96 if (!$this->is_gradable_response($response)) {
97 return false;
98 }
99 list($notused, $state) = $qa->get_question()->grade_response($response);
100 }
101
102 return $options->feedback && $state == $filearea;
103
104 } else if ($component == 'question' && $filearea == 'answerfeedback') {
105 $answerid = reset($args); // itemid is answer id.
106 $response = $this->get_response($qa);
107 $isselected = false;
108 foreach ($this->order as $value => $ansid) {
109 if ($ansid == $answerid) {
110 $isselected = $this->is_choice_selected($response, $value);
111 break;
112 }
113 }
114 // $options->suppresschoicefeedback is a hack specific to the
115 // oumultiresponse question type. It would be good to refactor to
116 // avoid refering to it here.
117 return $options->feedback && empty($options->suppresschoicefeedback) &&
118 $isselected;
119
120 } else {
121 return parent::check_file_access($qa, $options, $component, $filearea, $args, $forcedownload);
122 }
123 }
124}
125
126
127/**
128 * Represents a multiple choice question where only one choice should be selected.
129 *
130 * @copyright 2009 The Open University
131 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
132 */
133class qtype_multichoice_single_question extends qtype_multichoice_base {
134 public function get_renderer() {
135 global $PAGE; // TODO get rid of this global.
136 return $PAGE->get_renderer('qtype_multichoice', 'single');
137 }
138
139 public function get_min_fraction() {
140 $minfraction = 0;
141 foreach ($this->answers as $ans) {
142 $minfraction = min($minfraction, $ans->fraction);
143 }
144 return $minfraction;
145 }
146
147 /**
148 * Return an array of the question type variables that could be submitted
149 * as part of a question of this type, with their types, so they can be
150 * properly cleaned.
151 * @return array variable name => PARAM_... constant.
152 */
153 public function get_expected_data() {
154 return array('answer' => PARAM_INT);
155 }
156
157 public function summarise_response(array $response) {
158 if (!array_key_exists('answer', $response) ||
159 !array_key_exists($response['answer'], $this->order)) {
160 return null;
161 }
162 $ansid = $this->order[$response['answer']];
163 return $this->html_to_text($this->answers[$ansid]->answer);
164 }
165
166 public function classify_response(array $response) {
167 if (!array_key_exists('answer', $response) ||
168 !array_key_exists($response['answer'], $this->order)) {
169 return array($this->id => question_classified_response::no_response());
170 }
171 $choiceid = $this->order[$response['answer']];
172 $ans = $this->answers[$choiceid];
173 return array($this->id => new question_classified_response($choiceid,
174 $this->html_to_text($ans->answer), $ans->fraction));
175 }
176
177 public function get_correct_response() {
178 foreach ($this->order as $key => $answerid) {
179 if (question_state::graded_state_for_fraction(
180 $this->answers[$answerid]->fraction)->is_correct()) {
181 return array('answer' => $key);
182 }
183 }
184 return array();
185 }
186
187 public function is_same_response(array $prevresponse, array $newresponse) {
188 return question_utils::arrays_same_at_key($prevresponse, $newresponse, 'answer');
189 }
190
191 public function is_complete_response(array $response) {
192 return array_key_exists('answer', $response);
193 }
194
195 public function is_gradable_response(array $response) {
196 return $this->is_complete_response($response);
197 }
198
199 public function grade_response(array $response) {
200 if (array_key_exists('answer', $response) &&
201 array_key_exists($response['answer'], $this->order)) {
202 $fraction = $this->answers[$this->order[$response['answer']]]->fraction;
203 } else {
204 $fraction = 0;
205 }
206 return array($fraction, question_state::graded_state_for_fraction($fraction));
207 }
208
209 public function get_validation_error(array $response) {
210 if ($this->is_gradable_response($response)) {
211 return '';
212 }
213 return get_string('pleaseselectananswer', 'qtype_multichoice');
214 }
215
216 public function get_response(question_attempt $qa) {
217 return $qa->get_last_qt_var('answer', -1);
218 }
219
220 public function is_choice_selected($response, $value) {
221 return $response == $value;
222 }
223}
224
225
226/**
227 * Represents a multiple choice question where multiple choices can be selected.
228 *
229 * @copyright 2009 The Open University
230 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
231 */
232class qtype_multichoice_multi_question extends qtype_multichoice_base {
233 public function get_renderer() {
234 global $PAGE; // TODO get rid of this global.
235 return $PAGE->get_renderer('qtype_multichoice', 'multi');
236 }
237
238 public function get_min_fraction() {
239 return 0;
240 }
241
242 public function clear_wrong_from_response(array $response) {
243 foreach ($this->order as $key => $ans) {
244 if (array_key_exists($this->field($key), $response) &&
245 question_state::graded_state_for_fraction(
246 $this->answers[$ans]->fraction)->is_incorrect()) {
247 $response[$this->field($key)] = 0;
248 }
249 }
250 return $response;
251 }
252
253 public function get_num_parts_right(array $response) {
254 $numright = 0;
255 foreach ($this->order as $key => $ans) {
256 $fieldname = $this->field($key);
257 if (!array_key_exists($fieldname, $response) || !$response[$fieldname]) {
258 continue;
259 }
260
261 if (!question_state::graded_state_for_fraction(
262 $this->answers[$ans]->fraction)->is_incorrect()) {
263 $numright += 1;
264 }
265 }
266 return array($numright, count($this->order));
267 }
268
269 /**
270 * @param integer $key choice number
271 * @return string the question-type variable name.
272 */
273 protected function field($key) {
274 return 'choice' . $key;
275 }
276
277 public function get_expected_data() {
278 $expected = array();
279 foreach ($this->order as $key => $notused) {
280 $expected[$this->field($key)] = PARAM_BOOL;
281 }
282 return $expected;
283 }
284
285 public function summarise_response(array $response) {
286 $selectedchoices = array();
287 foreach ($this->order as $key => $ans) {
288 $fieldname = $this->field($key);
289 if (array_key_exists($fieldname, $response) && $response[$fieldname]) {
290 $selectedchoices[] = $this->html_to_text($this->answers[$ans]->answer);
291 }
292 }
293 if (empty($selectedchoices)) {
294 return null;
295 }
296 return implode('; ', $selectedchoices);
297 }
298
299 public function classify_response(array $response) {
300 $selectedchoices = array();
301 foreach ($this->order as $key => $ansid) {
302 $fieldname = $this->field($key);
303 if (array_key_exists($fieldname, $response) && $response[$fieldname]) {
304 $selectedchoices[$ansid] = 1;
305 }
306 }
307 $choices = array();
308 foreach ($this->answers as $ansid => $ans) {
309 if (isset($selectedchoices[$ansid])) {
310 $choices[$ansid] = new question_classified_response($ansid,
311 $this->html_to_text($ans->answer), $ans->fraction);
312 }
313 }
314 return $choices;
315 }
316
317 public function get_correct_response() {
318 $response = array();
319 foreach ($this->order as $key => $ans) {
320 if (!question_state::graded_state_for_fraction(
321 $this->answers[$ans]->fraction)->is_incorrect()) {
322 $response[$this->field($key)] = 1;
323 }
324 }
325 return $response;
326 }
327
328 public function is_same_response(array $prevresponse, array $newresponse) {
329 foreach ($this->order as $key => $notused) {
330 $fieldname = $this->field($key);
331 if (!question_utils::arrays_same_at_key($prevresponse, $newresponse, $fieldname)) {
332 return false;
333 }
334 }
335 return true;
336 }
337
338 public function is_complete_response(array $response) {
339 foreach ($this->order as $key => $notused) {
340 if (!empty($response[$this->field($key)])) {
341 return true;
342 }
343 }
344 return false;
345 }
346
347 public function is_gradable_response(array $response) {
348 return $this->is_complete_response($response);
349 }
350
351 /**
352 * @param array $response responses, as returned by {@link question_attempt_step::get_qt_data()}.
353 * @return integer the number of choices that were selected. in this response.
354 */
355 public function get_num_selected_choices(array $response) {
356 $numselected = 0;
357 foreach ($response as $key => $value) {
358 if (!empty($value)) {
359 $numselected += 1;
360 }
361 }
362 return $numselected;
363 }
364
365 /**
366 * @return integer the number of choices that are correct.
367 */
368 public function get_num_correct_choices() {
369 $numcorrect = 0;
370 foreach ($this->answers as $ans) {
371 if (!question_state::graded_state_for_fraction($ans->fraction)->is_incorrect()) {
372 $numcorrect += 1;
373 }
374 }
375 return $numcorrect;
376 }
377
378 public function grade_response(array $response) {
379 $fraction = 0;
380 foreach ($this->order as $key => $ansid) {
381 if (!empty($response[$this->field($key)])) {
382 $fraction += $this->answers[$ansid]->fraction;
383 }
384 }
385 $fraction = min(max(0, $fraction), 1.0);
386 return array($fraction, question_state::graded_state_for_fraction($fraction));
387 }
388
389 public function get_validation_error(array $response) {
390 if ($this->is_gradable_response($response)) {
391 return '';
392 }
393 return get_string('pleaseselectatleastoneanswer', 'qtype_multichoice');
394 }
395
396 /**
397 * Disable those hint settings that we don't want when the student has selected
398 * more choices than the number of right choices. This avoids giving the game away.
399 * @param question_hint_with_parts $hint a hint.
400 */
401 protected function disable_hint_settings_when_too_many_selected(question_hint_with_parts $hint) {
402 $hint->clearwrong = false;
403 }
404
405 public function get_hint($hintnumber, question_attempt $qa) {
406 $hint = parent::get_hint($hintnumber, $qa);
407 if (is_null($hint)) {
408 return $hint;
409 }
410
411 if ($this->get_num_selected_choices($qa->get_last_qt_data()) >
412 $this->get_num_correct_choices()) {
413 $hint = clone($hint);
414 $this->disable_hint_settings_when_too_many_selected($hint);
415 }
416 return $hint;
417 }
418
419 public function get_response(question_attempt $qa) {
420 return $qa->get_last_qt_data();
421 }
422
423 public function is_choice_selected($response, $value) {
424 return !empty($response['choice' . $value]);
425 }
426}