Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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/>.
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
 
27
defined('MOODLE_INTERNAL') || die();
28
 
29
require_once($CFG->dirroot . '/question/type/questiontypebase.php');
30
require_once($CFG->dirroot . '/question/type/multichoice/question.php');
31
require_once($CFG->dirroot . '/question/type/numerical/questiontype.php');
32
 
33
/**
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
38
 */
39
class qtype_multianswer extends question_type {
40
 
41
    /**
42
     * Generate a subquestion replacement question class.
43
     *
44
     * Due to a bug, subquestions can be lost (see MDL-54724). This class exists to take
45
     * the place of those lost questions so that the system can keep working and inform
46
     * the user of the corrupted data.
47
     *
48
     * @return question_automatically_gradable The replacement question class.
49
     */
50
    public static function deleted_subquestion_replacement(): question_automatically_gradable {
51
        return new class implements question_automatically_gradable {
52
            public $qtype;
53
 
54
            public function __construct() {
55
                $this->qtype = new class() {
56
                    public function name() {
57
                        return 'subquestion_replacement';
58
                    }
59
                };
60
            }
61
 
62
            public function is_gradable_response(array $response) {
63
                return false;
64
            }
65
 
66
            public function is_complete_response(array $response) {
67
                return false;
68
            }
69
 
70
            public function is_same_response(array $prevresponse, array $newresponse) {
71
                return false;
72
            }
73
 
74
            public function summarise_response(array $response) {
75
                return '';
76
            }
77
 
78
            public function un_summarise_response(string $summary) {
79
                return [];
80
            }
81
 
82
            public function classify_response(array $response) {
83
                return [];
84
            }
85
 
86
            public function get_validation_error(array $response) {
87
                return '';
88
            }
89
 
90
            public function grade_response(array $response) {
91
                return [];
92
            }
93
 
94
            public function get_hint($hintnumber, question_attempt $qa) {
95
                return;
96
            }
97
 
98
            public function get_right_answer_summary() {
99
                return null;
100
            }
101
        };
102
    }
103
 
104
    public function can_analyse_responses() {
105
        return false;
106
    }
107
 
108
    public function get_question_options($question) {
109
        global $DB;
110
 
111
        parent::get_question_options($question);
112
        // Get relevant data indexed by positionkey from the multianswers table.
1441 ariadna 113
        $sequence = $DB->get_field('question_multianswer', 'sequence', ['question' => $question->id]);
1 efrain 114
 
115
        if (empty($sequence)) {
116
            $question->options->questions = [];
117
            return true;
118
        }
119
 
120
        $wrappedquestions = $DB->get_records_list('question', 'id',
121
                explode(',', $sequence), 'id ASC');
122
 
123
        // We want an array with question ids as index and the positions as values.
124
        $sequence = array_flip(explode(',', $sequence));
125
        array_walk($sequence, function(&$val) {
126
            $val++;
127
        });
128
 
129
        // Due to a bug, questions can be lost (see MDL-54724). So we first fill the question
130
        // options with this dummy "replacement" type. These are overridden in the loop below
131
        // leaving behind only those questions which no longer exist. The renderer then looks
132
        // for this deleted type to display information to the user about the corrupted question
133
        // data.
134
        foreach ($sequence as $seq) {
135
            $question->options->questions[$seq] = (object)[
136
                'qtype' => 'subquestion_replacement',
137
                'defaultmark' => 1,
138
                'options' => (object)[
139
                    'answers' => []
140
                ]
141
            ];
142
        }
143
 
144
        foreach ($wrappedquestions as $wrapped) {
145
            question_bank::get_qtype($wrapped->qtype)->get_question_options($wrapped);
146
            // For wrapped questions the maxgrade is always equal to the defaultmark,
147
            // there is no entry in the question_instances table for them.
148
            $wrapped->category = $question->categoryobject->id;
149
            $question->options->questions[$sequence[$wrapped->id]] = $wrapped;
150
        }
151
        $question->hints = $DB->get_records('question_hints',
152
                array('questionid' => $question->id), 'id ASC');
153
 
154
        return true;
155
    }
156
 
157
    public function save_question_options($question) {
158
        global $DB;
159
        $result = new stdClass();
160
 
161
        // This function needs to be able to handle the case where the existing set of wrapped
162
        // questions does not match the new set of wrapped questions so that some need to be
163
        // created, some modified and some deleted.
1441 ariadna 164
        // Thanks to versioning, we no longer overwrite existing questions in the sequence
165
        // by re-using IDs, but instead create a new version of each wrapped question for
166
        // the new version of the parent.
1 efrain 167
 
168
        // First we get all the existing wrapped questions.
169
        $oldwrappedquestions = [];
170
        if (isset($question->oldparent)) {
171
            if ($oldwrappedids = $DB->get_field('question_multianswer', 'sequence',
172
                ['question' => $question->oldparent])) {
173
                $oldwrappedidsarray = explode(',', $oldwrappedids);
174
                $unorderedquestions = $DB->get_records_list('question', 'id', $oldwrappedidsarray);
175
 
176
                // Keep the order as given in the sequence field.
177
                foreach ($oldwrappedidsarray as $questionid) {
178
                    if (isset($unorderedquestions[$questionid])) {
179
                        $oldwrappedquestions[] = $unorderedquestions[$questionid];
180
                    }
181
                }
182
            }
183
        }
184
 
185
        $sequence = array();
186
        foreach ($question->options->questions as $wrapped) {
187
            if (!empty($wrapped)) {
1441 ariadna 188
                // If we still have some old wrapped question ids, reuse the next of them to save
189
                // the new version against its question bank entry.
1 efrain 190
                $wrapped->id = 0;
191
                if (is_array($oldwrappedquestions) &&
192
                        $oldwrappedquestion = array_shift($oldwrappedquestions)) {
193
                    $wrapped->oldid = $oldwrappedquestion->id;
194
                }
195
            }
196
            $wrapped->name = $question->name;
197
            $wrapped->parent = $question->id;
198
            $previousid = $wrapped->id;
199
            // Save_question strips this extra bit off the category again.
200
            $wrapped->category = $question->category . ',1';
201
            $wrapped = question_bank::get_qtype($wrapped->qtype)->save_question(
202
                    $wrapped, clone($wrapped));
203
            $sequence[] = $wrapped->id;
204
            if ($previousid != 0 && $previousid != $wrapped->id) {
205
                // For some reasons a new question has been created
206
                // so delete the old one.
207
                question_delete_question($previousid);
208
            }
209
        }
210
 
211
        // Delete redundant wrapped questions.
212
        if (is_array($oldwrappedquestions) && count($oldwrappedquestions)) {
213
            foreach ($oldwrappedquestions as $oldwrappedquestion) {
214
                question_delete_question($oldwrappedquestion->id);
215
            }
216
        }
217
 
218
        if (!empty($sequence)) {
219
            $multianswer = new stdClass();
220
            $multianswer->question = $question->id;
221
            $multianswer->sequence = implode(',', $sequence);
222
            if ($oldid = $DB->get_field('question_multianswer', 'id',
223
                    array('question' => $question->id))) {
224
                $multianswer->id = $oldid;
225
                $DB->update_record('question_multianswer', $multianswer);
226
            } else {
227
                $DB->insert_record('question_multianswer', $multianswer);
228
            }
229
        }
230
 
231
        $this->save_hints($question, true);
232
    }
233
 
234
    public function save_question($authorizedquestion, $form) {
235
        $question = qtype_multianswer_extract_question($form->questiontext);
236
        if (isset($authorizedquestion->id)) {
237
            $question->id = $authorizedquestion->id;
238
        }
239
 
240
        $question->category = $form->category;
241
        $form->defaultmark = $question->defaultmark;
242
        $form->questiontext = $question->questiontext;
243
        $form->questiontextformat = 0;
244
        $form->options = clone($question->options);
245
        unset($question->options);
246
        return parent::save_question($question, $form);
247
    }
248
 
249
    protected function make_hint($hint) {
250
        return question_hint_with_parts::load_from_record($hint);
251
    }
252
 
253
    public function delete_question($questionid, $contextid) {
254
        global $DB;
255
        $DB->delete_records('question_multianswer', array('question' => $questionid));
256
 
257
        parent::delete_question($questionid, $contextid);
258
    }
259
 
260
    protected function initialise_question_instance(question_definition $question, $questiondata) {
261
        parent::initialise_question_instance($question, $questiondata);
262
 
263
        $bits = preg_split('/\{#(\d+)\}/', $question->questiontext,
264
                -1, PREG_SPLIT_DELIM_CAPTURE);
265
        $question->textfragments[0] = array_shift($bits);
266
        $i = 1;
267
        while (!empty($bits)) {
268
            $question->places[$i] = array_shift($bits);
269
            $question->textfragments[$i] = array_shift($bits);
270
            $i += 1;
271
        }
272
        foreach ($questiondata->options->questions as $key => $subqdata) {
273
            if ($subqdata->qtype == 'subquestion_replacement') {
274
                continue;
275
            }
276
 
277
            $subqdata->contextid = $questiondata->contextid;
278
            if ($subqdata->qtype == 'multichoice') {
279
                $answerregs = array();
280
                if ($subqdata->options->shuffleanswers == 1 &&  isset($questiondata->options->shuffleanswers)
281
                    && $questiondata->options->shuffleanswers == 0 ) {
282
                    $subqdata->options->shuffleanswers = 0;
283
                }
284
            }
285
            $question->subquestions[$key] = question_bank::make_question($subqdata);
286
            $question->subquestions[$key]->defaultmark = $subqdata->defaultmark;
287
            if (isset($subqdata->options->layout)) {
288
                $question->subquestions[$key]->layout = $subqdata->options->layout;
289
            }
290
        }
291
    }
292
 
293
    public function get_random_guess_score($questiondata) {
294
        $fractionsum = 0;
295
        $fractionmax = 0;
296
        foreach ($questiondata->options->questions as $key => $subqdata) {
297
            if ($subqdata->qtype == 'subquestion_replacement') {
298
                continue;
299
            }
300
            $fractionmax += $subqdata->defaultmark;
301
            $fractionsum += question_bank::get_qtype(
302
                    $subqdata->qtype)->get_random_guess_score($subqdata);
303
        }
304
        if ($fractionmax > question_utils::MARK_TOLERANCE) {
305
            return $fractionsum / $fractionmax;
306
        } else {
307
            return null;
308
        }
309
    }
310
 
311
    public function move_files($questionid, $oldcontextid, $newcontextid) {
312
        parent::move_files($questionid, $oldcontextid, $newcontextid);
313
        $this->move_files_in_hints($questionid, $oldcontextid, $newcontextid);
314
    }
315
 
316
    protected function delete_files($questionid, $contextid) {
317
        parent::delete_files($questionid, $contextid);
318
        $this->delete_files_in_hints($questionid, $contextid);
319
    }
320
}
321
 
