Merge branch 'MDL-70094-310' of https://github.com/SangNguyen2601/moodle into MOODLE_...
[moodle.git] / question / type / gapselect / questionbase.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/>.
17 /**
18  * Definition class for embedded element in question text question.
19  *
20  * Used by gap-select, drag and drop and possibly others.
21  *
22  * @package    qtype_gapselect
23  * @copyright  2011 The Open University
24  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25  */
28 defined('MOODLE_INTERNAL') || die();
30 require_once($CFG->dirroot . '/question/type/questionbase.php');
32 /**
33  * Represents embedded element in question text question.
34  *
35  * Parent of drag and drop and select from drop down list and others.
36  *
37  * @copyright  2011 The Open University
38  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
39  */
40 abstract class qtype_gapselect_question_base extends question_graded_automatically_with_countback {
41     /** @var boolean Whether the question stems should be shuffled. */
42     public $shufflechoices;
44     /** @var string Feedback for any correct response. */
45     public $correctfeedback;
46     /** @var int format of $correctfeedback. */
47     public $correctfeedbackformat;
48     /** @var string Feedback for any partially correct response. */
49     public $partiallycorrectfeedback;
50     /** @var int format of $partiallycorrectfeedback. */
51     public $partiallycorrectfeedbackformat;
52     /** @var string Feedback for any incorrect response. */
53     public $incorrectfeedback;
54     /** @var int format of $incorrectfeedback. */
55     public $incorrectfeedbackformat;
57     /**
58      * @var array of arrays. The outer keys are the choice group numbers.
59      * The inner keys for most question types number sequentialy from 1. However
60      * for ddimageortext questions it is strange (and difficult to change now).
61      * the first item in each group gets numbered 1, and the other items get numbered
62      * $choice->no. Be careful!
63      * The values are arrays of qtype_gapselect_choice objects (or a subclass).
64      */
65     public $choices;
67     /**
68      * @var array place number => group number of the places in the question
69      * text where choices can be put. Places are numbered from 1.
70      */
71     public $places;
73     /**
74      * @var array of strings, one longer than $places, which is achieved by
75      * indexing from 0. The bits of question text that go between the placeholders.
76      */
77     public $textfragments;
79     /** @var array index of the right choice for each stem. */
80     public $rightchoices;
82     /** @var array shuffled choice indexes. */
83     protected $choiceorder;
85     public function start_attempt(question_attempt_step $step, $variant) {
86         foreach ($this->choices as $group => $choices) {
87             $choiceorder = array_keys($choices);
88             if ($this->shufflechoices) {
89                 shuffle($choiceorder);
90             }
91             $step->set_qt_var('_choiceorder' . $group, implode(',', $choiceorder));
92             $this->set_choiceorder($group, $choiceorder);
93         }
94     }
96     public function apply_attempt_state(question_attempt_step $step) {
97         foreach ($this->choices as $group => $choices) {
98             $this->set_choiceorder($group, explode(',',
99                     $step->get_qt_var('_choiceorder' . $group)));
100         }
101     }
103     /**
104      * Helper method used by both {@link start_attempt()} and
105      * {@link apply_attempt_state()}.
106      * @param int $group the group number.
107      * @param array $choiceorder the choices, in order.
108      */
109     protected function set_choiceorder($group, $choiceorder) {
110         foreach ($choiceorder as $key => $value) {
111             $this->choiceorder[$group][$key + 1] = $value;
112         }
113     }
115     public function get_question_summary() {
116         $question = $this->html_to_text($this->questiontext, $this->questiontextformat);
117         $groups = array();
118         foreach ($this->choices as $group => $choices) {
119             $cs = array();
120             foreach ($choices as $choice) {
121                 $cs[] = html_to_text($choice->text, 0, false);
122             }
123             $groups[] = '[[' . $group . ']] -> {' . implode(' / ', $cs) . '}';
124         }
125         return $question . '; ' . implode('; ', $groups);
126     }
128     protected function get_selected_choice($group, $shuffledchoicenumber) {
129         $choiceno = $this->choiceorder[$group][$shuffledchoicenumber];
130         return isset($this->choices[$group][$choiceno]) ? $this->choices[$group][$choiceno] : null;
131     }
133     public function summarise_response(array $response) {
134         $matches = array();
135         $allblank = true;
136         foreach ($this->places as $place => $group) {
137             if (array_key_exists($this->field($place), $response) &&
138                     $response[$this->field($place)]) {
139                 $choices[] = '{' . $this->summarise_choice(
140                         $this->get_selected_choice($group, $response[$this->field($place)])) . '}';
141                 $allblank = false;
142             } else {
143                 $choices[] = '{}';
144             }
145         }
146         if ($allblank) {
147             return null;
148         }
149         return implode(' ', $choices);
150     }
152     /**
153      * Convert a choice to plain text.
154      * @param qtype_gapselect_choice $choice one of the choices for a place.
155      * @return a plain text summary of the choice.
156      */
157     public function summarise_choice($choice) {
158         return $this->html_to_text($choice->text, FORMAT_PLAIN);
159     }
161     public function get_random_guess_score() {
162         $accum = 0;
164         foreach ($this->places as $placegroup) {
165             $accum += 1 / count($this->choices[$placegroup]);
166         }
168         return $accum / count($this->places);
169     }
171     public function clear_wrong_from_response(array $response) {
172         foreach ($this->places as $place => $notused) {
173             if (array_key_exists($this->field($place), $response) &&
174                     $response[$this->field($place)] != $this->get_right_choice_for($place)) {
175                 $response[$this->field($place)] = '0';
176             }
177         }
178         return $response;
179     }
181     public function get_num_parts_right(array $response) {
182         $numright = 0;
183         foreach ($this->places as $place => $notused) {
184             if (!array_key_exists($this->field($place), $response)) {
185                 continue;
186             }
187             if ($response[$this->field($place)] == $this->get_right_choice_for($place)) {
188                 $numright += 1;
189             }
190         }
191         return array($numright, count($this->places));
192     }
194     /**
195      * Get the field name corresponding to a given place.
196      * @param int $place stem number
197      * @return string the question-type variable name.
198      */
199     public function field($place) {
200         return 'p' . $place;
201     }
203     public function get_expected_data() {
204         $vars = array();
205         foreach ($this->places as $place => $notused) {
206             $vars[$this->field($place)] = PARAM_INTEGER;
207         }
208         return $vars;
209     }
211     public function get_correct_response() {
212         $response = array();
213         foreach ($this->places as $place => $notused) {
214             $response[$this->field($place)] = $this->get_right_choice_for($place);
215         }
216         return $response;
217     }
219     public function get_right_choice_for($place) {
220         $group = $this->places[$place];
221         foreach ($this->choiceorder[$group] as $choicekey => $choiceid) {
222             if ($this->rightchoices[$place] == $choiceid) {
223                 return $choicekey;
224             }
225         }
226     }
228     public function get_ordered_choices($group) {
229         $choices = array();
230         foreach ($this->choiceorder[$group] as $choicekey => $choiceid) {
231             $choices[$choicekey] = $this->choices[$group][$choiceid];
232         }
233         return $choices;
234     }
236     public function is_complete_response(array $response) {
237         $complete = true;
238         foreach ($this->places as $place => $notused) {
239             $complete = $complete && !empty($response[$this->field($place)]);
240         }
241         return $complete;
242     }
244     public function is_gradable_response(array $response) {
245         foreach ($this->places as $place => $notused) {
246             if (!empty($response[$this->field($place)])) {
247                 return true;
248             }
249         }
250         return false;
251     }
253     public function is_same_response(array $prevresponse, array $newresponse) {
254         foreach ($this->places as $place => $notused) {
255             $fieldname = $this->field($place);
256             if (!question_utils::arrays_same_at_key_integer(
257                     $prevresponse, $newresponse, $fieldname)) {
258                 return false;
259             }
260         }
261         return true;
262     }
264     public function get_validation_error(array $response) {
265         if ($this->is_complete_response($response)) {
266             return '';
267         }
268         return get_string('pleaseputananswerineachbox', 'qtype_gapselect');
269     }
271     public function grade_response(array $response) {
272         list($right, $total) = $this->get_num_parts_right($response);
273         $fraction = $right / $total;
274         return array($fraction, question_state::graded_state_for_fraction($fraction));
275     }
277     public function compute_final_grade($responses, $totaltries) {
278         $totalscore = 0;
279         foreach ($this->places as $place => $notused) {
280             $fieldname = $this->field($place);
282             $lastwrongindex = -1;
283             $finallyright = false;
284             foreach ($responses as $i => $response) {
285                 if (!array_key_exists($fieldname, $response) ||
286                         $response[$fieldname] != $this->get_right_choice_for($place)) {
287                     $lastwrongindex = $i;
288                     $finallyright = false;
289                 } else {
290                     $finallyright = true;
291                 }
292             }
294             if ($finallyright) {
295                 $totalscore += max(0, 1 - ($lastwrongindex + 1) * $this->penalty);
296             }
297         }
299         return $totalscore / count($this->places);
300     }
302     public function classify_response(array $response) {
303         $parts = array();
304         foreach ($this->places as $place => $group) {
305             if (!array_key_exists($this->field($place), $response) ||
306                     !$response[$this->field($place)]) {
307                 $parts[$place] = question_classified_response::no_response();
308                 continue;
309             }
311             $fieldname = $this->field($place);
312             $choiceno = $this->choiceorder[$group][$response[$fieldname]];
313             $choice = $this->choices[$group][$choiceno];
314             $parts[$place] = new question_classified_response(
315                     $choiceno, html_to_text($choice->text, 0, false),
316                     ($this->get_right_choice_for($place) == $response[$fieldname]) / count($this->places));
317         }
318         return $parts;
319     }
321     public function check_file_access($qa, $options, $component, $filearea, $args, $forcedownload) {
322         if ($component == 'question' && in_array($filearea,
323                 array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback'))) {
324             return $this->check_combined_feedback_file_access($qa, $options, $filearea, $args);
326         } else if ($component == 'question' && $filearea == 'hint') {
327             return $this->check_hint_file_access($qa, $options, $args);
329         } else {
330             return parent::check_file_access($qa, $options, $component, $filearea,
331                     $args, $forcedownload);
332         }
333     }
335     /**
336      * Return the question settings that define this question as structured data.
337      *
338      * @param question_attempt $qa the current attempt for which we are exporting the settings.
339      * @param question_display_options $options the question display options which say which aspects of the question
340      * should be visible.
341      * @return mixed structure representing the question settings. In web services, this will be JSON-encoded.
342      */
343     public function get_question_definition_for_external_rendering(question_attempt $qa, question_display_options $options) {
344         // This is a partial implementation, returning only the most relevant question settings for now,
345         // ideally, we should return as much as settings as possible (depending on the state and display options).
347         return [
348             'shufflechoices' => $this->shufflechoices,
349         ];
350     }