Commit | Line | Data |
---|---|---|
ab50232b | 1 | <?php |
ab50232b 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 | ||
17 | ||
18 | /** | |
19 | * Multianswer question definition class. | |
20 | * | |
21 | * @package qtype | |
22 | * @subpackage multianswer | |
23 | * @copyright 2010 Pierre Pichet | |
24 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
25 | */ | |
26 | ||
27 | require_once($CFG->dirroot . '/question/type/shortanswer/question.php'); | |
28 | require_once($CFG->dirroot . '/question/type/numerical/question.php'); | |
29 | require_once($CFG->dirroot . '/question/type/multichoice/question.php'); | |
30 | ||
31 | ||
32 | /** | |
33 | * Represents a multianswer question. | |
34 | * | |
35 | * A multi-answer question is made of of several subquestions of various types. | |
36 | * You can think of it as an application of the composite pattern to qusetion | |
37 | * types. | |
38 | * | |
39 | * @copyright 2010 Pierre Pichet | |
40 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
41 | */ | |
c2f056a9 | 42 | class qtype_multianswer_question extends question_graded_automatically_with_countback { |
ab50232b TH |
43 | /** @var array of question_graded_automatically. */ |
44 | public $subquestions = array(); | |
45 | ||
42a5b055 TH |
46 | /** |
47 | * @var array place number => insex in the $subquestions array. Places are | |
48 | * numbered from 1. | |
49 | */ | |
50 | public $places; | |
51 | ||
52 | /** | |
53 | * @var array of strings, one longer than $places, which is achieved by | |
54 | * indexing from 0. The bits of question text that go between the subquestions. | |
55 | */ | |
56 | public $textfragments; | |
57 | ||
ab50232b TH |
58 | /** |
59 | * Get a question_attempt_step_subquestion_adapter | |
60 | * @param question_attempt_step $step the step to adapt. | |
61 | * @param int $i the subquestion index. | |
62 | * @return question_attempt_step_subquestion_adapter. | |
63 | */ | |
64 | protected function get_substep($step, $i) { | |
65 | return new question_attempt_step_subquestion_adapter($step, 'sub' . $i . '_'); | |
66 | } | |
67 | ||
1da821bb | 68 | public function start_attempt(question_attempt_step $step, $variant) { |
ab50232b | 69 | foreach ($this->subquestions as $i => $subq) { |
1da821bb | 70 | $subq->start_attempt($this->get_substep($step, $i), $variant); |
ab50232b TH |
71 | } |
72 | } | |
73 | ||
74 | public function apply_attempt_state(question_attempt_step $step) { | |
75 | foreach ($this->subquestions as $i => $subq) { | |
76 | $subq->apply_attempt_state($this->get_substep($step, $i)); | |
ab50232b TH |
77 | } |
78 | } | |
79 | ||
fa6c8620 TH |
80 | public function get_question_summary() { |
81 | $summary = $this->html_to_text($this->questiontext, $this->questiontextformat); | |
82 | foreach ($this->subquestions as $i => $subq) { | |
83 | switch ($subq->qtype->name()) { | |
84 | case 'multichoice': | |
85 | $choices = array(); | |
86 | $dummyqa = new question_attempt($subq, $this->contextid); | |
87 | foreach ($subq->get_order($dummyqa) as $ansid) { | |
88 | $choices[] = $this->html_to_text($subq->answers[$ansid]->answer, | |
89 | $subq->answers[$ansid]->answerformat); | |
90 | } | |
91 | $answerbit = '{' . implode('; ', $choices) . '}'; | |
92 | break; | |
93 | case 'numerical': | |
94 | case 'shortanswer': | |
95 | $answerbit = '_____'; | |
96 | break; | |
97 | default: | |
98 | $answerbit = '{ERR unknown sub-question type}'; | |
99 | } | |
100 | $summary = str_replace('{#' . $i . '}', $answerbit, $summary); | |
101 | } | |
102 | return $summary; | |
103 | } | |
ab50232b TH |
104 | |
105 | public function get_min_fraction() { | |
106 | $fractionsum = 0; | |
107 | $fractionmax = 0; | |
108 | foreach ($this->subquestions as $i => $subq) { | |
109 | $fractionmax += $subq->defaultmark; | |
110 | $fractionsum += $subq->defaultmark * $subq->get_min_fraction(); | |
111 | } | |
112 | return $fractionsum / $fractionmax; | |
113 | } | |
114 | ||
4e3d8293 TH |
115 | public function get_max_fraction() { |
116 | $fractionsum = 0; | |
117 | $fractionmax = 0; | |
118 | foreach ($this->subquestions as $i => $subq) { | |
119 | $fractionmax += $subq->defaultmark; | |
120 | $fractionsum += $subq->defaultmark * $subq->get_max_fraction(); | |
121 | } | |
122 | return $fractionsum / $fractionmax; | |
123 | } | |
124 | ||
ab50232b TH |
125 | public function get_expected_data() { |
126 | $expected = array(); | |
127 | foreach ($this->subquestions as $i => $subq) { | |
128 | $substep = $this->get_substep(null, $i); | |
129 | foreach ($subq->get_expected_data() as $name => $type) { | |
7ac7977c | 130 | if ($subq->qtype->name() == 'multichoice' && |
dcedbb0e | 131 | $subq->layout == qtype_multichoice_base::LAYOUT_DROPDOWN) { |
7ac7977c TH |
132 | // Hack or MC inline does not work. |
133 | $expected[$substep->add_prefix($name)] = PARAM_RAW; | |
134 | } else { | |
135 | $expected[$substep->add_prefix($name)] = $type; | |
136 | } | |
ab50232b TH |
137 | } |
138 | } | |
139 | return $expected; | |
140 | } | |
141 | ||
142 | public function get_correct_response() { | |
143 | $right = array(); | |
144 | foreach ($this->subquestions as $i => $subq) { | |
145 | $substep = $this->get_substep(null, $i); | |
fa6c8620 | 146 | foreach ($subq->get_correct_response() as $name => $type) { |
ab50232b TH |
147 | $right[$substep->add_prefix($name)] = $type; |
148 | } | |
149 | } | |
150 | return $right; | |
151 | } | |
152 | ||
a3c5150b JP |
153 | public function prepare_simulated_post_data($simulatedresponse) { |
154 | $postdata = array(); | |
155 | foreach ($this->subquestions as $i => $subq) { | |
156 | $substep = $this->get_substep(null, $i); | |
157 | foreach ($subq->prepare_simulated_post_data($simulatedresponse[$i]) as $name => $value) { | |
158 | $postdata[$substep->add_prefix($name)] = $value; | |
159 | } | |
160 | } | |
161 | return $postdata; | |
162 | } | |
163 | ||
58794ac9 JP |
164 | public function get_student_response_values_for_simulation($postdata) { |
165 | $simulatedresponse = array(); | |
166 | foreach ($this->subquestions as $i => $subq) { | |
167 | $substep = $this->get_substep(null, $i); | |
168 | $subqpostdata = $substep->filter_array($postdata); | |
169 | $subqsimulatedresponse = $subq->get_student_response_values_for_simulation($subqpostdata); | |
170 | foreach ($subqsimulatedresponse as $subresponsekey => $responsevalue) { | |
171 | $simulatedresponse[$i.'.'.$subresponsekey] = $responsevalue; | |
172 | } | |
173 | } | |
174 | ksort($simulatedresponse); | |
175 | return $simulatedresponse; | |
176 | } | |
177 | ||
ab50232b TH |
178 | public function is_complete_response(array $response) { |
179 | foreach ($this->subquestions as $i => $subq) { | |
180 | $substep = $this->get_substep(null, $i); | |
181 | if (!$subq->is_complete_response($substep->filter_array($response))) { | |
182 | return false; | |
183 | } | |
184 | } | |
185 | return true; | |
186 | } | |
187 | ||
188 | public function is_gradable_response(array $response) { | |
189 | foreach ($this->subquestions as $i => $subq) { | |
190 | $substep = $this->get_substep(null, $i); | |
191 | if ($subq->is_gradable_response($substep->filter_array($response))) { | |
192 | return true; | |
193 | } | |
194 | } | |
195 | return false; | |
196 | } | |
197 | ||
198 | public function is_same_response(array $prevresponse, array $newresponse) { | |
199 | foreach ($this->subquestions as $i => $subq) { | |
200 | $substep = $this->get_substep(null, $i); | |
201 | if (!$subq->is_same_response($substep->filter_array($prevresponse), | |
202 | $substep->filter_array($newresponse))) { | |
203 | return false; | |
204 | } | |
205 | } | |
206 | return true; | |
207 | } | |
208 | ||
209 | public function get_validation_error(array $response) { | |
86969816 TH |
210 | if ($this->is_complete_response($response)) { |
211 | return ''; | |
ab50232b | 212 | } |
86969816 | 213 | return get_string('pleaseananswerallparts', 'qtype_multianswer'); |
ab50232b TH |
214 | } |
215 | ||
216 | /** | |
217 | * Used by grade_response to combine the states of the subquestions. | |
218 | * The combined state is accumulates in $overallstate. That will be right | |
219 | * if all the separate states are right; and wrong if all the separate states | |
220 | * are wrong, otherwise, it will be partially right. | |
221 | * @param question_state $overallstate the result so far. | |
222 | * @param question_state $newstate the new state to add to the combination. | |
223 | * @return question_state the new combined state. | |
224 | */ | |
225 | protected function combine_states($overallstate, $newstate) { | |
226 | if (is_null($overallstate)) { | |
227 | return $newstate; | |
228 | } else if ($overallstate == question_state::$gaveup && | |
229 | $newstate == question_state::$gaveup) { | |
230 | return question_state::$gaveup; | |
231 | } else if ($overallstate == question_state::$gaveup && | |
232 | $newstate == question_state::$gradedwrong) { | |
233 | return question_state::$gradedwrong; | |
234 | } else if ($overallstate == question_state::$gradedwrong && | |
235 | $newstate == question_state::$gaveup) { | |
236 | return question_state::$gradedwrong; | |
237 | } else if ($overallstate == question_state::$gradedwrong && | |
238 | $newstate == question_state::$gradedwrong) { | |
239 | return question_state::$gradedwrong; | |
240 | } else if ($overallstate == question_state::$gradedright && | |
241 | $newstate == question_state::$gradedright) { | |
242 | return question_state::$gradedright; | |
243 | } else { | |
244 | return question_state::$gradedpartial; | |
245 | } | |
246 | } | |
247 | ||
248 | public function grade_response(array $response) { | |
249 | $overallstate = null; | |
250 | $fractionsum = 0; | |
251 | $fractionmax = 0; | |
252 | foreach ($this->subquestions as $i => $subq) { | |
253 | $fractionmax += $subq->defaultmark; | |
254 | $substep = $this->get_substep(null, $i); | |
255 | $subresp = $substep->filter_array($response); | |
256 | if (!$subq->is_gradable_response($subresp)) { | |
257 | $overallstate = $this->combine_states($overallstate, question_state::$gaveup); | |
258 | } else { | |
259 | list($subfraction, $newstate) = $subq->grade_response($subresp); | |
260 | $fractionsum += $subfraction * $subq->defaultmark; | |
261 | $overallstate = $this->combine_states($overallstate, $newstate); | |
262 | } | |
263 | } | |
264 | return array($fractionsum / $fractionmax, $overallstate); | |
265 | } | |
266 | ||
e9af6091 JMV |
267 | public function clear_wrong_from_response(array $response) { |
268 | foreach ($this->subquestions as $i => $subq) { | |
269 | $substep = $this->get_substep(null, $i); | |
270 | $subresp = $substep->filter_array($response); | |
271 | list($subfraction, $newstate) = $subq->grade_response($subresp); | |
272 | if ($newstate != question_state::$gradedright) { | |
273 | foreach ($subresp as $ind => $resp) { | |
d55399a0 JMV |
274 | if ($subq->qtype == 'multichoice' && ($subq->layout == qtype_multichoice_base::LAYOUT_VERTICAL |
275 | || $subq->layout == qtype_multichoice_base::LAYOUT_HORIZONTAL)) { | |
e9af6091 JMV |
276 | $response[$substep->add_prefix($ind)] = '-1'; |
277 | } else { | |
278 | $response[$substep->add_prefix($ind)] = ''; | |
279 | } | |
280 | } | |
281 | } | |
282 | } | |
283 | return $response; | |
284 | } | |
285 | ||
286 | public function get_num_parts_right(array $response) { | |
287 | $numright = 0; | |
288 | foreach ($this->subquestions as $i => $subq) { | |
289 | $substep = $this->get_substep(null, $i); | |
290 | $subresp = $substep->filter_array($response); | |
291 | list($subfraction, $newstate) = $subq->grade_response($subresp); | |
292 | if ($newstate == question_state::$gradedright) { | |
293 | $numright += 1; | |
294 | } | |
295 | } | |
296 | return array($numright, count($this->subquestions)); | |
297 | } | |
298 | ||
c2f056a9 JMV |
299 | public function compute_final_grade($responses, $totaltries) { |
300 | $fractionsum = 0; | |
301 | $fractionmax = 0; | |
302 | foreach ($this->subquestions as $i => $subq) { | |
303 | $fractionmax += $subq->defaultmark; | |
304 | ||
305 | $lastresponse = array(); | |
306 | $lastchange = 0; | |
307 | $subfraction = 0; | |
308 | foreach ($responses as $responseindex => $response) { | |
309 | $substep = $this->get_substep(null, $i); | |
310 | $subresp = $substep->filter_array($response); | |
311 | if ($subq->is_same_response($lastresponse, $subresp)) { | |
312 | continue; | |
313 | } | |
314 | $lastresponse = $subresp; | |
315 | $lastchange = $responseindex; | |
316 | list($subfraction, $newstate) = $subq->grade_response($subresp); | |
317 | } | |
318 | ||
319 | $fractionsum += $subq->defaultmark * max(0, $subfraction - $lastchange * $this->penalty); | |
320 | } | |
321 | ||
322 | return $fractionsum / $fractionmax; | |
323 | } | |
324 | ||
ab50232b TH |
325 | public function summarise_response(array $response) { |
326 | $summary = array(); | |
327 | foreach ($this->subquestions as $i => $subq) { | |
328 | $substep = $this->get_substep(null, $i); | |
329 | $a = new stdClass(); | |
330 | $a->i = $i; | |
331 | $a->response = $subq->summarise_response($substep->filter_array($response)); | |
332 | $summary[] = get_string('subqresponse', 'qtype_multianswer', $a); | |
333 | } | |
334 | ||
335 | return implode('; ', $summary); | |
336 | } | |
337 | ||
338 | public function check_file_access($qa, $options, $component, $filearea, $args, $forcedownload) { | |
03b2b8fa TH |
339 | if ($component == 'question' && $filearea == 'answer') { |
340 | return true; | |
341 | ||
342 | } else if ($component == 'question' && $filearea == 'answerfeedback') { | |
343 | // Full logic to control which feedbacks a student can see is too complex. | |
344 | // Just allow access to all images. There is a theoretical chance the | |
345 | // students could see files they are not meant to see by guessing URLs, | |
346 | // but it is remote. | |
347 | return $options->feedback; | |
348 | ||
349 | } else if ($component == 'question' && $filearea == 'hint') { | |
350 | return $this->check_hint_file_access($qa, $options, $args); | |
351 | ||
352 | } else { | |
353 | return parent::check_file_access($qa, $options, $component, $filearea, | |
354 | $args, $forcedownload); | |
355 | } | |
ab50232b TH |
356 | } |
357 | } |