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