MDL-3782 qtype_multianswer: support 'multiple-answer' subquestions
[moodle.git] / question / type / multianswer / questiontype.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  * 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  */
27 defined('MOODLE_INTERNAL') || die();
29 require_once($CFG->dirroot . '/question/type/questiontypebase.php');
30 require_once($CFG->dirroot . '/question/type/multichoice/question.php');
32 /**
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
37  */
38 class qtype_multianswer extends question_type {
40     public function can_analyse_responses() {
41         return false;
42     }
44     public function get_question_options($question) {
45         global $DB, $OUTPUT;
47         // Get relevant data indexed by positionkey from the multianswers table.
48         $sequence = $DB->get_field('question_multianswer', 'sequence',
49                 array('question' => $question->id), '*', MUST_EXIST);
51         $wrappedquestions = $DB->get_records_list('question', 'id',
52                 explode(',', $sequence), 'id ASC');
54         // We want an array with question ids as index and the positions as values.
55         $sequence = array_flip(explode(',', $sequence));
56         array_walk($sequence, create_function('&$val', '$val++;'));
58         // If a question is lost, the corresponding index is null
59         // so this null convention is used to test $question->options->questions
60         // before using the values.
61         // First all possible questions from sequence are nulled
62         // then filled with the data if available in  $wrappedquestions.
63         foreach ($sequence as $seq) {
64             $question->options->questions[$seq] = '';
65         }
67         foreach ($wrappedquestions as $wrapped) {
68             question_bank::get_qtype($wrapped->qtype)->get_question_options($wrapped);
69             // For wrapped questions the maxgrade is always equal to the defaultmark,
70             // there is no entry in the question_instances table for them.
71             $wrapped->maxmark = $wrapped->defaultmark;
72             $question->options->questions[$sequence[$wrapped->id]] = $wrapped;
73         }
74         $question->hints = $DB->get_records('question_hints',
75                 array('questionid' => $question->id), 'id ASC');
77         return true;
78     }
80     public function save_question_options($question) {
81         global $DB;
82         $result = new stdClass();
84         // This function needs to be able to handle the case where the existing set of wrapped
85         // questions does not match the new set of wrapped questions so that some need to be
86         // created, some modified and some deleted.
87         // Unfortunately the code currently simply overwrites existing ones in sequence. This
88         // will make re-marking after a re-ordering of wrapped questions impossible and
89         // will also create difficulties if questiontype specific tables reference the id.
91         // First we get all the existing wrapped questions.
92         if (!$oldwrappedids = $DB->get_field('question_multianswer', 'sequence',
93                 array('question' => $question->id))) {
94             $oldwrappedquestions = array();
95         } else {
96             $oldwrappedquestions = $DB->get_records_list('question', 'id',
97                     explode(',', $oldwrappedids), 'id ASC');
98         }
100         $sequence = array();
101         foreach ($question->options->questions as $wrapped) {
102             if (!empty($wrapped)) {
103                 // If we still have some old wrapped question ids, reuse the next of them.
105                 if (is_array($oldwrappedquestions) &&
106                         $oldwrappedquestion = array_shift($oldwrappedquestions)) {
107                     $wrapped->id = $oldwrappedquestion->id;
108                     if ($oldwrappedquestion->qtype != $wrapped->qtype) {
109                         switch ($oldwrappedquestion->qtype) {
110                             case 'multichoice':
111                                 $DB->delete_records('qtype_multichoice_options',
112                                         array('questionid' => $oldwrappedquestion->id));
113                                 break;
114                             case 'shortanswer':
115                                 $DB->delete_records('qtype_shortanswer_options',
116                                         array('questionid' => $oldwrappedquestion->id));
117                                 break;
118                             case 'numerical':
119                                 $DB->delete_records('question_numerical',
120                                         array('question' => $oldwrappedquestion->id));
121                                 break;
122                             default:
123                                 throw new moodle_exception('qtypenotrecognized',
124                                         'qtype_multianswer', '', $oldwrappedquestion->qtype);
125                                 $wrapped->id = 0;
126                         }
127                     }
128                 } else {
129                     $wrapped->id = 0;
130                 }
131             }
132             $wrapped->name = $question->name;
133             $wrapped->parent = $question->id;
134             $previousid = $wrapped->id;
135             // Save_question strips this extra bit off the category again.
136             $wrapped->category = $question->category . ',1';
137             $wrapped = question_bank::get_qtype($wrapped->qtype)->save_question(
138                     $wrapped, clone($wrapped));
139             $sequence[] = $wrapped->id;
140             if ($previousid != 0 && $previousid != $wrapped->id) {
141                 // For some reasons a new question has been created
142                 // so delete the old one.
143                 question_delete_question($previousid);
144             }
145         }
147         // Delete redundant wrapped questions.
148         if (is_array($oldwrappedquestions) && count($oldwrappedquestions)) {
149             foreach ($oldwrappedquestions as $oldwrappedquestion) {
150                 question_delete_question($oldwrappedquestion->id);
151             }
152         }
154         if (!empty($sequence)) {
155             $multianswer = new stdClass();
156             $multianswer->question = $question->id;
157             $multianswer->sequence = implode(',', $sequence);
158             if ($oldid = $DB->get_field('question_multianswer', 'id',
159                     array('question' => $question->id))) {
160                 $multianswer->id = $oldid;
161                 $DB->update_record('question_multianswer', $multianswer);
162             } else {
163                 $DB->insert_record('question_multianswer', $multianswer);
164             }
165         }
167         $this->save_hints($question, true);
168     }
170     public function save_question($authorizedquestion, $form) {
171         $question = qtype_multianswer_extract_question($form->questiontext);
172         if (isset($authorizedquestion->id)) {
173             $question->id = $authorizedquestion->id;
174         }
176         $question->category = $authorizedquestion->category;
177         $form->defaultmark = $question->defaultmark;
178         $form->questiontext = $question->questiontext;
179         $form->questiontextformat = 0;
180         $form->options = clone($question->options);
181         unset($question->options);
182         return parent::save_question($question, $form);
183     }
185     protected function make_hint($hint) {
186         return question_hint_with_parts::load_from_record($hint);
187     }
189     public function delete_question($questionid, $contextid) {
190         global $DB;
191         $DB->delete_records('question_multianswer', array('question' => $questionid));
193         parent::delete_question($questionid, $contextid);
194     }
196     protected function initialise_question_instance(question_definition $question, $questiondata) {
197         parent::initialise_question_instance($question, $questiondata);
199         $bits = preg_split('/\{#(\d+)\}/', $question->questiontext,
200                 null, PREG_SPLIT_DELIM_CAPTURE);
201         $question->textfragments[0] = array_shift($bits);
202         $i = 1;
203         while (!empty($bits)) {
204             $question->places[$i] = array_shift($bits);
205             $question->textfragments[$i] = array_shift($bits);
206             $i += 1;
207         }
208         foreach ($questiondata->options->questions as $key => $subqdata) {
209             $subqdata->contextid = $questiondata->contextid;
210             if ($subqdata->qtype == 'multichoice') {
211                 $answerregs = array();
212                 if ($subqdata->options->shuffleanswers == 1 &&  isset($questiondata->options->shuffleanswers)
213                     && $questiondata->options->shuffleanswers == 0 ) {
214                     $subqdata->options->shuffleanswers = 0;
215                 }
216             }
217             $question->subquestions[$key] = question_bank::make_question($subqdata);
218             $question->subquestions[$key]->maxmark = $subqdata->defaultmark;
219             if (isset($subqdata->options->layout)) {
220                 $question->subquestions[$key]->layout = $subqdata->options->layout;
221             }
222         }
223     }
225     public function get_random_guess_score($questiondata) {
226         $fractionsum = 0;
227         $fractionmax = 0;
228         foreach ($questiondata->options->questions as $key => $subqdata) {
229             $fractionmax += $subqdata->defaultmark;
230             $fractionsum += question_bank::get_qtype(
231                     $subqdata->qtype)->get_random_guess_score($subqdata);
232         }
233         return $fractionsum / $fractionmax;
234     }
236     public function move_files($questionid, $oldcontextid, $newcontextid) {
237         parent::move_files($questionid, $oldcontextid, $newcontextid);
238         $this->move_files_in_hints($questionid, $oldcontextid, $newcontextid);
239     }
241     protected function delete_files($questionid, $contextid) {
242         parent::delete_files($questionid, $contextid);
243         $this->delete_files_in_hints($questionid, $contextid);
244     }
248 // ANSWER_ALTERNATIVE regexes.
249 define('ANSWER_ALTERNATIVE_FRACTION_REGEX',
250        '=|%(-?[0-9]+)%');
251 // For the syntax '(?<!' see http://www.perl.com/doc/manual/html/pod/perlre.html#item_C.
252 define('ANSWER_ALTERNATIVE_ANSWER_REGEX',
253         '.+?(?<!\\\\|&|&amp;)(?=[~#}]|$)');
254 define('ANSWER_ALTERNATIVE_FEEDBACK_REGEX',
255         '.*?(?<!\\\\)(?=[~}]|$)');
256 define('ANSWER_ALTERNATIVE_REGEX',
257        '(' . ANSWER_ALTERNATIVE_FRACTION_REGEX .')?' .
258        '(' . ANSWER_ALTERNATIVE_ANSWER_REGEX . ')' .
259        '(#(' . ANSWER_ALTERNATIVE_FEEDBACK_REGEX .'))?');
261 // Parenthesis positions for ANSWER_ALTERNATIVE_REGEX.
262 define('ANSWER_ALTERNATIVE_REGEX_PERCENTILE_FRACTION', 2);
263 define('ANSWER_ALTERNATIVE_REGEX_FRACTION', 1);
264 define('ANSWER_ALTERNATIVE_REGEX_ANSWER', 3);
265 define('ANSWER_ALTERNATIVE_REGEX_FEEDBACK', 5);
267 // NUMBER_FORMATED_ALTERNATIVE_ANSWER_REGEX is used
268 // for identifying numerical answers in ANSWER_ALTERNATIVE_REGEX_ANSWER.
269 define('NUMBER_REGEX',
270         '-?(([0-9]+[.,]?[0-9]*|[.,][0-9]+)([eE][-+]?[0-9]+)?)');
271 define('NUMERICAL_ALTERNATIVE_REGEX',
272         '^(' . NUMBER_REGEX . ')(:' . NUMBER_REGEX . ')?$');
274 // Parenthesis positions for NUMERICAL_FORMATED_ALTERNATIVE_ANSWER_REGEX.
275 define('NUMERICAL_CORRECT_ANSWER', 1);
276 define('NUMERICAL_ABS_ERROR_MARGIN', 6);
278 // Remaining ANSWER regexes.
279 define('ANSWER_TYPE_DEF_REGEX',
280         '(NUMERICAL|NM)|(MULTICHOICE|MC)|(MULTICHOICE_V|MCV)|(MULTICHOICE_H|MCH)|' .
281         '(SHORTANSWER|SA|MW)|(SHORTANSWER_C|SAC|MWC)|' .
282         '(MULTICHOICE_S|MCS)|(MULTICHOICE_VS|MCVS)|(MULTICHOICE_HS|MCHS)|'.
283         '(MULTIRESPONSE|MR)|(MULTIRESPONSE_H|MRH)|(MULTIRESPONSE_S|MRS)|(MULTIRESPONSE_HS|MRHS)');
284 define('ANSWER_START_REGEX',
285        '\{([0-9]*):(' . ANSWER_TYPE_DEF_REGEX . '):');
287 define('ANSWER_REGEX',
288         ANSWER_START_REGEX
289         . '(' . ANSWER_ALTERNATIVE_REGEX
290         . '(~'
291         . ANSWER_ALTERNATIVE_REGEX
292         . ')*)\}');
294 // Parenthesis positions for singulars in ANSWER_REGEX.
295 define('ANSWER_REGEX_NORM', 1);
296 define('ANSWER_REGEX_ANSWER_TYPE_NUMERICAL', 3);
297 define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE', 4);
298 define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_REGULAR', 5);
299 define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_HORIZONTAL', 6);
300 define('ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER', 7);
301 define('ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER_C', 8);
302 define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_SHUFFLED', 9);
303 define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_REGULAR_SHUFFLED', 10);
304 define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_HORIZONTAL_SHUFFLED', 11);
305 define('ANSWER_REGEX_ANSWER_TYPE_MULTIRESPONSE', 12);
306 define('ANSWER_REGEX_ANSWER_TYPE_MULTIRESPONSE_HORIZONTAL', 13);
307 define('ANSWER_REGEX_ANSWER_TYPE_MULTIRESPONSE_SHUFFLED', 14);
308 define('ANSWER_REGEX_ANSWER_TYPE_MULTIRESPONSE_HORIZONTAL_SHUFFLED', 15);
309 define('ANSWER_REGEX_ALTERNATIVES', 16);
311 /**
312  * Initialise subquestion fields that are constant across all MULTICHOICE
313  * types.
314  *
315  * @param objet $wrapped  The subquestion to initialise
316  *
317  */
318 function qtype_multianswer_initialise_multichoice_subquestion($wrapped) {
319     $wrapped->qtype = 'multichoice';
320     $wrapped->single = 1;
321     $wrapped->answernumbering = 0;
322     $wrapped->correctfeedback['text'] = '';
323     $wrapped->correctfeedback['format'] = FORMAT_HTML;
324     $wrapped->correctfeedback['itemid'] = '';
325     $wrapped->partiallycorrectfeedback['text'] = '';
326     $wrapped->partiallycorrectfeedback['format'] = FORMAT_HTML;
327     $wrapped->partiallycorrectfeedback['itemid'] = '';
328     $wrapped->incorrectfeedback['text'] = '';
329     $wrapped->incorrectfeedback['format'] = FORMAT_HTML;
330     $wrapped->incorrectfeedback['itemid'] = '';
333 function qtype_multianswer_extract_question($text) {
334     // Variable $text is an array [text][format][itemid].
335     $question = new stdClass();
336     $question->qtype = 'multianswer';
337     $question->questiontext = $text;
338     $question->generalfeedback['text'] = '';
339     $question->generalfeedback['format'] = FORMAT_HTML;
340     $question->generalfeedback['itemid'] = '';
342     $question->options = new stdClass();
343     $question->options->questions = array();
344     $question->defaultmark = 0; // Will be increased for each answer norm.
346     for ($positionkey = 1;
347             preg_match('/'.ANSWER_REGEX.'/s', $question->questiontext['text'], $answerregs);
348             ++$positionkey) {
349         $wrapped = new stdClass();
350         $wrapped->generalfeedback['text'] = '';
351         $wrapped->generalfeedback['format'] = FORMAT_HTML;
352         $wrapped->generalfeedback['itemid'] = '';
353         if (isset($answerregs[ANSWER_REGEX_NORM]) && $answerregs[ANSWER_REGEX_NORM] !== '') {
354             $wrapped->defaultmark = $answerregs[ANSWER_REGEX_NORM];
355         } else {
356             $wrapped->defaultmark = '1';
357         }
358         if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_NUMERICAL])) {
359             $wrapped->qtype = 'numerical';
360             $wrapped->multiplier = array();
361             $wrapped->units      = array();
362             $wrapped->instructions['text'] = '';
363             $wrapped->instructions['format'] = FORMAT_HTML;
364             $wrapped->instructions['itemid'] = '';
365         } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER])) {
366             $wrapped->qtype = 'shortanswer';
367             $wrapped->usecase = 0;
368         } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER_C])) {
369             $wrapped->qtype = 'shortanswer';
370             $wrapped->usecase = 1;
371         } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE])) {
372             qtype_multianswer_initialise_multichoice_subquestion($wrapped);
373             $wrapped->shuffleanswers = 0;
374             $wrapped->layout = qtype_multichoice_base::LAYOUT_DROPDOWN;
375         } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_SHUFFLED])) {
376             qtype_multianswer_initialise_multichoice_subquestion($wrapped);
377             $wrapped->shuffleanswers = 1;
378             $wrapped->layout = qtype_multichoice_base::LAYOUT_DROPDOWN;
379         } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_REGULAR])) {
380             qtype_multianswer_initialise_multichoice_subquestion($wrapped);
381             $wrapped->shuffleanswers = 0;
382             $wrapped->layout = qtype_multichoice_base::LAYOUT_VERTICAL;
383         } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_REGULAR_SHUFFLED])) {
384             qtype_multianswer_initialise_multichoice_subquestion($wrapped);
385             $wrapped->shuffleanswers = 1;
386             $wrapped->layout = qtype_multichoice_base::LAYOUT_VERTICAL;
387         } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_HORIZONTAL])) {
388             qtype_multianswer_initialise_multichoice_subquestion($wrapped);
389             $wrapped->shuffleanswers = 0;
390             $wrapped->layout = qtype_multichoice_base::LAYOUT_HORIZONTAL;
391         } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_HORIZONTAL_SHUFFLED])) {
392             qtype_multianswer_initialise_multichoice_subquestion($wrapped);
393             $wrapped->shuffleanswers = 1;
394             $wrapped->layout = qtype_multichoice_base::LAYOUT_HORIZONTAL;
395         } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTIRESPONSE])) {
396             qtype_multianswer_initialise_multichoice_subquestion($wrapped);
397             $wrapped->single = 0;
398             $wrapped->shuffleanswers = 0;
399             $wrapped->layout = qtype_multichoice_base::LAYOUT_VERTICAL;
400         } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTIRESPONSE_HORIZONTAL])) {
401             qtype_multianswer_initialise_multichoice_subquestion($wrapped);
402             $wrapped->single = 0;
403             $wrapped->shuffleanswers = 0;
404             $wrapped->layout = qtype_multichoice_base::LAYOUT_HORIZONTAL;
405         } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTIRESPONSE_SHUFFLED])) {
406             qtype_multianswer_initialise_multichoice_subquestion($wrapped);
407             $wrapped->single = 0;
408             $wrapped->shuffleanswers = 1;
409             $wrapped->layout = qtype_multichoice_base::LAYOUT_VERTICAL;
410         } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTIRESPONSE_HORIZONTAL_SHUFFLED])) {
411             qtype_multianswer_initialise_multichoice_subquestion($wrapped);
412             $wrapped->single = 0;
413             $wrapped->shuffleanswers = 1;
414             $wrapped->layout = qtype_multichoice_base::LAYOUT_HORIZONTAL;
415         } else {
416             print_error('unknownquestiontype', 'question', '', $answerregs[2]);
417             return false;
418         }
420         // Each $wrapped simulates a $form that can be processed by the
421         // respective save_question and save_question_options methods of the
422         // wrapped questiontypes.
423         $wrapped->answer   = array();
424         $wrapped->fraction = array();
425         $wrapped->feedback = array();
426         $wrapped->questiontext['text'] = $answerregs[0];
427         $wrapped->questiontext['format'] = FORMAT_HTML;
428         $wrapped->questiontext['itemid'] = '';
429         $answerindex = 0;
431         $hasspecificfraction = false;
432         $remainingalts = $answerregs[ANSWER_REGEX_ALTERNATIVES];
433         while (preg_match('/~?'.ANSWER_ALTERNATIVE_REGEX.'/s', $remainingalts, $altregs)) {
434             if ('=' == $altregs[ANSWER_ALTERNATIVE_REGEX_FRACTION]) {
435                 $wrapped->fraction["{$answerindex}"] = '1';
436             } else if ($percentile = $altregs[ANSWER_ALTERNATIVE_REGEX_PERCENTILE_FRACTION]) {
437                 $wrapped->fraction["{$answerindex}"] = .01 * $percentile;
438                 $hasspecificfraction = true;
439             } else {
440                 $wrapped->fraction["{$answerindex}"] = '0';
441             }
442             if (isset($altregs[ANSWER_ALTERNATIVE_REGEX_FEEDBACK])) {
443                 $feedback = html_entity_decode(
444                         $altregs[ANSWER_ALTERNATIVE_REGEX_FEEDBACK], ENT_QUOTES, 'UTF-8');
445                 $feedback = str_replace('\}', '}', $feedback);
446                 $wrapped->feedback["{$answerindex}"]['text'] = str_replace('\#', '#', $feedback);
447                 $wrapped->feedback["{$answerindex}"]['format'] = FORMAT_HTML;
448                 $wrapped->feedback["{$answerindex}"]['itemid'] = '';
449             } else {
450                 $wrapped->feedback["{$answerindex}"]['text'] = '';
451                 $wrapped->feedback["{$answerindex}"]['format'] = FORMAT_HTML;
452                 $wrapped->feedback["{$answerindex}"]['itemid'] = '';
454             }
455             if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_NUMERICAL])
456                     && preg_match('~'.NUMERICAL_ALTERNATIVE_REGEX.'~s',
457                             $altregs[ANSWER_ALTERNATIVE_REGEX_ANSWER], $numregs)) {
458                 $wrapped->answer[] = $numregs[NUMERICAL_CORRECT_ANSWER];
459                 if (array_key_exists(NUMERICAL_ABS_ERROR_MARGIN, $numregs)) {
460                     $wrapped->tolerance["{$answerindex}"] =
461                     $numregs[NUMERICAL_ABS_ERROR_MARGIN];
462                 } else {
463                     $wrapped->tolerance["{$answerindex}"] = 0;
464                 }
465             } else { // Tolerance can stay undefined for non numerical questions.
466                 // Undo quoting done by the HTML editor.
467                 $answer = html_entity_decode(
468                         $altregs[ANSWER_ALTERNATIVE_REGEX_ANSWER], ENT_QUOTES, 'UTF-8');
469                 $answer = str_replace('\}', '}', $answer);
470                 $wrapped->answer["{$answerindex}"] = str_replace('\#', '#', $answer);
471                 if ($wrapped->qtype == 'multichoice') {
472                     $wrapped->answer["{$answerindex}"] = array(
473                             'text' => $wrapped->answer["{$answerindex}"],
474                             'format' => FORMAT_HTML,
475                             'itemid' => '');
476                 }
477             }
478             $tmp = explode($altregs[0], $remainingalts, 2);
479             $remainingalts = $tmp[1];
480             $answerindex++;
481         }
483         // Fix the score for multichoice_multi questions (as positive scores should add up to 1, not have a maximum of 1).
484         if (isset($wrapped->single) && $wrapped->single == 0) {
485             $total = 0;
486             foreach ($wrapped->fraction as $idx => $fraction) {
487                 if ($fraction > 0) {
488                     $total += $fraction;
489                 }
490             }
491             if ($total) {
492                 foreach ($wrapped->fraction as $idx => $fraction) {
493                     if ($fraction > 0) {
494                         $wrapped->fraction[$idx] = $fraction / $total;
495                     } else if (!$hasspecificfraction) {
496                         // If no specific fractions are given, set incorrect answers to each cancel out one correct answer.
497                         $wrapped->fraction[$idx] = -(1.0 / $total);
498                     }
499                 }
500             }
501         }
503         $question->defaultmark += $wrapped->defaultmark;
504         $question->options->questions[$positionkey] = clone($wrapped);
505         $question->questiontext['text'] = implode("{#$positionkey}",
506                     explode($answerregs[0], $question->questiontext['text'], 2));
507     }
508     return $question;