322
 
323
// ANSWER_ALTERNATIVE regexes.
324
define('ANSWER_ALTERNATIVE_FRACTION_REGEX',
325
       '=|%(-?[0-9]+(?:[.,][0-9]*)?)%');
326
// For the syntax '(?<!' see http://www.perl.com/doc/manual/html/pod/perlre.html#item_C.
327
define('ANSWER_ALTERNATIVE_ANSWER_REGEX',
328
        '.+?(?<!\\\\|&|&amp;)(?=[~#}]|$)');
329
define('ANSWER_ALTERNATIVE_FEEDBACK_REGEX',
330
        '.*?(?<!\\\\)(?=[~}]|$)');
331
define('ANSWER_ALTERNATIVE_REGEX',
332
       '(' . ANSWER_ALTERNATIVE_FRACTION_REGEX .')?' .
333
       '(' . ANSWER_ALTERNATIVE_ANSWER_REGEX . ')' .
334
       '(#(' . ANSWER_ALTERNATIVE_FEEDBACK_REGEX .'))?');
335
 
336
// Parenthesis positions for ANSWER_ALTERNATIVE_REGEX.
337
define('ANSWER_ALTERNATIVE_REGEX_PERCENTILE_FRACTION', 2);
338
define('ANSWER_ALTERNATIVE_REGEX_FRACTION', 1);
339
define('ANSWER_ALTERNATIVE_REGEX_ANSWER', 3);
340
define('ANSWER_ALTERNATIVE_REGEX_FEEDBACK', 5);
341
 
