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