13c5d48ec77ac93d3aebb31fa9a969f4c9e00780
[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/>.
18 /**
19  * Definition class for embedded element in question text question. Parent of
20  * gap-select, drag and drop and possibly others.
21  *
22  * @package    qtype
23  * @subpackage gapselect
24  * @copyright  2011 The Open University
25  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
26  */
29 defined('MOODLE_INTERNAL') || die();
32 /**
33  * Represents embedded element in question text question. Parent of drag and drop and select from
34  * drop down list and ?others?
35  *
36  * @copyright  2011 The Open University
37  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
38  */
39 abstract class qtype_gapselect_question_base extends question_graded_automatically_with_countback {
40     /** @var boolean Whether the question stems should be shuffled. */
41     public $shufflechoices;
43     public $correctfeedback;
44     public $partiallycorrectfeedback;
45     public $incorrectfeedback;
47     /** @var array of arrays. The keys are the choice group numbers. The values
48      * are arrays of qtype_gapselect_choice objects. */
49     public $choices;
51     /**
52      * @var array place number => group number of the places in the question
53      * text where choices can be put. Places are numbered from 1.
54      */
55     public $places;
57     /**
58      * @var array of strings, one longer than $places, which is achieved by
59      * indexing from 0. The bits of question text that go between the placeholders.
60      */
61     public $textfragments;
63     /** @var array index of the right choice for each stem. */
64     public $rightchoices;
66     /** @var array shuffled choice indexes. */
67     protected $choiceorder;
69     public function start_attempt(question_attempt_step $step) {
70         foreach ($this->choices as $group => $choices) {
71             $choiceorder = array_keys($choices);
72             if ($this->shufflechoices) {
73                 shuffle($choiceorder);
74             }
75             $step->set_qt_var('_choiceorder' . $group, implode(',', $choiceorder));
76             $this->set_choiceorder($group, $choiceorder);
77         }
78     }
80     public function apply_attempt_state(question_attempt_step $step) {
81         foreach ($this->choices as $group => $choices) {
82             $this->set_choiceorder($group, explode(',',
83                     $step->get_qt_var('_choiceorder' . $group)));
84         }
85     }
87     /**
88      * Helper method used by both {@link start_attempt()} and
89      * {@link apply_attempt_state()}.
90      * @param int $group the group number.
91      * @param array $choiceorder the choices, in order.
92      */
93     protected function set_choiceorder($group, $choiceorder) {
94         foreach ($choiceorder as $key => $value) {
95             $this->choiceorder[$group][$key + 1] = $value;
96         }
97     }
99     public function get_question_summary() {
100         $question = $this->html_to_text($this->questiontext);
101         $groups = array();
102         foreach ($this->choices as $group => $choices) {
103             $cs = array();
104             foreach ($choices as $choice) {
105                 $cs[] = $this->html_to_text($choice->text);
106             }
107             $groups[] = '[[' . $group . ']] -> {' . implode(' / ', $cs) . '}';
108         }
109         return $question . '; ' . implode('; ', $groups);
110     }
112     protected function get_selected_choice($group, $shuffledchoicenumber) {
113         $choiceno = $this->choiceorder[$group][$shuffledchoicenumber];
114         return $this->choices[$group][$choiceno];
115     }
117     public function summarise_response(array $response) {
118         $matches = array();
119         $allblank = true;
120         foreach ($this->places as $place => $group) {
121             if (array_key_exists($this->field($place), $response) &&
122                     $response[$this->field($place)]) {
123                 $choices[] = '{' . $this->html_to_text($this->get_selected_choice(
124                         $group, $response[$this->field($place)])->text) . '}';
125                 $allblank = false;
126             } else {
127                 $choices[] = '{}';
128             }
129         }
130         if ($allblank) {
131             return null;
132         }
133         return implode(' ', $choices);
134     }
136     public function get_random_guess_score() {
137         $accum = 0;
139         foreach ($this->places as $placegroup) {
140             $accum += 1 / count($this->choices[$placegroup]);
141         }
143         return $accum / count($this->places);
144     }
146     public function clear_wrong_from_response(array $response) {
147         foreach ($this->places as $place => $notused) {
148             if (array_key_exists($this->field($place), $response) &&
149                     $response[$this->field($place)] != $this->get_right_choice_for($place)) {
150                 $response[$this->field($place)] = '0';
151             }
152         }
153         return $response;
154     }
156     public function get_num_parts_right(array $response) {
157         $numright = 0;
158         foreach ($this->places as $place => $notused) {
159             if (!array_key_exists($this->field($place), $response)) {
160                 continue;
161             }
162             if ($response[$this->field($place)] == $this->get_right_choice_for($place)) {
163                 $numright += 1;
164             }
165         }
166         return array($numright, count($this->places));
167     }
169     /**
170      * @param int $key stem number
171      * @return string the question-type variable name.
172      */
173     public function field($place) {
174         return 'p' . $place;
175     }
177     public function get_expected_data() {
178         $vars = array();
179         foreach ($this->places as $place => $notused) {
180             $vars[$this->field($place)] = PARAM_INTEGER;
181         }
182         return $vars;
183     }
185     public function get_correct_response() {
186         $response = array();
187         foreach ($this->places as $place => $notused) {
188             $response[$this->field($place)] = $this->get_right_choice_for($place);
189         }
190         return $response;
191     }
193     public function get_right_choice_for($place) {
194         $group = $this->places[$place];
195         foreach ($this->choiceorder[$group] as $choicekey => $choiceid) {
196             if ($this->rightchoices[$place] == $choiceid) {
197                 return $choicekey;
198             }
199         }
200     }
202     public function get_ordered_choices($group) {
203         $choices = array();
204         foreach ($this->choiceorder[$group] as $choicekey => $choiceid) {
205             $choices[$choicekey] = $this->choices[$group][$choiceid];
206         }
207         return $choices;
208     }
210     public function is_complete_response(array $response) {
211         $complete = true;
212         foreach ($this->places as $place => $notused) {
213             $complete = $complete && !empty($response[$this->field($place)]);
214         }
215         return $complete;
216     }
218     public function is_gradable_response(array $response) {
219         foreach ($this->places as $place => $notused) {
220             if (!empty($response[$this->field($place)])) {
221                 return true;
222             }
223         }
224         return false;
225     }
227     public function is_same_response(array $prevresponse, array $newresponse) {
228         foreach ($this->places as $place => $notused) {
229             $fieldname = $this->field($place);
230             if (!question_utils::arrays_same_at_key_integer(
231                     $prevresponse, $newresponse, $fieldname)) {
232                 return false;
233             }
234         }
235         return true;
236     }
238     public function get_validation_error(array $response) {
239         if ($this->is_complete_response($response)) {
240             return '';
241         }
242         return get_string('pleaseputananswerineachbox', 'qtype_gapselect');
243     }
245     public function grade_response(array $response) {
246         list($right, $total) = $this->get_num_parts_right($response);
247         $fraction = $right / $total;
248         return array($fraction, question_state::graded_state_for_fraction($fraction));
249     }
251     public function compute_final_grade($responses, $totaltries) {
252         $totalscore = 0;
253         foreach ($this->places as $place => $notused) {
254             $fieldname = $this->field($place);
256             $lastwrongindex = -1;
257             $finallyright = false;
258             foreach ($responses as $i => $response) {
259                 if (!array_key_exists($fieldname, $response) ||
260                         $response[$fieldname] != $this->get_right_choice_for($place)) {
261                     $lastwrongindex = $i;
262                     $finallyright = false;
263                 } else {
264                     $finallyright = true;
265                 }
266             }
268             if ($finallyright) {
269                 $totalscore += max(0, 1 - ($lastwrongindex + 1) * $this->penalty);
270             }
271         }
273         return $totalscore / count($this->places);
274     }
276     public function classify_response(array $response) {
277         $parts = array();
278         foreach ($this->places as $place => $group) {
279             if (!array_key_exists($this->field($place), $response) ||
280                     !$response[$this->field($place)]) {
281                 $parts[$place] = question_classified_response::no_response();
282                 continue;
283             }
285             $fieldname = $this->field($place);
286             $choiceno = $this->choiceorder[$group][$response[$fieldname]];
287             $choice = $this->choices[$group][$choiceno];
288             $parts[$place] = new question_classified_response(
289                     $choiceno, $this->html_to_text($choice->text),
290                     $this->get_right_choice_for($place) == $response[$fieldname]);
291         }
292         return $parts;
293     }
295     public function check_file_access($qa, $options, $component, $filearea, $args, $forcedownload) {
296         if ($component == 'question' && in_array($filearea,
297                 array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback'))) {
298             return $this->check_combined_feedback_file_access($qa, $options, $filearea);
300         } else if ($component == 'question' && $filearea == 'hint') {
301             return $this->check_hint_file_access($qa, $options, $args);
303         } else {
304             return parent::check_file_access($qa, $options, $component, $filearea, $args, $forcedownload);
305         }
306     }