342
// NUMBER_FORMATED_ALTERNATIVE_ANSWER_REGEX is used
343
// for identifying numerical answers in ANSWER_ALTERNATIVE_REGEX_ANSWER.
344
define('NUMBER_REGEX',
345
        '-?(([0-9]+[.,]?[0-9]*|[.,][0-9]+)([eE][-+]?[0-9]+)?)');
346
define('NUMERICAL_ALTERNATIVE_REGEX',
347
        '^(' . NUMBER_REGEX . ')(:' . NUMBER_REGEX . ')?$');
348
 
349
// Parenthesis positions for NUMERICAL_FORMATED_ALTERNATIVE_ANSWER_REGEX.
350
define('NUMERICAL_CORRECT_ANSWER', 1);
351
define('NUMERICAL_ABS_ERROR_MARGIN', 6);
352
 
353
// Remaining ANSWER regexes.
354
define('ANSWER_TYPE_DEF_REGEX',
355
        '(NUMERICAL|NM)|(MULTICHOICE|MC)|(MULTICHOICE_V|MCV)|(MULTICHOICE_H|MCH)|' .
356
        '(SHORTANSWER|SA|MW)|(SHORTANSWER_C|SAC|MWC)|' .
357
        '(MULTICHOICE_S|MCS)|(MULTICHOICE_VS|MCVS)|(MULTICHOICE_HS|MCHS)|'.
358
        '(MULTIRESPONSE|MR)|(MULTIRESPONSE_H|MRH)|(MULTIRESPONSE_S|MRS)|(MULTIRESPONSE_HS|MRHS)');
