MDL-20636 Finish making ddwtos work, mostly. Also various other JS fixes.
[moodle.git] / question / type / gapselect / questionbase.php
1 <?php
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/>.
19 /**
20  * Definition class for embedded element in question text question. Parent of
21  * gap-select, drag and drop and possibly others.
22  *
23  * @package qtype
24  * @subpackage gapselect
25  * @copyright 2011 The Open University
26  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27  */
30 /**
31  * Represents embedded element in question text question. Parent of drag and drop and select from
32  * drop down list and ?others?
33  *
34  * @copyright 2009 The Open University
35  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
36  */
37 abstract class qtype_gapselect_question_base extends question_graded_automatically_with_countback {
38     /** @var boolean Whether the question stems should be shuffled. */
39     public $shufflechoices;
41     public $correctfeedback;
42     public $partiallycorrectfeedback;
43     public $incorrectfeedback;
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;
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;
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;
61     /** @var array index of the right choice for each stem. */
62     public $rightchoices;
64     /** @var array shuffled choice indexes. */
65     protected $choiceorder;
67     public function init_first_step(question_attempt_step $step) {
68         foreach ($this->choices as $group => $choices) {
69             $varname = '_choiceorder' . $group;
71             if ($step->has_qt_var($varname)) {
72                 $choiceorder = explode(',', $step->get_qt_var($varname));
74             } else {
75                 $choiceorder = array_keys($choices);
76                 if ($this->shufflechoices) {
77                     shuffle($choiceorder);
78                 }
79             }
81             foreach ($choiceorder as $key => $value) {
82                 $this->choiceorder[$group][$key + 1] = $value;
83             }
85             if (!$step->has_qt_var($varname)) {
86                 $step->set_qt_var($varname, implode(',', $this->choiceorder[$group]));
87             }
88         }
89     }
91     public function get_question_summary() {
92         $question = $this->html_to_text($this->questiontext);
93         $groups = array();
94         foreach ($this->choices as $group => $choices) {
95             $cs = array();
96             foreach ($choices as $choice) {
97                 $cs[] = $this->html_to_text($choice->text);
98             }
99             $groups[] = '[[' . $group . ']] -> {' . implode(' / ', $cs) . '}';
100         }
101         return $question . '; ' . implode('; ', $groups);
102     }
104     protected function get_selected_choice($group, $shuffledchoicenumber) {
105         $choiceno = $this->choiceorder[$group][$shuffledchoicenumber];
106         return $this->choices[$group][$choiceno];
107     }
109     public function summarise_response(array $response) {
110         $matches = array();
111         $allblank = true;
112         foreach ($this->places as $place => $group) {
113             if (array_key_exists($this->field($place), $response) &&
114                     $response[$this->field($place)]) {
115                 $choices[] = '{' . $this->html_to_text($this->get_selected_choice(
116                         $group, $response[$this->field($place)])->text) . '}';
117                 $allblank = false;
118             } else {
119                 $choices[] = '{}';
120             }
121         }
122         if ($allblank) {
123             return null;
124         }
125         return implode(' ', $choices);
126     }
128     public function get_random_guess_score() {
129         $accum = 0;
131         foreach ($this->places as $placegroup) {
132             $accum += 1 / count($this->choices[$placegroup]);
133         }
135         return $accum / count($this->places);
136     }
138     public function clear_wrong_from_response(array $response) {
139         foreach ($this->places as $place => $notused) {
140             if (array_key_exists($this->field($place), $response) &&
141                     $response[$this->field($place)] != $this->get_right_choice_for($place)) {
142                 $response[$this->field($place)] = '0';
143             }
144         }
145         return $response;
146     }
148     public function get_num_parts_right(array $response) {
149         $numright = 0;
150         foreach ($this->places as $place => $notused) {
151             if (!array_key_exists($this->field($place), $response)) {
152                 continue;
153             }
154             if ($response[$this->field($place)] == $this->get_right_choice_for($place)) {
155                 $numright += 1;
156             }
157         }
158         return array($numright, count($this->places));
159     }
161     /**
162      * @param integer $key stem number
163      * @return string the question-type variable name.
164      */
165     public function field($place) {
166         return 'p' . $place;
167     }
169     public function get_expected_data() {
170         $vars = array();
171         foreach ($this->places as $place => $notused) {
172             $vars[$this->field($place)] = PARAM_INTEGER;
173         }
174         return $vars;
175     }
177     public function get_correct_response() {
178         $response = array();
179         foreach ($this->places as $place => $notused) {
180             $response[$this->field($place)] = $this->get_right_choice_for($place);
181         }
182         return $response;
183     }
185     public function get_right_choice_for($place) {
186         $group = $this->places[$place];
187         foreach ($this->choiceorder[$group] as $choicekey => $choiceid) {
188             if ($this->rightchoices[$place] == $choiceid) {
189                 return $choicekey;
190             }
191         }
192     }
194     public function get_ordered_choices($group) {
195         $choices = array();
196         foreach ($this->choiceorder[$group] as $choicekey => $choiceid) {
197             $choices[$choicekey] = $this->choices[$group][$choiceid];
198         }
199         return $choices;
200     }
202     public function is_complete_response(array $response) {
203         $complete = true;
204         foreach ($this->places as $place => $notused) {
205             $complete = $complete && !empty($response[$this->field($place)]);
206         }
207         return $complete;
208     }
210     public function is_gradable_response(array $response) {
211         foreach ($this->places as $place => $notused) {
212             if (!empty($response[$this->field($place)])) {
213                 return true;
214             }
215         }
216         return false;
217     }
219     public function is_same_response(array $prevresponse, array $newresponse) {
220         foreach ($this->places as $place => $notused) {
221             $fieldname = $this->field($place);
222             if (!question_utils::arrays_same_at_key_integer(
223                     $prevresponse, $newresponse, $fieldname)) {
224                 return false;
225             }
226         }
227         return true;
228     }
230     public function get_validation_error(array $response) {
231         if ($this->is_complete_response($response)) {
232             return '';
233         }
234         return get_string('pleaseputananswerineachbox', 'qtype_gapselect');
235     }
237     public function grade_response(array $response) {
238         list($right, $total) = $this->get_num_parts_right($response);
239         $fraction = $right / $total;
240         return array($fraction, question_state::graded_state_for_fraction($fraction));
241     }
243     public function compute_final_grade($responses, $totaltries) {
244         $totalscore = 0;
245         foreach ($this->places as $place => $notused) {
246             $fieldname = $this->field($place);
248             $lastwrongindex = -1;
249             $finallyright = false;
250             foreach ($responses as $i => $response) {
251                 if (!array_key_exists($fieldname, $response) ||
252                         $response[$fieldname] != $this->get_right_choice_for($place)) {
253                     $lastwrongindex = $i;
254                     $finallyright = false;
255                 } else {
256                     $finallyright = true;
257                 }
258             }
260             if ($finallyright) {
261                 $totalscore += max(0, 1 - ($lastwrongindex + 1) * $this->penalty);
262             }
263         }
265         return $totalscore / count($this->places);
266     }
268     public function classify_response(array $response) {
269         $parts = array();
270         foreach ($this->places as $place => $group) {
271             if (!array_key_exists($this->field($place), $response) ||
272                     !$response[$this->field($place)]) {
273                 $parts[$place] = question_classified_response::no_response();
274                 continue;
275             }
277             $fieldname = $this->field($place);
278             $choiceno = $this->choiceorder[$group][$response[$fieldname]];
279             $choice = $this->choices[$group][$choiceno];
280             $parts[$place] = new question_classified_response(
281                     $choiceno, $this->html_to_text($choice->text),
282                     $this->get_right_choice_for($place) == $response[$fieldname]);
283         }
284         return $parts;
285     }
287     public function check_file_access($qa, $options, $component, $filearea, $args, $forcedownload) {
288         if ($component == 'question' && in_array($filearea,
289                 array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback'))) {
290             return $this->check_combined_feedback_file_access($qa, $options, $filearea);
292         } else if ($component == 'question' && $filearea == 'hint') {
293             return $this->check_hint_file_access($qa, $options, $args);
295         } else {
296             return parent::check_file_access($qa, $options, $component, $filearea, $args, $forcedownload);
297         }
298     }