MDL-27413 qtype_multianswer add missing module.js
[moodle.git] / question / type / multianswer / questiontype.php
CommitLineData
aeb15530 1<?php
d3603157
TH
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/>.
16
17/**
18 * Question type class for the multi-answer question type.
19 *
20 * @package qtype
21 * @subpackage multianswer
22 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24 */
25
26
a17b297d
TH
27defined('MOODLE_INTERNAL') || die();
28
29
1976496e 30/**
d3603157
TH
31 * The multi-answer question type class.
32 *
33 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
34 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
7375c542 35 */
ab50232b 36class qtype_multianswer extends question_type {
59a3fcd3 37 public function requires_qtypes() {
869309b8 38 return array('shortanswer', 'numerical', 'multichoice');
39 }
516cf3eb 40
ab50232b
TH
41 public function can_analyse_responses() {
42 return false;
43 }
44
59a3fcd3
TH
45 public function get_question_options($question) {
46 global $DB, $OUTPUT;
516cf3eb 47
48 // Get relevant data indexed by positionkey from the multianswers table
ab50232b
TH
49 $sequence = $DB->get_field('question_multianswer', 'sequence',
50 array('question' => $question->id), '*', MUST_EXIST);
516cf3eb 51
ab50232b
TH
52 $wrappedquestions = $DB->get_records_list('question', 'id',
53 explode(',', $sequence), 'id ASC');
516cf3eb 54
55 // We want an array with question ids as index and the positions as values
56 $sequence = array_flip(explode(',', $sequence));
57 array_walk($sequence, create_function('&$val', '$val++;'));
ab50232b
TH
58
59 // If a question is lost, the corresponding index is null
aeb15530 60 // so this null convention is used to test $question->options->questions
857caf3b 61 // before using the values.
aeb15530 62 // first all possible questions from sequence are nulled
857caf3b 63 // then filled with the data if available in $wrappedquestions
59a3fcd3
TH
64 foreach ($sequence as $seq) {
65 $question->options->questions[$seq] = '';
df79079f 66 }
ab50232b
TH
67
68 foreach ($wrappedquestions as $wrapped) {
69 question_bank::get_qtype($wrapped->qtype)->get_question_options($wrapped);
70 // for wrapped questions the maxgrade is always equal to the defaultmark,
71 // there is no entry in the question_instances table for them
72 $wrapped->maxmark = $wrapped->defaultmark;
73 $question->options->questions[$sequence[$wrapped->id]] = $wrapped;
857caf3b 74 }
516cf3eb 75
76 return true;
77 }
78
59a3fcd3
TH
79 public function save_question_options($question) {
80 global $DB;
0ff4bd08 81 $result = new stdClass();
9fc3100f 82
516cf3eb 83 // This function needs to be able to handle the case where the existing set of wrapped
84 // questions does not match the new set of wrapped questions so that some need to be
85 // created, some modified and some deleted
86 // Unfortunately the code currently simply overwrites existing ones in sequence. This
9fc3100f 87 // will make re-marking after a re-ordering of wrapped questions impossible and
516cf3eb 88 // will also create difficulties if questiontype specific tables reference the id.
9fc3100f 89
516cf3eb 90 // First we get all the existing wrapped questions
ab50232b
TH
91 if (!$oldwrappedids = $DB->get_field('question_multianswer', 'sequence',
92 array('question' => $question->id))) {
857caf3b 93 $oldwrappedquestions = array();
0a5b58af 94 } else {
12c6e008
TH
95 $oldwrappedquestions = $DB->get_records_list('question', 'id',
96 explode(',', $oldwrappedids), 'id ASC');
516cf3eb 97 }
ab50232b 98
516cf3eb 99 $sequence = array();
59a3fcd3
TH
100 foreach ($question->options->questions as $wrapped) {
101 if (!empty($wrapped)) {
df79079f 102 // if we still have some old wrapped question ids, reuse the next of them
f34488b2 103
12c6e008
TH
104 if (is_array($oldwrappedquestions) &&
105 $oldwrappedquestion = array_shift($oldwrappedquestions)) {
857caf3b 106 $wrapped->id = $oldwrappedquestion->id;
59a3fcd3 107 if ($oldwrappedquestion->qtype != $wrapped->qtype) {
857caf3b 108 switch ($oldwrappedquestion->qtype) {
59a3fcd3 109 case 'multichoice':
ab50232b
TH
110 $DB->delete_records('question_multichoice',
111 array('question' => $oldwrappedquestion->id));
59a3fcd3
TH
112 break;
113 case 'shortanswer':
ab50232b
TH
114 $DB->delete_records('question_shortanswer',
115 array('question' => $oldwrappedquestion->id));
59a3fcd3
TH
116 break;
117 case 'numerical':
ab50232b
TH
118 $DB->delete_records('question_numerical',
119 array('question' => $oldwrappedquestion->id));
59a3fcd3
TH
120 break;
121 default:
ab50232b
TH
122 throw new moodle_exception('qtypenotrecognized',
123 'qtype_multianswer', '', $oldwrappedquestion->qtype);
59a3fcd3 124 $wrapped->id = 0;
df79079f 125 }
e9028ffc 126 }
59a3fcd3
TH
127 } else {
128 $wrapped->id = 0;
e9028ffc 129 }
516cf3eb 130 }
77fa3a0d 131 $wrapped->name = $question->name;
132 $wrapped->parent = $question->id;
59a3fcd3 133 $previousid = $wrapped->id;
ab50232b
TH
134 // save_question strips this extra bit off the category again.
135 $wrapped->category = $question->category . ',1';
136 $wrapped = question_bank::get_qtype($wrapped->qtype)->save_question(
137 $wrapped, clone($wrapped));
516cf3eb 138 $sequence[] = $wrapped->id;
59a3fcd3 139 if ($previousid != 0 && $previousid != $wrapped->id) {
26053641 140 // for some reasons a new question has been created
141 // so delete the old one
59a3fcd3 142 delete_question($previousid);
26053641 143 }
516cf3eb 144 }
145
146 // Delete redundant wrapped questions
59a3fcd3 147 if (is_array($oldwrappedquestions) && count($oldwrappedquestions)) {
26053641 148 foreach ($oldwrappedquestions as $oldwrappedquestion) {
59a3fcd3 149 delete_question($oldwrappedquestion->id);
e9028ffc 150 }
4bc4ca50 151 }
516cf3eb 152
153 if (!empty($sequence)) {
0ff4bd08 154 $multianswer = new stdClass();
516cf3eb 155 $multianswer->question = $question->id;
156 $multianswer->sequence = implode(',', $sequence);
12c6e008
TH
157 if ($oldid = $DB->get_field('question_multianswer', 'id',
158 array('question' => $question->id))) {
516cf3eb 159 $multianswer->id = $oldid;
ab50232b 160 $DB->update_record('question_multianswer', $multianswer);
516cf3eb 161 } else {
ab50232b 162 $DB->insert_record('question_multianswer', $multianswer);
516cf3eb 163 }
164 }
165 }
166
59a3fcd3 167 public function save_question($authorizedquestion, $form) {
e51efd7e 168 $question = qtype_multianswer_extract_question($form->questiontext);
516cf3eb 169 if (isset($authorizedquestion->id)) {
170 $question->id = $authorizedquestion->id;
516cf3eb 171 }
172
516cf3eb 173 $question->category = $authorizedquestion->category;
ab50232b 174 $form->defaultmark = $question->defaultmark;
516cf3eb 175 $form->questiontext = $question->questiontext;
176 $form->questiontextformat = 0;
77fa3a0d 177 $form->options = clone($question->options);
516cf3eb 178 unset($question->options);
94dbfb3a 179 return parent::save_question($question, $form);
516cf3eb 180 }
181
59a3fcd3 182 public function delete_question($questionid, $contextid) {
f34488b2 183 global $DB;
ab50232b 184 $DB->delete_records('question_multianswer', array('question' => $questionid));
9203b705
TH
185
186 parent::delete_question($questionid, $contextid);
516cf3eb 187 }
188
ab50232b
TH
189 protected function initialise_question_instance($question, $questiondata) {
190 parent::initialise_question_instance($question, $questiondata);
42a5b055
TH
191
192 $bits = preg_split('/\{#(\d+)\}/', $question->questiontext,
193 null, PREG_SPLIT_DELIM_CAPTURE);
194 $question->textfragments[0] = array_shift($bits);
195 $i = 1;
196 while (!empty($bits)) {
197 $question->places[$i] = array_shift($bits);
198 $question->textfragments[$i] = array_shift($bits);
199 $i += 1;
200 }
201
ab50232b
TH
202 foreach ($questiondata->options->questions as $key => $subqdata) {
203 $subqdata->contextid = $questiondata->contextid;
204 $question->subquestions[$key] = question_bank::make_question($subqdata);
fa6c8620 205 $question->subquestions[$key]->maxmark = $subqdata->defaultmark;
869309b8 206 }
869309b8 207 }
869309b8 208
ab50232b
TH
209 public function get_random_guess_score($questiondata) {
210 $fractionsum = 0;
211 $fractionmax = 0;
212 foreach ($questiondata->options->questions as $key => $subqdata) {
fa6c8620 213 $fractionmax += $subqdata->defaultmark;
ab50232b
TH
214 $fractionsum += question_bank::get_qtype(
215 $subqdata->qtype)->get_random_guess_score($subqdata);
516cf3eb 216 }
ab50232b 217 return $fractionsum / $fractionmax;
516cf3eb 218 }
516cf3eb 219}
516cf3eb 220
ab50232b 221
0b346164 222// ANSWER_ALTERNATIVE regexes
ab50232b 223define('ANSWER_ALTERNATIVE_FRACTION_REGEX',
0b346164 224 '=|%(-?[0-9]+)%');
225// for the syntax '(?<!' see http://www.perl.com/doc/manual/html/pod/perlre.html#item_C
ab50232b 226define('ANSWER_ALTERNATIVE_ANSWER_REGEX',
e51efd7e 227 '.+?(?<!\\\\|&|&amp;)(?=[~#}]|$)');
ab50232b 228define('ANSWER_ALTERNATIVE_FEEDBACK_REGEX',
0b346164 229 '.*?(?<!\\\\)(?=[~}]|$)');
ab50232b 230define('ANSWER_ALTERNATIVE_REGEX',
e51efd7e 231 '(' . ANSWER_ALTERNATIVE_FRACTION_REGEX .')?' .
232 '(' . ANSWER_ALTERNATIVE_ANSWER_REGEX . ')' .
233 '(#(' . ANSWER_ALTERNATIVE_FEEDBACK_REGEX .'))?');
0b346164 234
235// Parenthesis positions for ANSWER_ALTERNATIVE_REGEX
ab50232b
TH
236define('ANSWER_ALTERNATIVE_REGEX_PERCENTILE_FRACTION', 2);
237define('ANSWER_ALTERNATIVE_REGEX_FRACTION', 1);
238define('ANSWER_ALTERNATIVE_REGEX_ANSWER', 3);
239define('ANSWER_ALTERNATIVE_REGEX_FEEDBACK', 5);
0b346164 240
241// NUMBER_FORMATED_ALTERNATIVE_ANSWER_REGEX is used
242// for identifying numerical answers in ANSWER_ALTERNATIVE_REGEX_ANSWER
ab50232b 243define('NUMBER_REGEX',
0b346164 244 '-?(([0-9]+[.,]?[0-9]*|[.,][0-9]+)([eE][-+]?[0-9]+)?)');
ab50232b 245define('NUMERICAL_ALTERNATIVE_REGEX',
0b346164 246 '^(' . NUMBER_REGEX . ')(:' . NUMBER_REGEX . ')?$');
247
248// Parenthesis positions for NUMERICAL_FORMATED_ALTERNATIVE_ANSWER_REGEX
ab50232b
TH
249define('NUMERICAL_CORRECT_ANSWER', 1);
250define('NUMERICAL_ABS_ERROR_MARGIN', 6);
0b346164 251
252// Remaining ANSWER regexes
ab50232b 253define('ANSWER_TYPE_DEF_REGEX',
12c6e008
TH
254 '(NUMERICAL|NM)|(MULTICHOICE|MC)|(MULTICHOICE_V|MCV)|(MULTICHOICE_H|MCH)|' .
255 '(SHORTANSWER|SA|MW)|(SHORTANSWER_C|SAC|MWC)');
ab50232b 256define('ANSWER_START_REGEX',
0b346164 257 '\{([0-9]*):(' . ANSWER_TYPE_DEF_REGEX . '):');
258
ab50232b 259define('ANSWER_REGEX',
0b346164 260 ANSWER_START_REGEX
261 . '(' . ANSWER_ALTERNATIVE_REGEX
262 . '(~'
263 . ANSWER_ALTERNATIVE_REGEX
59a3fcd3 264 . ')*)\}');
0b346164 265
266// Parenthesis positions for singulars in ANSWER_REGEX
ab50232b
TH
267define('ANSWER_REGEX_NORM', 1);
268define('ANSWER_REGEX_ANSWER_TYPE_NUMERICAL', 3);
269define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE', 4);
270define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_REGULAR', 5);
271define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_HORIZONTAL', 6);
272define('ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER', 7);
273define('ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER_C', 8);
274define('ANSWER_REGEX_ALTERNATIVES', 9);
516cf3eb 275
7518b645 276function qtype_multianswer_extract_question($text) {
61dfe97e 277 // $text is an array [text][format][itemid]
0ff4bd08 278 $question = new stdClass();
dfa47f96 279 $question->qtype = 'multianswer';
516cf3eb 280 $question->questiontext = $text;
61dfe97e
PP
281 $question->generalfeedback['text'] = '';
282 $question->generalfeedback['format'] = '1';
283 $question->generalfeedback['itemid'] = '';
59a3fcd3
TH
284
285 $question->options->questions = array();
ab50232b 286 $question->defaultmark = 0; // Will be increased for each answer norm
516cf3eb 287
ab50232b
TH
288 for ($positionkey = 1;
289 preg_match('/'.ANSWER_REGEX.'/', $question->questiontext['text'], $answerregs);
290 ++$positionkey) {
0ff4bd08 291 $wrapped = new stdClass();
61dfe97e
PP
292 $wrapped->generalfeedback['text'] = '';
293 $wrapped->generalfeedback['format'] = '1';
294 $wrapped->generalfeedback['itemid'] = '';
59a3fcd3 295 if (isset($answerregs[ANSWER_REGEX_NORM])&& $answerregs[ANSWER_REGEX_NORM]!== '') {
ab50232b 296 $wrapped->defaultmark = $answerregs[ANSWER_REGEX_NORM];
8795a5ae 297 } else {
ab50232b 298 $wrapped->defaultmark = '1';
8795a5ae 299 }
516cf3eb 300 if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_NUMERICAL])) {
dfa47f96 301 $wrapped->qtype = 'numerical';
516cf3eb 302 $wrapped->multiplier = array();
303 $wrapped->units = array();
61dfe97e
PP
304 $wrapped->instructions['text'] = '';
305 $wrapped->instructions['format'] = '1';
306 $wrapped->instructions['itemid'] = '';
59a3fcd3 307 } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER])) {
dfa47f96 308 $wrapped->qtype = 'shortanswer';
516cf3eb 309 $wrapped->usecase = 0;
59a3fcd3 310 } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER_C])) {
fd97082c 311 $wrapped->qtype = 'shortanswer';
312 $wrapped->usecase = 1;
59a3fcd3 313 } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE])) {
dfa47f96 314 $wrapped->qtype = 'multichoice';
516cf3eb 315 $wrapped->single = 1;
271e6dec 316 $wrapped->answernumbering = 0;
61dfe97e
PP
317 $wrapped->correctfeedback['text'] = '';
318 $wrapped->correctfeedback['format'] = '1';
319 $wrapped->correctfeedback['itemid'] = '';
320 $wrapped->partiallycorrectfeedback['text'] = '';
321 $wrapped->partiallycorrectfeedback['format'] = '1';
322 $wrapped->partiallycorrectfeedback['itemid'] = '';
323 $wrapped->incorrectfeedback['text'] = '';
324 $wrapped->incorrectfeedback['format'] = '1';
325 $wrapped->incorrectfeedback['itemid'] = '';
e5ebbd53 326 $wrapped->layout = 0;
59a3fcd3 327 } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_REGULAR])) {
e5ebbd53 328 $wrapped->qtype = 'multichoice';
329 $wrapped->single = 1;
330 $wrapped->answernumbering = 0;
61dfe97e
PP
331 $wrapped->correctfeedback['text'] = '';
332 $wrapped->correctfeedback['format'] = '1';
333 $wrapped->correctfeedback['itemid'] = '';
334 $wrapped->partiallycorrectfeedback['text'] = '';
335 $wrapped->partiallycorrectfeedback['format'] = '1';
336 $wrapped->partiallycorrectfeedback['itemid'] = '';
337 $wrapped->incorrectfeedback['text'] = '';
338 $wrapped->incorrectfeedback['format'] = '1';
339 $wrapped->incorrectfeedback['itemid'] = '';
e5ebbd53 340 $wrapped->layout = 1;
59a3fcd3 341 } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_HORIZONTAL])) {
e5ebbd53 342 $wrapped->qtype = 'multichoice';
343 $wrapped->single = 1;
344 $wrapped->answernumbering = 0;
61dfe97e
PP
345 $wrapped->correctfeedback['text'] = '';
346 $wrapped->correctfeedback['format'] = '1';
347 $wrapped->correctfeedback['itemid'] = '';
348 $wrapped->partiallycorrectfeedback['text'] = '';
349 $wrapped->partiallycorrectfeedback['format'] = '1';
350 $wrapped->partiallycorrectfeedback['itemid'] = '';
351 $wrapped->incorrectfeedback['text'] = '';
352 $wrapped->incorrectfeedback['format'] = '1';
353 $wrapped->incorrectfeedback['itemid'] = '';
e5ebbd53 354 $wrapped->layout = 2;
516cf3eb 355 } else {
2471ef86 356 print_error('unknownquestiontype', 'question', '', $answerregs[2]);
516cf3eb 357 return false;
358 }
359
360 // Each $wrapped simulates a $form that can be processed by the
361 // respective save_question and save_question_options methods of the
362 // wrapped questiontypes
363 $wrapped->answer = array();
364 $wrapped->fraction = array();
365 $wrapped->feedback = array();
366 $wrapped->shuffleanswers = 1;
61dfe97e 367 $wrapped->questiontext['text'] = $answerregs[0];
59a3fcd3
TH
368 $wrapped->questiontext['format'] = 0;
369 $wrapped->questiontext['itemid'] = '';
370 $answerindex = 0;
516cf3eb 371
372 $remainingalts = $answerregs[ANSWER_REGEX_ALTERNATIVES];
373 while (preg_match('/~?'.ANSWER_ALTERNATIVE_REGEX.'/', $remainingalts, $altregs)) {
374 if ('=' == $altregs[ANSWER_ALTERNATIVE_REGEX_FRACTION]) {
61dfe97e 375 $wrapped->fraction["$answerindex"] = '1';
59a3fcd3 376 } else if ($percentile = $altregs[ANSWER_ALTERNATIVE_REGEX_PERCENTILE_FRACTION]) {
61dfe97e 377 $wrapped->fraction["$answerindex"] = .01 * $percentile;
516cf3eb 378 } else {
61dfe97e 379 $wrapped->fraction["$answerindex"] = '0';
516cf3eb 380 }
e51efd7e 381 if (isset($altregs[ANSWER_ALTERNATIVE_REGEX_FEEDBACK])) {
ab50232b
TH
382 $feedback = html_entity_decode(
383 $altregs[ANSWER_ALTERNATIVE_REGEX_FEEDBACK], ENT_QUOTES, 'UTF-8');
095b599a 384 $feedback = str_replace('\}', '}', $feedback);
61dfe97e
PP
385 $wrapped->feedback["$answerindex"]['text'] = str_replace('\#', '#', $feedback);
386 $wrapped->feedback["$answerindex"]['format'] = '1';
387 $wrapped->feedback["$answerindex"]['itemid'] = '';
e51efd7e 388 } else {
61dfe97e
PP
389 $wrapped->feedback["$answerindex"]['text'] = '';
390 $wrapped->feedback["$answerindex"]['format'] = '1';
391 $wrapped->feedback["$answerindex"]['itemid'] = '1';
392
e51efd7e 393 }
516cf3eb 394 if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_NUMERICAL])
ab50232b
TH
395 && preg_match('~'.NUMERICAL_ALTERNATIVE_REGEX.'~',
396 $altregs[ANSWER_ALTERNATIVE_REGEX_ANSWER], $numregs)) {
e51efd7e 397 $wrapped->answer[] = $numregs[NUMERICAL_CORRECT_ANSWER];
516cf3eb 398 if ($numregs[NUMERICAL_ABS_ERROR_MARGIN]) {
61dfe97e 399 $wrapped->tolerance["$answerindex"] =
e51efd7e 400 $numregs[NUMERICAL_ABS_ERROR_MARGIN];
516cf3eb 401 } else {
61dfe97e 402 $wrapped->tolerance["$answerindex"] = 0;
516cf3eb 403 }
404 } else { // Tolerance can stay undefined for non numerical questions
1f8db780 405 // Undo quoting done by the HTML editor.
ab50232b
TH
406 $answer = html_entity_decode(
407 $altregs[ANSWER_ALTERNATIVE_REGEX_ANSWER], ENT_QUOTES, 'UTF-8');
095b599a 408 $answer = str_replace('\}', '}', $answer);
61dfe97e 409 $wrapped->answer["$answerindex"] = str_replace('\#', '#', $answer);
516cf3eb 410 }
411 $tmp = explode($altregs[0], $remainingalts, 2);
412 $remainingalts = $tmp[1];
59a3fcd3 413 $answerindex++;
516cf3eb 414 }
415
ab50232b 416 $question->defaultmark += $wrapped->defaultmark;
516cf3eb 417 $question->options->questions[$positionkey] = clone($wrapped);
61dfe97e
PP
418 $question->questiontext['text'] = implode("{#$positionkey}",
419 explode($answerregs[0], $question->questiontext['text'], 2));
516cf3eb 420 }
e51efd7e 421 $question->questiontext = $question->questiontext;
516cf3eb 422 return $question;
423}