359
define('ANSWER_START_REGEX',
360
       '\{([0-9]*):(' . ANSWER_TYPE_DEF_REGEX . '):');
361
 
362
define('ANSWER_REGEX',
363
        ANSWER_START_REGEX
364
        . '(' . ANSWER_ALTERNATIVE_REGEX
365
        . '(~'
366
        . ANSWER_ALTERNATIVE_REGEX
367
        . ')*)\}');
368
 
369
// Parenthesis positions for singulars in ANSWER_REGEX.
370
define('ANSWER_REGEX_NORM', 1);
371
define('ANSWER_REGEX_ANSWER_TYPE_NUMERICAL', 3);
372
define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE', 4);
373
define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_REGULAR', 5);
374
define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_HORIZONTAL', 6);
375
define('ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER', 7);
376
define('ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER_C', 8);
377
define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_SHUFFLED', 9);
378
define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_REGULAR_SHUFFLED', 10);
379
define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_HORIZONTAL_SHUFFLED', 11);
380
define('ANSWER_REGEX_ANSWER_TYPE_MULTIRESPONSE', 12);
381
define('ANSWER_REGEX_ANSWER_TYPE_MULTIRESPONSE_HORIZONTAL', 13);
382
define('ANSWER_REGEX_ANSWER_TYPE_MULTIRESPONSE_SHUFFLED', 14);
383
define('ANSWER_REGEX_ANSWER_TYPE_MULTIRESPONSE_HORIZONTAL_SHUFFLED', 15);
384
define('ANSWER_REGEX_ALTERNATIVES', 16);
385
 
386
/**
387
 * Initialise subquestion fields that are constant across all MULTICHOICE
388
 * types.
389
 *
390
 * @param objet $wrapped  The subquestion to initialise
391
 *
392
 */
393
function qtype_multianswer_initialise_multichoice_subquestion($wrapped) {
394
    $wrapped->qtype = 'multichoice';
395
    $wrapped->single = 1;
396
    $wrapped->answernumbering = 0;
397
    $wrapped->correctfeedback['text'] = '';
398
    $wrapped->correctfeedback['format'] = FORMAT_HTML;
399
    $wrapped->correctfeedback['itemid'] = '';
400
    $wrapped->partiallycorrectfeedback['text'] = '';
401
    $wrapped->partiallycorrectfeedback['format'] = FORMAT_HTML;
402
    $wrapped->partiallycorrectfeedback['itemid'] = '';
403
    $wrapped->incorrectfeedback['text'] = '';
404
    $wrapped->incorrectfeedback['format'] = FORMAT_HTML;
405
    $wrapped->incorrectfeedback['itemid'] = '';
406
}
407
 
