MDL-47494 gapselect: work-in-progress converting the ddwtos and gapselect qtypes.
[moodle.git] / question / type / gapselect / questiontypebase.php
CommitLineData
0d24b17a
TH
1<?php
2
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/>.
17
18
19/**
20 * Question type class for the embedded element in question text question types.
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 */
27
28
29require_once($CFG->libdir . '/questionlib.php');
30require_once($CFG->dirroot . '/question/engine/lib.php');
31require_once($CFG->dirroot . '/question/format/xml/format.php');
32
33
34/**
35 * The embedded element in question text question type class.
36 *
37 * @copyright 2009 The Open University
38 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
39 */
40class qtype_gapselect_base extends question_type {
41 /**
42 * Choices are stored in the question_answers table, and any options need to
43 * be put into the feedback field somehow. This method is responsible for
44 * converting all the options to a single string for this purpose. It is used
45 * by {@link save_question_options()}.
46 * @param array $choice the form data relating to this choice.
47 * @return string ready to store in the database.
48 */
49 protected abstract function choice_options_to_feedback($choice);
50
51 public function save_question_options($question) {
52 $result = new stdClass();
53
54 if (!$oldanswers = get_records('question_answers', 'question', $question->id, 'id ASC')) {
55 $oldanswers = array();
56 }
57
58 // Insert all the new answers
59 foreach ($question->choices as $key => $choice) {
60
61 if (trim($choice['answer']) == '') {
62 continue;
63 }
64
65 $feedback = $this->choice_options_to_feedback($choice);
66
67 if ($answer = array_shift($oldanswers)) { // Existing answer, so reuse it
68 $answer->answer = $choice['answer'];
69 $answer->fraction = 0;
70 $answer->feedback = $feedback;
71 if (!update_record('question_answers', $answer)) {
72 $result->error = "Could not update question type '".$this->name()."' question answer! (id=$answer->id)";
73 return $result;
74 }
75 } else {
76 $answer = new stdClass;
77 $answer->answer = $choice['answer'];
78 $answer->question = $question->id;
79 $answer->fraction = 0;
80 $answer->feedback = $feedback;
81 if (!$answer->id = insert_record('question_answers', $answer)) {
82 $result->error = 'Could not insert question type \''.$this->name().'\' question answer!';
83 return $result;
84 }
85 }
86 }
87
88 // Delete old answer records
89 if (!empty($oldanswers)) {
90 foreach($oldanswers as $oa) {
91 delete_records('question_answers', 'id', $oa->id);
92 }
93 }
94
95 $update = true;
96 $options = get_record('question_'.$this->name(), 'questionid', $question->id);
97 if (!$options) {
98 $update = false;
99 $options = new stdClass;
100 $options->questionid = $question->id;
101 }
102
103 $options->shuffleanswers = !empty($question->shuffleanswers);
104 $options->correctfeedback = trim($question->correctfeedback);
105 $options->partiallycorrectfeedback = trim($question->partiallycorrectfeedback);
106 $options->shownumcorrect = !empty($question->shownumcorrect);
107 $options->incorrectfeedback = trim($question->incorrectfeedback);
108
109 if ($update) {
110 if (!update_record('question_'.$this->name(), $options)) {
111 $result->error = "Could not update question type '".$this->name()."' options! (id=$options->id)";
112 return $result;
113 }
114
115 } else {
116 if (!insert_record('question_gapselect', $options)) {
117 $result->error = 'Could not insert question type \''.$this->name().'\' options!';
118 return $result;
119 }
120 }
121
122 $this->save_hints($question, true);
123
124 return true;
125 }
126
127 public function get_question_options($question) {
128 // Get additional information from database and attach it to the question object
129 if (!$question->options = get_record('question_'.$this->name(), 'questionid', $question->id)) {
130 notify('Error: Missing question options for question type \''.$this->name().'\' question '.$question->id.'!');
131 return false;
132 }
133
134 parent::get_question_options($question);
135 return true;
136 }
137
138 public function delete_question($questionid) {
139 delete_records('question_'.$this->name(), 'questionid', $questionid);
140 return parent::delete_question($questionid);
141 }
142
143 /**
144 * Used by {@link initialise_question_instance()} to set up the choice-specific data.
145 * @param object $choicedata as loaded from the question_answers table.
146 * @return object an appropriate object for representing the choice.
147 */
148 protected abstract function make_choice($choicedata);
149
150 protected function initialise_question_instance(question_definition $question, $questiondata) {
151 parent::initialise_question_instance($question, $questiondata);
152
153 $question->shufflechoices = $questiondata->options->shuffleanswers;
154
155 $question->correctfeedback = $questiondata->options->correctfeedback;
156 $question->partiallycorrectfeedback = $questiondata->options->partiallycorrectfeedback;
157 $question->incorrectfeedback = $questiondata->options->incorrectfeedback;
158 $question->shownumcorrect = $questiondata->options->shownumcorrect;
159
160 $question->choices = array();
161 $choiceindexmap= array();
162
163 // Store the choices in arrays by group.
164 $i = 1;
165 foreach ($questiondata->options->answers as $choicedata) {
166 $choice = $this->make_choice($choicedata);
167
168 if (array_key_exists($choice->choice_group(), $question->choices)) {
169 $question->choices[$choice->choice_group()][] = $choice;
170 } else {
171 $question->choices[$choice->choice_group()][1] = $choice;
172 }
173
174 end($question->choices[$choice->choice_group()]);
175 $choiceindexmap[$i] = array($choice->choice_group(),
176 key($question->choices[$choice->choice_group()]));
177 $i += 1;
178 }
179
180 $question->places = array();
181 $question->textfragments = array();
182 $question->rightchoices = array();
183 // Break up the question text, and store the fragments, places and right answers.
184
185 $bits = preg_split('/\[\[(\d+)]]/', $question->questiontext, null, PREG_SPLIT_DELIM_CAPTURE);
186 $question->textfragments[0] = array_shift($bits);
187 $i = 1;
188
189 while (!empty($bits)) {
190 $choice = array_shift($bits);
191
192 list($group, $choiceindex) = $choiceindexmap[$choice];
193 $question->places[$i] = $group;
194 $question->rightchoices[$i] = $choiceindex;
195
196 $question->textfragments[$i] = array_shift($bits);
197 $i += 1;
198 }
199 }
200
201 protected function make_hint($hint) {
202 return question_hint_with_parts::load_from_record($hint);
203 }
204
205 public function get_random_guess_score($questiondata) {
206 $question = $this->make_question($questiondata);
207 return $question->get_random_guess_score();
208 }
209
210 /**
211 * This function should reverse {@link choice_options_to_feedback()}.
212 * @param string $feedback the data loaded from the database.
213 * @return array the choice options.
214 */
215 protected abstract function feedback_to_choice_options($feedback);
216
217 /**
218 * This method gets the choices (answers)
219 * in a 2 dimentional array.
220 *
221 * @param object $question
222 * @return array of groups
223 */
224 protected function get_array_of_choices($question) {
225 $subquestions = $question->options->answers;
226 $count = 0;
227 foreach ($subquestions as $key=>$subquestion) {
228 $answers[$count]['id'] = $subquestion->id;
229 $answers[$count]['answer'] = $subquestion->answer;
230 $answers[$count]['fraction'] = $subquestion->fraction;
231 $answers[$count] += $this->feedback_to_choice_options($subquestion->feedback);
232 $answers[$count]['choice'] = $count+1;
233 ++$count;
234 }
235 return $answers;
236 }
237
238 /* This method gets the choices (answers) and sort them by groups
239 * in a 2 dimentional array.
240 *
241 * @param object $question
242 * @return array of groups
243 */
244 protected function get_array_of_groups($question, $state) {
245 $answers = $this->get_array_of_choices($question);
246 $arr = array();
247 for($group=1;$group<count($answers);$group++) {
248 $players = $this->get_group_of_players ($question, $state, $answers, $group);
249 if($players) {
250 $arr [$group]= $players;
251 }
252 }
253 return $arr;
254 }
255
256 /**
257 * This method gets the correct answers in a 2 dimentional array.
258 *
259 * @param object $question
260 * @return array of groups
261 */
262 protected function get_correct_answers($question) {
263 $arrayofchoices = $this->get_array_of_choices($question);
264 $arrayofplaceholdeers = $this->get_array_of_placeholders($question);
265
266 $correctplayeers = array();
267 foreach($arrayofplaceholdeers as $ph) {
268 foreach($arrayofchoices as $key=>$choice) {
269 if(($key+1) == $ph) {
270 $correctplayeers[]= $choice;
271 }
272 }
273 }
274 return $correctplayeers;
275 }
276
277 protected function get_array_of_placeholders($question) {
278 $qtext = $question->questiontext;
279 $error = '<b> ERROR</b>: Please check the form for this question. ';
280 if(!$qtext) {
281 echo $error . 'The question text is empty!';
282 return false;
283 }
284
285 //get the slots
286 $slots = $this->getEmbeddedTextArray($question);
287
288 if(!$slots) {
289 echo $error . 'The question text is not in the correct format!';
290 return false;
291 }
292
293 $output = array();
294 foreach ($slots as $slot) {
295 $output[]=substr($slot, 2, (strlen($slot)-4));//2 is for'[[' and 4 is for '[[]]'
296 }
297 return $output;
298 }
299
300 protected function get_group_of_players ($question, $state, $subquestions, $group) {
301 $goupofanswers=array();
302 foreach($subquestions as $key=>$subquestion) {
303 if($subquestion[$this->choice_group_key()] == $group) {
304 $goupofanswers[] = $subquestion;
305 }
306 }
307
308 //shuffle answers within this group
309 if ($question->options->shuffleanswers == 1) {
310 srand($state->attempt);
311 shuffle($goupofanswers);
312 }
313 return $goupofanswers;
314 }
315
316 public function get_possible_responses($questiondata) {
317 $question = $this->make_question($questiondata);
318
319 $parts = array();
320 foreach ($question->places as $place => $group) {
321 $choices = array();
322
323 foreach ($question->choices[$group] as $i => $choice) {
324 $choices[$i] = new question_possible_response(
325 html_to_text($question->format_text($choice->text), 0, false),
326 $question->rightchoices[$place] == $i);
327 }
328 $choices[null] = question_possible_response::no_response();
329
330 $parts[$place] = $choices;
331 }
332
333 return $parts;
334 }
335
336
337}