Merge branch 'MDL-70119-310' of git://github.com/ferranrecio/moodle into MOODLE_310_S...
[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
97562c4d 29require_once($CFG->dirroot . '/question/type/questiontypebase.php');
dcedbb0e 30require_once($CFG->dirroot . '/question/type/multichoice/question.php');
48aad79a 31require_once($CFG->dirroot . '/question/type/numerical/questiontype.php');
dcedbb0e 32
1976496e 33/**
d3603157
TH
34 * The multi-answer question type class.
35 *
36 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
37 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
7375c542 38 */
ab50232b 39class qtype_multianswer extends question_type {
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
bb930b7d 48 parent::get_question_options($question);
3d9645ae 49 // Get relevant data indexed by positionkey from the multianswers table.
ab50232b 50 $sequence = $DB->get_field('question_multianswer', 'sequence',
3112c220 51 array('question' => $question->id), MUST_EXIST);
516cf3eb 52
ab50232b
TH
53 $wrappedquestions = $DB->get_records_list('question', 'id',
54 explode(',', $sequence), 'id ASC');
516cf3eb 55
3d9645ae 56 // We want an array with question ids as index and the positions as values.
516cf3eb 57 $sequence = array_flip(explode(',', $sequence));
484b43f4
MG
58 array_walk($sequence, function(&$val) {
59 $val++;
60 });
ab50232b
TH
61
62 // If a question is lost, the corresponding index is null
aeb15530 63 // so this null convention is used to test $question->options->questions
857caf3b 64 // before using the values.
3d9645ae 65 // First all possible questions from sequence are nulled
66 // then filled with the data if available in $wrappedquestions.
59a3fcd3
TH
67 foreach ($sequence as $seq) {
68 $question->options->questions[$seq] = '';
df79079f 69 }
ab50232b
TH
70
71 foreach ($wrappedquestions as $wrapped) {
72 question_bank::get_qtype($wrapped->qtype)->get_question_options($wrapped);
3d9645ae 73 // For wrapped questions the maxgrade is always equal to the defaultmark,
74 // there is no entry in the question_instances table for them.
ab50232b
TH
75 $wrapped->maxmark = $wrapped->defaultmark;
76 $question->options->questions[$sequence[$wrapped->id]] = $wrapped;
857caf3b 77 }
a4499532
TH
78 $question->hints = $DB->get_records('question_hints',
79 array('questionid' => $question->id), 'id ASC');
80
516cf3eb 81 return true;
82 }
83
59a3fcd3
TH
84 public function save_question_options($question) {
85 global $DB;
0ff4bd08 86 $result = new stdClass();
9fc3100f 87
516cf3eb 88 // This function needs to be able to handle the case where the existing set of wrapped
89 // questions does not match the new set of wrapped questions so that some need to be
3d9645ae 90 // created, some modified and some deleted.
516cf3eb 91 // Unfortunately the code currently simply overwrites existing ones in sequence. This
9fc3100f 92 // will make re-marking after a re-ordering of wrapped questions impossible and
516cf3eb 93 // will also create difficulties if questiontype specific tables reference the id.
9fc3100f 94
3d9645ae 95 // First we get all the existing wrapped questions.
3e24d6f6
MG
96 $oldwrappedquestions = [];
97 if ($oldwrappedids = $DB->get_field('question_multianswer', 'sequence',
ab50232b 98 array('question' => $question->id))) {
3e24d6f6
MG
99 $oldwrappedidsarray = explode(',', $oldwrappedids);
100 $unorderedquestions = $DB->get_records_list('question', 'id', $oldwrappedidsarray);
101
102 // Keep the order as given in the sequence field.
103 foreach ($oldwrappedidsarray as $questionid) {
104 if (isset($unorderedquestions[$questionid])) {
105 $oldwrappedquestions[] = $unorderedquestions[$questionid];
106 }
107 }
516cf3eb 108 }
ab50232b 109
516cf3eb 110 $sequence = array();
59a3fcd3
TH
111 foreach ($question->options->questions as $wrapped) {
112 if (!empty($wrapped)) {
3d9645ae 113 // If we still have some old wrapped question ids, reuse the next of them.
f34488b2 114
12c6e008
TH
115 if (is_array($oldwrappedquestions) &&
116 $oldwrappedquestion = array_shift($oldwrappedquestions)) {
857caf3b 117 $wrapped->id = $oldwrappedquestion->id;
59a3fcd3 118 if ($oldwrappedquestion->qtype != $wrapped->qtype) {
857caf3b 119 switch ($oldwrappedquestion->qtype) {
59a3fcd3 120 case 'multichoice':
0aa04a7f
TH
121 $DB->delete_records('qtype_multichoice_options',
122 array('questionid' => $oldwrappedquestion->id));
59a3fcd3
TH
123 break;
124 case 'shortanswer':
b4cb0957
TH
125 $DB->delete_records('qtype_shortanswer_options',
126 array('questionid' => $oldwrappedquestion->id));
59a3fcd3
TH
127 break;
128 case 'numerical':
ab50232b
TH
129 $DB->delete_records('question_numerical',
130 array('question' => $oldwrappedquestion->id));
59a3fcd3
TH
131 break;
132 default:
ab50232b
TH
133 throw new moodle_exception('qtypenotrecognized',
134 'qtype_multianswer', '', $oldwrappedquestion->qtype);
59a3fcd3 135 $wrapped->id = 0;
df79079f 136 }
e9028ffc 137 }
59a3fcd3
TH
138 } else {
139 $wrapped->id = 0;
e9028ffc 140 }
516cf3eb 141 }
77fa3a0d 142 $wrapped->name = $question->name;
143 $wrapped->parent = $question->id;
59a3fcd3 144 $previousid = $wrapped->id;
3d9645ae 145 // Save_question strips this extra bit off the category again.
ab50232b
TH
146 $wrapped->category = $question->category . ',1';
147 $wrapped = question_bank::get_qtype($wrapped->qtype)->save_question(
148 $wrapped, clone($wrapped));
516cf3eb 149 $sequence[] = $wrapped->id;
59a3fcd3 150 if ($previousid != 0 && $previousid != $wrapped->id) {
3d9645ae 151 // For some reasons a new question has been created
152 // so delete the old one.
3908a523 153 question_delete_question($previousid);
26053641 154 }
516cf3eb 155 }
156
3d9645ae 157 // Delete redundant wrapped questions.
59a3fcd3 158 if (is_array($oldwrappedquestions) && count($oldwrappedquestions)) {
26053641 159 foreach ($oldwrappedquestions as $oldwrappedquestion) {
3908a523 160 question_delete_question($oldwrappedquestion->id);
e9028ffc 161 }
4bc4ca50 162 }
516cf3eb 163
164 if (!empty($sequence)) {
0ff4bd08 165 $multianswer = new stdClass();
516cf3eb 166 $multianswer->question = $question->id;
167 $multianswer->sequence = implode(',', $sequence);
12c6e008
TH
168 if ($oldid = $DB->get_field('question_multianswer', 'id',
169 array('question' => $question->id))) {
516cf3eb 170 $multianswer->id = $oldid;
ab50232b 171 $DB->update_record('question_multianswer', $multianswer);
516cf3eb 172 } else {
ab50232b 173 $DB->insert_record('question_multianswer', $multianswer);
516cf3eb 174 }
175 }
a4499532 176
e9af6091 177 $this->save_hints($question, true);
516cf3eb 178 }
179
59a3fcd3 180 public function save_question($authorizedquestion, $form) {
e51efd7e 181 $question = qtype_multianswer_extract_question($form->questiontext);
516cf3eb 182 if (isset($authorizedquestion->id)) {
183 $question->id = $authorizedquestion->id;
516cf3eb 184 }
185
516cf3eb 186 $question->category = $authorizedquestion->category;
ab50232b 187 $form->defaultmark = $question->defaultmark;
516cf3eb 188 $form->questiontext = $question->questiontext;
189 $form->questiontextformat = 0;
77fa3a0d 190 $form->options = clone($question->options);
516cf3eb 191 unset($question->options);
94dbfb3a 192 return parent::save_question($question, $form);
516cf3eb 193 }
194
e9af6091
JMV
195 protected function make_hint($hint) {
196 return question_hint_with_parts::load_from_record($hint);
197 }
198
59a3fcd3 199 public function delete_question($questionid, $contextid) {
f34488b2 200 global $DB;
ab50232b 201 $DB->delete_records('question_multianswer', array('question' => $questionid));
9203b705
TH
202
203 parent::delete_question($questionid, $contextid);
516cf3eb 204 }
205
072db71c 206 protected function initialise_question_instance(question_definition $question, $questiondata) {
ab50232b 207 parent::initialise_question_instance($question, $questiondata);
42a5b055
TH
208
209 $bits = preg_split('/\{#(\d+)\}/', $question->questiontext,
210 null, PREG_SPLIT_DELIM_CAPTURE);
211 $question->textfragments[0] = array_shift($bits);
212 $i = 1;
213 while (!empty($bits)) {
214 $question->places[$i] = array_shift($bits);
215 $question->textfragments[$i] = array_shift($bits);
216 $i += 1;
217 }
ab50232b
TH
218 foreach ($questiondata->options->questions as $key => $subqdata) {
219 $subqdata->contextid = $questiondata->contextid;
bd156853
PP
220 if ($subqdata->qtype == 'multichoice') {
221 $answerregs = array();
222 if ($subqdata->options->shuffleanswers == 1 && isset($questiondata->options->shuffleanswers)
223 && $questiondata->options->shuffleanswers == 0 ) {
224 $subqdata->options->shuffleanswers = 0;
225 }
226 }
ab50232b 227 $question->subquestions[$key] = question_bank::make_question($subqdata);
fa6c8620 228 $question->subquestions[$key]->maxmark = $subqdata->defaultmark;
7ac7977c
TH
229 if (isset($subqdata->options->layout)) {
230 $question->subquestions[$key]->layout = $subqdata->options->layout;
231 }
869309b8 232 }
869309b8 233 }
869309b8 234
ab50232b
TH
235 public function get_random_guess_score($questiondata) {
236 $fractionsum = 0;
237 $fractionmax = 0;
238 foreach ($questiondata->options->questions as $key => $subqdata) {
fa6c8620 239 $fractionmax += $subqdata->defaultmark;
ab50232b
TH
240 $fractionsum += question_bank::get_qtype(
241 $subqdata->qtype)->get_random_guess_score($subqdata);
516cf3eb 242 }
ab50232b 243 return $fractionsum / $fractionmax;
516cf3eb 244 }
d44480f6
TH
245
246 public function move_files($questionid, $oldcontextid, $newcontextid) {
247 parent::move_files($questionid, $oldcontextid, $newcontextid);
248 $this->move_files_in_hints($questionid, $oldcontextid, $newcontextid);
249 }
250
251 protected function delete_files($questionid, $contextid) {
252 parent::delete_files($questionid, $contextid);
253 $this->delete_files_in_hints($questionid, $contextid);
254 }
516cf3eb 255}
516cf3eb 256
ab50232b 257
3d9645ae 258// ANSWER_ALTERNATIVE regexes.
ab50232b 259define('ANSWER_ALTERNATIVE_FRACTION_REGEX',
0b346164 260 '=|%(-?[0-9]+)%');
3d9645ae 261// For the syntax '(?<!' see http://www.perl.com/doc/manual/html/pod/perlre.html#item_C.
ab50232b 262define('ANSWER_ALTERNATIVE_ANSWER_REGEX',
e51efd7e 263 '.+?(?<!\\\\|&|&amp;)(?=[~#}]|$)');
ab50232b 264define('ANSWER_ALTERNATIVE_FEEDBACK_REGEX',
0b346164 265 '.*?(?<!\\\\)(?=[~}]|$)');
ab50232b 266define('ANSWER_ALTERNATIVE_REGEX',
e51efd7e 267 '(' . ANSWER_ALTERNATIVE_FRACTION_REGEX .')?' .
268 '(' . ANSWER_ALTERNATIVE_ANSWER_REGEX . ')' .
269 '(#(' . ANSWER_ALTERNATIVE_FEEDBACK_REGEX .'))?');
0b346164 270
3d9645ae 271// Parenthesis positions for ANSWER_ALTERNATIVE_REGEX.
ab50232b
TH
272define('ANSWER_ALTERNATIVE_REGEX_PERCENTILE_FRACTION', 2);
273define('ANSWER_ALTERNATIVE_REGEX_FRACTION', 1);
274define('ANSWER_ALTERNATIVE_REGEX_ANSWER', 3);
275define('ANSWER_ALTERNATIVE_REGEX_FEEDBACK', 5);
0b346164 276
277// NUMBER_FORMATED_ALTERNATIVE_ANSWER_REGEX is used
3d9645ae 278// for identifying numerical answers in ANSWER_ALTERNATIVE_REGEX_ANSWER.
ab50232b 279define('NUMBER_REGEX',
e64e28d7 280 '-?(([0-9]+[.,]?[0-9]*|[.,][0-9]+)([eE][-+]?[0-9]+)?)');
ab50232b 281define('NUMERICAL_ALTERNATIVE_REGEX',
e64e28d7 282 '^(' . NUMBER_REGEX . ')(:' . NUMBER_REGEX . ')?$');
0b346164 283
3d9645ae 284// Parenthesis positions for NUMERICAL_FORMATED_ALTERNATIVE_ANSWER_REGEX.
ab50232b
TH
285define('NUMERICAL_CORRECT_ANSWER', 1);
286define('NUMERICAL_ABS_ERROR_MARGIN', 6);
0b346164 287
3d9645ae 288// Remaining ANSWER regexes.
ab50232b 289define('ANSWER_TYPE_DEF_REGEX',
12c6e008 290 '(NUMERICAL|NM)|(MULTICHOICE|MC)|(MULTICHOICE_V|MCV)|(MULTICHOICE_H|MCH)|' .
bd156853 291 '(SHORTANSWER|SA|MW)|(SHORTANSWER_C|SAC|MWC)|' .
946ab15c
DS
292 '(MULTICHOICE_S|MCS)|(MULTICHOICE_VS|MCVS)|(MULTICHOICE_HS|MCHS)|'.
293 '(MULTIRESPONSE|MR)|(MULTIRESPONSE_H|MRH)|(MULTIRESPONSE_S|MRS)|(MULTIRESPONSE_HS|MRHS)');
ab50232b 294define('ANSWER_START_REGEX',
0b346164 295 '\{([0-9]*):(' . ANSWER_TYPE_DEF_REGEX . '):');
296
ab50232b 297define('ANSWER_REGEX',
0b346164 298 ANSWER_START_REGEX
299 . '(' . ANSWER_ALTERNATIVE_REGEX
300 . '(~'
301 . ANSWER_ALTERNATIVE_REGEX
59a3fcd3 302 . ')*)\}');
0b346164 303
3d9645ae 304// Parenthesis positions for singulars in ANSWER_REGEX.
ab50232b
TH
305define('ANSWER_REGEX_NORM', 1);
306define('ANSWER_REGEX_ANSWER_TYPE_NUMERICAL', 3);
307define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE', 4);
308define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_REGULAR', 5);
309define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_HORIZONTAL', 6);
310define('ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER', 7);
311define('ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER_C', 8);
bd156853
PP
312define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_SHUFFLED', 9);
313define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_REGULAR_SHUFFLED', 10);
314define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_HORIZONTAL_SHUFFLED', 11);
946ab15c
DS
315define('ANSWER_REGEX_ANSWER_TYPE_MULTIRESPONSE', 12);
316define('ANSWER_REGEX_ANSWER_TYPE_MULTIRESPONSE_HORIZONTAL', 13);
317define('ANSWER_REGEX_ANSWER_TYPE_MULTIRESPONSE_SHUFFLED', 14);
318define('ANSWER_REGEX_ANSWER_TYPE_MULTIRESPONSE_HORIZONTAL_SHUFFLED', 15);
319define('ANSWER_REGEX_ALTERNATIVES', 16);
bd156853
PP
320
321/**
322 * Initialise subquestion fields that are constant across all MULTICHOICE
323 * types.
324 *
325 * @param objet $wrapped The subquestion to initialise
326 *
327 */
328function qtype_multianswer_initialise_multichoice_subquestion($wrapped) {
329 $wrapped->qtype = 'multichoice';
330 $wrapped->single = 1;
331 $wrapped->answernumbering = 0;
332 $wrapped->correctfeedback['text'] = '';
333 $wrapped->correctfeedback['format'] = FORMAT_HTML;
334 $wrapped->correctfeedback['itemid'] = '';
335 $wrapped->partiallycorrectfeedback['text'] = '';
336 $wrapped->partiallycorrectfeedback['format'] = FORMAT_HTML;
337 $wrapped->partiallycorrectfeedback['itemid'] = '';
338 $wrapped->incorrectfeedback['text'] = '';
339 $wrapped->incorrectfeedback['format'] = FORMAT_HTML;
340 $wrapped->incorrectfeedback['itemid'] = '';
341}
516cf3eb 342
7518b645 343function qtype_multianswer_extract_question($text) {
3d9645ae 344 // Variable $text is an array [text][format][itemid].
0ff4bd08 345 $question = new stdClass();
dfa47f96 346 $question->qtype = 'multianswer';
516cf3eb 347 $question->questiontext = $text;
61dfe97e 348 $question->generalfeedback['text'] = '';
c1f15d35 349 $question->generalfeedback['format'] = FORMAT_HTML;
61dfe97e 350 $question->generalfeedback['itemid'] = '';
59a3fcd3 351
caee6e6c 352 $question->options = new stdClass();
59a3fcd3 353 $question->options->questions = array();
3d9645ae 354 $question->defaultmark = 0; // Will be increased for each answer norm.
516cf3eb 355
ab50232b 356 for ($positionkey = 1;
aa9d6e43 357 preg_match('/'.ANSWER_REGEX.'/s', $question->questiontext['text'], $answerregs);
ab50232b 358 ++$positionkey) {
0ff4bd08 359 $wrapped = new stdClass();
61dfe97e 360 $wrapped->generalfeedback['text'] = '';
c1f15d35 361 $wrapped->generalfeedback['format'] = FORMAT_HTML;
61dfe97e 362 $wrapped->generalfeedback['itemid'] = '';
48b5b28f 363 if (isset($answerregs[ANSWER_REGEX_NORM]) && $answerregs[ANSWER_REGEX_NORM] !== '') {
ab50232b 364 $wrapped->defaultmark = $answerregs[ANSWER_REGEX_NORM];
8795a5ae 365 } else {
ab50232b 366 $wrapped->defaultmark = '1';
8795a5ae 367 }
516cf3eb 368 if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_NUMERICAL])) {
dfa47f96 369 $wrapped->qtype = 'numerical';
516cf3eb 370 $wrapped->multiplier = array();
371 $wrapped->units = array();
61dfe97e 372 $wrapped->instructions['text'] = '';
c1f15d35 373 $wrapped->instructions['format'] = FORMAT_HTML;
61dfe97e 374 $wrapped->instructions['itemid'] = '';
59a3fcd3 375 } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER])) {
dfa47f96 376 $wrapped->qtype = 'shortanswer';
516cf3eb 377 $wrapped->usecase = 0;
59a3fcd3 378 } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER_C])) {
fd97082c 379 $wrapped->qtype = 'shortanswer';
380 $wrapped->usecase = 1;
59a3fcd3 381 } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE])) {
bd156853
PP
382 qtype_multianswer_initialise_multichoice_subquestion($wrapped);
383 $wrapped->shuffleanswers = 0;
384 $wrapped->layout = qtype_multichoice_base::LAYOUT_DROPDOWN;
385 } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_SHUFFLED])) {
386 qtype_multianswer_initialise_multichoice_subquestion($wrapped);
1cacb059 387 $wrapped->shuffleanswers = 1;
7ac7977c 388 $wrapped->layout = qtype_multichoice_base::LAYOUT_DROPDOWN;
59a3fcd3 389 } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_REGULAR])) {
bd156853 390 qtype_multianswer_initialise_multichoice_subquestion($wrapped);
1cacb059 391 $wrapped->shuffleanswers = 0;
bd156853
PP
392 $wrapped->layout = qtype_multichoice_base::LAYOUT_VERTICAL;
393 } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_REGULAR_SHUFFLED])) {
394 qtype_multianswer_initialise_multichoice_subquestion($wrapped);
395 $wrapped->shuffleanswers = 1;
7ac7977c 396 $wrapped->layout = qtype_multichoice_base::LAYOUT_VERTICAL;
59a3fcd3 397 } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_HORIZONTAL])) {
bd156853 398 qtype_multianswer_initialise_multichoice_subquestion($wrapped);
1cacb059 399 $wrapped->shuffleanswers = 0;
bd156853
PP
400 $wrapped->layout = qtype_multichoice_base::LAYOUT_HORIZONTAL;
401 } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_HORIZONTAL_SHUFFLED])) {
402 qtype_multianswer_initialise_multichoice_subquestion($wrapped);
403 $wrapped->shuffleanswers = 1;
7ac7977c 404 $wrapped->layout = qtype_multichoice_base::LAYOUT_HORIZONTAL;
946ab15c
DS
405 } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTIRESPONSE])) {
406 qtype_multianswer_initialise_multichoice_subquestion($wrapped);
407 $wrapped->single = 0;
408 $wrapped->shuffleanswers = 0;
409 $wrapped->layout = qtype_multichoice_base::LAYOUT_VERTICAL;
410 } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTIRESPONSE_HORIZONTAL])) {
411 qtype_multianswer_initialise_multichoice_subquestion($wrapped);
412 $wrapped->single = 0;
413 $wrapped->shuffleanswers = 0;
414 $wrapped->layout = qtype_multichoice_base::LAYOUT_HORIZONTAL;
415 } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTIRESPONSE_SHUFFLED])) {
416 qtype_multianswer_initialise_multichoice_subquestion($wrapped);
417 $wrapped->single = 0;
418 $wrapped->shuffleanswers = 1;
419 $wrapped->layout = qtype_multichoice_base::LAYOUT_VERTICAL;
420 } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTIRESPONSE_HORIZONTAL_SHUFFLED])) {
421 qtype_multianswer_initialise_multichoice_subquestion($wrapped);
422 $wrapped->single = 0;
423 $wrapped->shuffleanswers = 1;
424 $wrapped->layout = qtype_multichoice_base::LAYOUT_HORIZONTAL;
516cf3eb 425 } else {
2471ef86 426 print_error('unknownquestiontype', 'question', '', $answerregs[2]);
516cf3eb 427 return false;
428 }
429
430 // Each $wrapped simulates a $form that can be processed by the
431 // respective save_question and save_question_options methods of the
3d9645ae 432 // wrapped questiontypes.
516cf3eb 433 $wrapped->answer = array();
434 $wrapped->fraction = array();
435 $wrapped->feedback = array();
61dfe97e 436 $wrapped->questiontext['text'] = $answerregs[0];
c1f15d35 437 $wrapped->questiontext['format'] = FORMAT_HTML;
59a3fcd3
TH
438 $wrapped->questiontext['itemid'] = '';
439 $answerindex = 0;
516cf3eb 440
946ab15c 441 $hasspecificfraction = false;
516cf3eb 442 $remainingalts = $answerregs[ANSWER_REGEX_ALTERNATIVES];
aa9d6e43 443 while (preg_match('/~?'.ANSWER_ALTERNATIVE_REGEX.'/s', $remainingalts, $altregs)) {
516cf3eb 444 if ('=' == $altregs[ANSWER_ALTERNATIVE_REGEX_FRACTION]) {
f4fe3968 445 $wrapped->fraction["{$answerindex}"] = '1';
59a3fcd3 446 } else if ($percentile = $altregs[ANSWER_ALTERNATIVE_REGEX_PERCENTILE_FRACTION]) {
f4fe3968 447 $wrapped->fraction["{$answerindex}"] = .01 * $percentile;
946ab15c 448 $hasspecificfraction = true;
516cf3eb 449 } else {
f4fe3968 450 $wrapped->fraction["{$answerindex}"] = '0';
516cf3eb 451 }
e51efd7e 452 if (isset($altregs[ANSWER_ALTERNATIVE_REGEX_FEEDBACK])) {
ab50232b
TH
453 $feedback = html_entity_decode(
454 $altregs[ANSWER_ALTERNATIVE_REGEX_FEEDBACK], ENT_QUOTES, 'UTF-8');
095b599a 455 $feedback = str_replace('\}', '}', $feedback);
f4fe3968
TH
456 $wrapped->feedback["{$answerindex}"]['text'] = str_replace('\#', '#', $feedback);
457 $wrapped->feedback["{$answerindex}"]['format'] = FORMAT_HTML;
458 $wrapped->feedback["{$answerindex}"]['itemid'] = '';
e51efd7e 459 } else {
f4fe3968
TH
460 $wrapped->feedback["{$answerindex}"]['text'] = '';
461 $wrapped->feedback["{$answerindex}"]['format'] = FORMAT_HTML;
462 $wrapped->feedback["{$answerindex}"]['itemid'] = '';
61dfe97e 463
e51efd7e 464 }
516cf3eb 465 if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_NUMERICAL])
aa9d6e43 466 && preg_match('~'.NUMERICAL_ALTERNATIVE_REGEX.'~s',
ab50232b 467 $altregs[ANSWER_ALTERNATIVE_REGEX_ANSWER], $numregs)) {
e51efd7e 468 $wrapped->answer[] = $numregs[NUMERICAL_CORRECT_ANSWER];
c83ed025 469 if (array_key_exists(NUMERICAL_ABS_ERROR_MARGIN, $numregs)) {
f4fe3968 470 $wrapped->tolerance["{$answerindex}"] =
e51efd7e 471 $numregs[NUMERICAL_ABS_ERROR_MARGIN];
516cf3eb 472 } else {
f4fe3968 473 $wrapped->tolerance["{$answerindex}"] = 0;
516cf3eb 474 }
3d9645ae 475 } else { // Tolerance can stay undefined for non numerical questions.
1f8db780 476 // Undo quoting done by the HTML editor.
ab50232b
TH
477 $answer = html_entity_decode(
478 $altregs[ANSWER_ALTERNATIVE_REGEX_ANSWER], ENT_QUOTES, 'UTF-8');
095b599a 479 $answer = str_replace('\}', '}', $answer);
f4fe3968 480 $wrapped->answer["{$answerindex}"] = str_replace('\#', '#', $answer);
c1f15d35 481 if ($wrapped->qtype == 'multichoice') {
f4fe3968
TH
482 $wrapped->answer["{$answerindex}"] = array(
483 'text' => $wrapped->answer["{$answerindex}"],
c1f15d35
TH
484 'format' => FORMAT_HTML,
485 'itemid' => '');
486 }
516cf3eb 487 }
488 $tmp = explode($altregs[0], $remainingalts, 2);
489 $remainingalts = $tmp[1];
59a3fcd3 490 $answerindex++;
516cf3eb 491 }
492
946ab15c
DS
493 // Fix the score for multichoice_multi questions (as positive scores should add up to 1, not have a maximum of 1).
494 if (isset($wrapped->single) && $wrapped->single == 0) {
495 $total = 0;
496 foreach ($wrapped->fraction as $idx => $fraction) {
497 if ($fraction > 0) {
498 $total += $fraction;
499 }
500 }
501 if ($total) {
502 foreach ($wrapped->fraction as $idx => $fraction) {
503 if ($fraction > 0) {
504 $wrapped->fraction[$idx] = $fraction / $total;
505 } else if (!$hasspecificfraction) {
506 // If no specific fractions are given, set incorrect answers to each cancel out one correct answer.
507 $wrapped->fraction[$idx] = -(1.0 / $total);
508 }
509 }
510 }
511 }
512
ab50232b 513 $question->defaultmark += $wrapped->defaultmark;
516cf3eb 514 $question->options->questions[$positionkey] = clone($wrapped);
61dfe97e
PP
515 $question->questiontext['text'] = implode("{#$positionkey}",
516 explode($answerregs[0], $question->questiontext['text'], 2));
516cf3eb 517 }
516cf3eb 518 return $question;
519}
48aad79a
JMV
520
521/**
522 * Validate a multianswer question.
523 *
524 * @param object $question The multianswer question to validate as returned by qtype_multianswer_extract_question
525 * @return array Array of error messages with questions field names as keys.
526 */
527function qtype_multianswer_validate_question(stdClass $question) : array {
528 $errors = array();
529 if (!isset($question->options->questions)) {
530 $errors['questiontext'] = get_string('questionsmissing', 'qtype_multianswer');
531 } else {
532 $subquestions = fullclone($question->options->questions);
533 if (count($subquestions)) {
534 $sub = 1;
535 foreach ($subquestions as $subquestion) {
536 $prefix = 'sub_'.$sub.'_';
537 $answercount = 0;
538 $maxgrade = false;
539 $maxfraction = -1;
540
541 foreach ($subquestion->answer as $key => $answer) {
542 if (is_array($answer)) {
543 $answer = $answer['text'];
544 }
545 $trimmedanswer = trim($answer);
546 if ($trimmedanswer !== '') {
547 $answercount++;
548 if ($subquestion->qtype == 'numerical' &&
549 !(qtype_numerical::is_valid_number($trimmedanswer) || $trimmedanswer == '*')) {
550 $errors[$prefix.'answer['.$key.']'] =
551 get_string('answermustbenumberorstar', 'qtype_numerical');
552 }
553 if ($subquestion->fraction[$key] == 1) {
554 $maxgrade = true;
555 }
556 if ($subquestion->fraction[$key] > $maxfraction) {
557 $maxfraction = $subquestion->fraction[$key];
558 }
559 // For 'multiresponse' we are OK if there is at least one fraction > 0.
560 if ($subquestion->qtype == 'multichoice' && $subquestion->single == 0 &&
561 $subquestion->fraction[$key] > 0) {
562 $maxgrade = true;
563 }
564 }
565 }
566 if ($subquestion->qtype == 'multichoice' && $answercount < 2) {
567 $errors[$prefix.'answer[0]'] = get_string('notenoughanswers', 'qtype_multichoice', 2);
568 } else if ($answercount == 0) {
569 $errors[$prefix.'answer[0]'] = get_string('notenoughanswers', 'question', 1);
570 }
571 if ($maxgrade == false) {
572 $errors[$prefix.'fraction[0]'] = get_string('fractionsnomax', 'question');
573 }
574 $sub++;
575 }
576 } else {
577 $errors['questiontext'] = get_string('questionsmissing', 'qtype_multianswer');
578 }
579 }
580 return $errors;
581}