408
function qtype_multianswer_extract_question($text) {
409
    // Variable $text is an array [text][format][itemid].
410
    $question = new stdClass();
411
    $question->qtype = 'multianswer';
412
    $question->questiontext = $text;
413
    $question->generalfeedback['text'] = '';
414
    $question->generalfeedback['format'] = FORMAT_HTML;
415
    $question->generalfeedback['itemid'] = '';
416
 
417
    $question->options = new stdClass();
418
    $question->options->questions = array();
419
    $question->defaultmark = 0; // Will be increased for each answer norm.
420
 
421
    for ($positionkey = 1;
422
            preg_match('/'.ANSWER_REGEX.'/s', $question->questiontext['text'], $answerregs);
423
            ++$positionkey) {
424
        $wrapped = new stdClass();
425
        $wrapped->generalfeedback['text'] = '';
426
        $wrapped->generalfeedback['format'] = FORMAT_HTML;
427
        $wrapped->generalfeedback['itemid'] = '';
428
        if (isset($answerregs[ANSWER_REGEX_NORM]) && $answerregs[ANSWER_REGEX_NORM] !== '') {
429
            $wrapped->defaultmark = $answerregs[ANSWER_REGEX_NORM];
430
        } else {
431
            $wrapped->defaultmark = '1';
432
        }
433
        if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_NUMERICAL])) {
434
            $wrapped->qtype = 'numerical';
435
            $wrapped->multiplier = array();
436
            $wrapped->units      = array();
437
            $wrapped->instructions['text'] = '';
438
            $wrapped->instructions['format'] = FORMAT_HTML;
439
            $wrapped->instructions['itemid'] = '';
440
        } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER])) {
441
            $wrapped->qtype = 'shortanswer';
442
            $wrapped->usecase = 0;
443
        } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER_C])) {
444
            $wrapped->qtype = 'shortanswer';
445
            $wrapped->usecase = 1;
446
        } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE])) {
447
            qtype_multianswer_initialise_multichoice_subquestion($wrapped);
448
            $wrapped->shuffleanswers = 0;
449
            $wrapped->layout = qtype_multichoice_base::LAYOUT_DROPDOWN;
450
        } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_SHUFFLED])) {
451
            qtype_multianswer_initialise_multichoice_subquestion($wrapped);
452
            $wrapped->shuffleanswers = 1;
453
            $wrapped->layout = qtype_multichoice_base::LAYOUT_DROPDOWN;
454
        } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_REGULAR])) {
455
            qtype_multianswer_initialise_multichoice_subquestion($wrapped);
456
            $wrapped->shuffleanswers = 0;
457
            $wrapped->layout = qtype_multichoice_base::LAYOUT_VERTICAL;
458
        } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_REGULAR_SHUFFLED])) {
459
            qtype_multianswer_initialise_multichoice_subquestion($wrapped);
460
            $wrapped->shuffleanswers = 1;
461
            $wrapped->layout = qtype_multichoice_base::LAYOUT_VERTICAL;
462
        } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_HORIZONTAL])) {
463
            qtype_multianswer_initialise_multichoice_subquestion($wrapped);
464
            $wrapped->shuffleanswers = 0;
465
            $wrapped->layout = qtype_multichoice_base::LAYOUT_HORIZONTAL;
466
        } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_HORIZONTAL_SHUFFLED])) {
467
            qtype_multianswer_initialise_multichoice_subquestion($wrapped);
468
            $wrapped->shuffleanswers = 1;
469
            $wrapped->layout = qtype_multichoice_base::LAYOUT_HORIZONTAL;
470
        } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTIRESPONSE])) {
471
            qtype_multianswer_initialise_multichoice_subquestion($wrapped);
472
            $wrapped->single = 0;
473
            $wrapped->shuffleanswers = 0;
474
            $wrapped->layout = qtype_multichoice_base::LAYOUT_VERTICAL;
475
        } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTIRESPONSE_HORIZONTAL])) {
476
            qtype_multianswer_initialise_multichoice_subquestion($wrapped);
477
            $wrapped->single = 0;
478
            $wrapped->shuffleanswers = 0;
479
            $wrapped->layout = qtype_multichoice_base::LAYOUT_HORIZONTAL;
480
        } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTIRESPONSE_SHUFFLED])) {
481
            qtype_multianswer_initialise_multichoice_subquestion($wrapped);
482
            $wrapped->single = 0;
483
            $wrapped->shuffleanswers = 1;
484
            $wrapped->layout = qtype_multichoice_base::LAYOUT_VERTICAL;
485
        } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTIRESPONSE_HORIZONTAL_SHUFFLED])) {
486
            qtype_multianswer_initialise_multichoice_subquestion($wrapped);
487
            $wrapped->single = 0;
488
            $wrapped->shuffleanswers = 1;
489
            $wrapped->layout = qtype_multichoice_base::LAYOUT_HORIZONTAL;
490
        } else {
491
            throw new \moodle_exception('unknownquestiontype', 'question', '', $answerregs[2]);
492
            return false;
493
        }
