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