494
 
495
        // Each $wrapped simulates a $form that can be processed by the
496
        // respective save_question and save_question_options methods of the
497
        // wrapped questiontypes.
498
        $wrapped->answer   = array();
499
        $wrapped->fraction = array();
500
        $wrapped->feedback = array();
501
        $wrapped->questiontext['text'] = $answerregs[0];
502
        $wrapped->questiontext['format'] = FORMAT_HTML;
503
        $wrapped->questiontext['itemid'] = '';
504
        $answerindex = 0;
505
 
506
        $hasspecificfraction = false;
507
        $remainingalts = $answerregs[ANSWER_REGEX_ALTERNATIVES];
508
        while (preg_match('/~?'.ANSWER_ALTERNATIVE_REGEX.'/s', $remainingalts, $altregs)) {
509
            if ('=' == $altregs[ANSWER_ALTERNATIVE_REGEX_FRACTION]) {
510
                $wrapped->fraction["{$answerindex}"] = '1';
511
            } else if ($percentile = $altregs[ANSWER_ALTERNATIVE_REGEX_PERCENTILE_FRACTION]) {
512
                // Accept either decimal place character.
513
                $wrapped->fraction["{$answerindex}"] = .01 * str_replace(',', '.', $percentile);
514
                $hasspecificfraction = true;
515
            } else {
516
                $wrapped->fraction["{$answerindex}"] = '0';
517
            }
518
            if (isset($altregs[ANSWER_ALTERNATIVE_REGEX_FEEDBACK])) {
519
                $feedback = html_entity_decode(
520
                        $altregs[ANSWER_ALTERNATIVE_REGEX_FEEDBACK], ENT_QUOTES, 'UTF-8');
521
                $feedback = str_replace('\}', '}', $feedback);
522
                $wrapped->feedback["{$answerindex}"]['text'] = str_replace('\#', '#', $feedback);
523
                $wrapped->feedback["{$answerindex}"]['format'] = FORMAT_HTML;
524
                $wrapped->feedback["{$answerindex}"]['itemid'] = '';
525
            } else {
526
                $wrapped->feedback["{$answerindex}"]['text'] = '';
527
                $wrapped->feedback["{$answerindex}"]['format'] = FORMAT_HTML;
528
                $wrapped->feedback["{$answerindex}"]['itemid'] = '';
529
 
530
            }
531
            if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_NUMERICAL])
532
                    && preg_match('~'.NUMERICAL_ALTERNATIVE_REGEX.'~s',
533
                            $altregs[ANSWER_ALTERNATIVE_REGEX_ANSWER], $numregs)) {
534
                $wrapped->answer[] = $numregs[NUMERICAL_CORRECT_ANSWER];
535
                if (array_key_exists(NUMERICAL_ABS_ERROR_MARGIN, $numregs)) {
536
                    $wrapped->tolerance["{$answerindex}"] =
537
                    $numregs[NUMERICAL_ABS_ERROR_MARGIN];
538
                } else {
539
                    $wrapped->tolerance["{$answerindex}"] = 0;
540
                }
541
            } else { // Tolerance can stay undefined for non numerical questions.
542
                // Undo quoting done by the HTML editor.
543
                $answer = html_entity_decode(
544
                        $altregs[ANSWER_ALTERNATIVE_REGEX_ANSWER], ENT_QUOTES, 'UTF-8');
545
                $answer = str_replace('\}', '}', $answer);
546
                $wrapped->answer["{$answerindex}"] = str_replace('\#', '#', $answer);
547
                if ($wrapped->qtype == 'multichoice') {
548
                    $wrapped->answer["{$answerindex}"] = array(
549
                            'text' => $wrapped->answer["{$answerindex}"],
550
                            'format' => FORMAT_HTML,
551
                            'itemid' => '');
552
                }
553
            }
554
            $tmp = explode($altregs[0], $remainingalts, 2);
555
            $remainingalts = $tmp[1];
556
            $answerindex++;
557
        }
558
 
559
        // Fix the score for multichoice_multi questions (as positive scores should add up to 1, not have a maximum of 1).
560
        if (isset($wrapped->single) && $wrapped->single == 0) {
561
            $total = 0;
562
            foreach ($wrapped->fraction as $idx => $fraction) {
563
                if ($fraction > 0) {
564
                    $total += $fraction;
565
                }
566
            }
567
            if ($total) {
568
                foreach ($wrapped->fraction as $idx => $fraction) {
569
                    if ($fraction > 0) {
570
                        $wrapped->fraction[$idx] = $fraction / $total;
571
                    } else if (!$hasspecificfraction) {
572
                        // If no specific fractions are given, set incorrect answers to each cancel out one correct answer.
573
                        $wrapped->fraction[$idx] = -(1.0 / $total);
574
                    }
575
                }
576
            }
577
        }
578
 
579
        $question->defaultmark += $wrapped->defaultmark;
580
        $question->options->questions[$positionkey] = clone($wrapped);
581
        $question->questiontext['text'] = implode("{#$positionkey}",
582
                    explode($answerregs[0], $question->questiontext['text'], 2));
583
    }
584
    return $question;
585
}
586
 
587
/**
588
 * Validate a multianswer question.
589
 *
590
 * @param object $question  The multianswer question to validate as returned by qtype_multianswer_extract_question
591
 * @return array Array of error messages with questions field names as keys.
592
 */
593
function qtype_multianswer_validate_question(stdClass $question): array {
594
    $errors = array();
595
    if (!isset($question->options->questions)) {
596
        $errors['questiontext'] = get_string('questionsmissing', 'qtype_multianswer');
597
    } else {
598
        $subquestions = fullclone($question->options->questions);
599
        if (count($subquestions)) {
600
            $sub = 1;
601
            foreach ($subquestions as $subquestion) {
602
                $prefix = 'sub_'.$sub.'_';
603
                $answercount = 0;
604
                $maxgrade = false;
605
                $maxfraction = -1;
606
 
607
                foreach ($subquestion->answer as $key => $answer) {
608
                    if (is_array($answer)) {
609
                        $answer = $answer['text'];
610
                    }
611
                    $trimmedanswer = trim($answer);
612
                    if ($trimmedanswer !== '') {
613
                        $answercount++;
614
                        if ($subquestion->qtype == 'numerical' &&
615
                                !(qtype_numerical::is_valid_number($trimmedanswer) || $trimmedanswer == '*')) {
616
                            $errors[$prefix.'answer['.$key.']'] =
617
                                    get_string('answermustbenumberorstar', 'qtype_numerical');
618
                        }
619
                        if ($subquestion->fraction[$key] == 1) {
620
                            $maxgrade = true;
621
                        }
622
                        if ($subquestion->fraction[$key] > $maxfraction) {
623
                            $maxfraction = $subquestion->fraction[$key];
624
                        }
625
                        // For 'multiresponse' we are OK if there is at least one fraction > 0.
626
                        if ($subquestion->qtype == 'multichoice' && $subquestion->single == 0 &&
627
                            $subquestion->fraction[$key] > 0) {
628
                            $maxgrade = true;
629
                        }
630
                    }
631
                }
632
                if ($subquestion->qtype == 'multichoice' && $answercount < 2) {
633
                    $errors[$prefix.'answer[0]'] = get_string('notenoughanswers', 'qtype_multichoice', 2);
634
                } else if ($answercount == 0) {
635
                    $errors[$prefix.'answer[0]'] = get_string('notenoughanswers', 'question', 1);
636
                }
637
                if ($maxgrade == false) {
638
                    $errors[$prefix.'fraction[0]'] = get_string('fractionsnomax', 'question');
639
                }
640
                $sub++;
641
            }
642
        } else {
643
            $errors['questiontext'] = get_string('questionsmissing', 'qtype_multianswer');
644
        }
645
    }
646
    return $errors;
647
}