<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Question type class for the multi-answer question type.
*
* @package qtype
* @subpackage multianswer
* @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/question/type/questiontypebase.php');
require_once($CFG->dirroot . '/question/type/multichoice/question.php');
require_once($CFG->dirroot . '/question/type/numerical/questiontype.php');
/**
* The multi-answer question type class.
*
* @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_multianswer extends question_type {
> /**
public function can_analyse_responses() {
> * Generate a subquestion replacement question class.
return false;
> *
}
> * Due to a bug, subquestions can be lost (see MDL-54724). This class exists to take
> * the place of those lost questions so that the system can keep working and inform
public function get_question_options($question) {
> * the user of the corrupted data.
global $DB, $OUTPUT;
> *
> * @return question_automatically_gradable The replacement question class.
parent::get_question_options($question);
> */
// Get relevant data indexed by positionkey from the multianswers table.
> public static function deleted_subquestion_replacement(): question_automatically_gradable {
$sequence = $DB->get_field('question_multianswer', 'sequence',
> return new class implements question_automatically_gradable {
array('question' => $question->id), MUST_EXIST);
> public $qtype;
>
$wrappedquestions = $DB->get_records_list('question', 'id',
> public function __construct() {
explode(',', $sequence), 'id ASC');
> $this->qtype = new class() {
> public function name() {
// We want an array with question ids as index and the positions as values.
> return 'subquestion_replacement';
$sequence = array_flip(explode(',', $sequence));
> }
array_walk($sequence, function(&$val) {
> };
$val++;
> }
});
>
> public function is_gradable_response(array $response) {
// If a question is lost, the corresponding index is null
> return false;
// so this null convention is used to test $question->options->questions
> }
// before using the values.
>
// First all possible questions from sequence are nulled
> public function is_complete_response(array $response) {
// then filled with the data if available in $wrappedquestions.
> return false;
foreach ($sequence as $seq) {
> }
$question->options->questions[$seq] = '';
>
}
> public function is_same_response(array $prevresponse, array $newresponse) {
> return false;
foreach ($wrappedquestions as $wrapped) {
> }
question_bank::get_qtype($wrapped->qtype)->get_question_options($wrapped);
>
// For wrapped questions the maxgrade is always equal to the defaultmark,
> public function summarise_response(array $response) {
// there is no entry in the question_instances table for them.
> return '';
$wrapped->maxmark = $wrapped->defaultmark;
> }
$question->options->questions[$sequence[$wrapped->id]] = $wrapped;
>
}
> public function un_summarise_response(string $summary) {
$question->hints = $DB->get_records('question_hints',
> return [];
array('questionid' => $question->id), 'id ASC');
> }
>
return true;
> public function classify_response(array $response) {
}
> return [];
> }
public function save_question_options($question) {
>
global $DB;
> public function get_validation_error(array $response) {
$result = new stdClass();
> return '';
> }
// This function needs to be able to handle the case where the existing set of wrapped
>
// questions does not match the new set of wrapped questions so that some need to be
> public function grade_response(array $response) {
// created, some modified and some deleted.
> return [];
// Unfortunately the code currently simply overwrites existing ones in sequence. This
> }
// will make re-marking after a re-ordering of wrapped questions impossible and
>
// will also create difficulties if questiontype specific tables reference the id.
> public function get_hint($hintnumber, question_attempt $qa) {
> return;
// First we get all the existing wrapped questions.
> }
$oldwrappedquestions = [];
>
if ($oldwrappedids = $DB->get_field('question_multianswer', 'sequence',
> public function get_right_answer_summary() {
array('question' => $question->id))) {
> return null;
$oldwrappedidsarray = explode(',', $oldwrappedids);
> }
$unorderedquestions = $DB->get_records_list('question', 'id', $oldwrappedidsarray);
> };
> }
// Keep the order as given in the sequence field.
>
< global $DB, $OUTPUT;
> global $DB;
if (isset($unorderedquestions[$questionid])) {
> if (empty($sequence)) {
$oldwrappedquestions[] = $unorderedquestions[$questionid];
> $question->options->questions = [];
}
> return true;
}
> }
}
>
< // If a question is lost, the corresponding index is null
< // so this null convention is used to test $question->options->questions
< // before using the values.
< // First all possible questions from sequence are nulled
< // then filled with the data if available in $wrappedquestions.
> // Due to a bug, questions can be lost (see MDL-54724). So we first fill the question
> // options with this dummy "replacement" type. These are overridden in the loop below
> // leaving behind only those questions which no longer exist. The renderer then looks
> // for this deleted type to display information to the user about the corrupted question
> // data.
< $question->options->questions[$seq] = '';
> $question->options->questions[$seq] = (object)[
> 'qtype' => 'subquestion_replacement',
> 'defaultmark' => 1,
> 'options' => (object)[
> 'answers' => []
> ]
> ];
< $wrapped->maxmark = $wrapped->defaultmark;
> $wrapped->category = $question->categoryobject->id;
$oldwrappedquestion = array_shift($oldwrappedquestions)) {
> if (isset($question->oldparent)) {
< array('question' => $question->id))) {
> ['question' => $question->oldparent])) {
if ($oldwrappedquestion->qtype != $wrapped->qtype) {
> }
<
> $wrapped->id = 0;
< $wrapped->id = $oldwrappedquestion->id;
> $wrapped->oldid = $oldwrappedquestion->id;
$DB->delete_records('qtype_multichoice_options',
array('questionid' => $oldwrappedquestion->id));
break;
case 'shortanswer':
$DB->delete_records('qtype_shortanswer_options',
array('questionid' => $oldwrappedquestion->id));
break;
case 'numerical':
$DB->delete_records('question_numerical',
array('question' => $oldwrappedquestion->id));
break;
default:
throw new moodle_exception('qtypenotrecognized',
'qtype_multianswer', '', $oldwrappedquestion->qtype);
< $wrapped->id = 0;
}
}
< } else {
< $wrapped->id = 0;
}
}
$wrapped->name = $question->name;
$wrapped->parent = $question->id;
$previousid = $wrapped->id;
// Save_question strips this extra bit off the category again.
$wrapped->category = $question->category . ',1';
$wrapped = question_bank::get_qtype($wrapped->qtype)->save_question(
$wrapped, clone($wrapped));
$sequence[] = $wrapped->id;
if ($previousid != 0 && $previousid != $wrapped->id) {
// For some reasons a new question has been created
// so delete the old one.
question_delete_question($previousid);
}
}
// Delete redundant wrapped questions.
if (is_array($oldwrappedquestions) && count($oldwrappedquestions)) {
foreach ($oldwrappedquestions as $oldwrappedquestion) {
question_delete_question($oldwrappedquestion->id);
}
}
if (!empty($sequence)) {
$multianswer = new stdClass();
$multianswer->question = $question->id;
$multianswer->sequence = implode(',', $sequence);
if ($oldid = $DB->get_field('question_multianswer', 'id',
array('question' => $question->id))) {
$multianswer->id = $oldid;
$DB->update_record('question_multianswer', $multianswer);
} else {
$DB->insert_record('question_multianswer', $multianswer);
}
}
$this->save_hints($question, true);
}
public function save_question($authorizedquestion, $form) {
$question = qtype_multianswer_extract_question($form->questiontext);
if (isset($authorizedquestion->id)) {
$question->id = $authorizedquestion->id;
}
< $question->category = $authorizedquestion->category;
> $question->category = $form->category;
$form->defaultmark = $question->defaultmark;
$form->questiontext = $question->questiontext;
$form->questiontextformat = 0;
$form->options = clone($question->options);
unset($question->options);
return parent::save_question($question, $form);
}
protected function make_hint($hint) {
return question_hint_with_parts::load_from_record($hint);
}
public function delete_question($questionid, $contextid) {
global $DB;
$DB->delete_records('question_multianswer', array('question' => $questionid));
parent::delete_question($questionid, $contextid);
}
protected function initialise_question_instance(question_definition $question, $questiondata) {
parent::initialise_question_instance($question, $questiondata);
$bits = preg_split('/\{#(\d+)\}/', $question->questiontext,
< null, PREG_SPLIT_DELIM_CAPTURE);
> -1, PREG_SPLIT_DELIM_CAPTURE);
$question->textfragments[0] = array_shift($bits);
$i = 1;
while (!empty($bits)) {
$question->places[$i] = array_shift($bits);
$question->textfragments[$i] = array_shift($bits);
$i += 1;
}
foreach ($questiondata->options->questions as $key => $subqdata) {
> if ($subqdata->qtype == 'subquestion_replacement') {
$subqdata->contextid = $questiondata->contextid;
> continue;
if ($subqdata->qtype == 'multichoice') {
> }
$answerregs = array();
>
if ($subqdata->options->shuffleanswers == 1 && isset($questiondata->options->shuffleanswers)
&& $questiondata->options->shuffleanswers == 0 ) {
$subqdata->options->shuffleanswers = 0;
}
}
$question->subquestions[$key] = question_bank::make_question($subqdata);
< $question->subquestions[$key]->maxmark = $subqdata->defaultmark;
> $question->subquestions[$key]->defaultmark = $subqdata->defaultmark;
if (isset($subqdata->options->layout)) {
$question->subquestions[$key]->layout = $subqdata->options->layout;
}
}
}
public function get_random_guess_score($questiondata) {
$fractionsum = 0;
$fractionmax = 0;
foreach ($questiondata->options->questions as $key => $subqdata) {
> if ($subqdata->qtype == 'subquestion_replacement') {
$fractionmax += $subqdata->defaultmark;
> continue;
$fractionsum += question_bank::get_qtype(
> }
$subqdata->qtype)->get_random_guess_score($subqdata);
}
> if ($fractionmax > question_utils::MARK_TOLERANCE) {
return $fractionsum / $fractionmax;
> } else {
}
> return null;
> }
public function move_files($questionid, $oldcontextid, $newcontextid) {
parent::move_files($questionid, $oldcontextid, $newcontextid);
$this->move_files_in_hints($questionid, $oldcontextid, $newcontextid);
}
protected function delete_files($questionid, $contextid) {
parent::delete_files($questionid, $contextid);
$this->delete_files_in_hints($questionid, $contextid);
}
}
// ANSWER_ALTERNATIVE regexes.
define('ANSWER_ALTERNATIVE_FRACTION_REGEX',
'=|%(-?[0-9]+(?:[.,][0-9]*)?)%');
// For the syntax '(?<!' see http://www.perl.com/doc/manual/html/pod/perlre.html#item_C.
define('ANSWER_ALTERNATIVE_ANSWER_REGEX',
'.+?(?<!\\\\|&|&)(?=[~#}]|$)');
define('ANSWER_ALTERNATIVE_FEEDBACK_REGEX',
'.*?(?<!\\\\)(?=[~}]|$)');
define('ANSWER_ALTERNATIVE_REGEX',
'(' . ANSWER_ALTERNATIVE_FRACTION_REGEX .')?' .
'(' . ANSWER_ALTERNATIVE_ANSWER_REGEX . ')' .
'(#(' . ANSWER_ALTERNATIVE_FEEDBACK_REGEX .'))?');
// Parenthesis positions for ANSWER_ALTERNATIVE_REGEX.
define('ANSWER_ALTERNATIVE_REGEX_PERCENTILE_FRACTION', 2);
define('ANSWER_ALTERNATIVE_REGEX_FRACTION', 1);
define('ANSWER_ALTERNATIVE_REGEX_ANSWER', 3);
define('ANSWER_ALTERNATIVE_REGEX_FEEDBACK', 5);
// NUMBER_FORMATED_ALTERNATIVE_ANSWER_REGEX is used
// for identifying numerical answers in ANSWER_ALTERNATIVE_REGEX_ANSWER.
define('NUMBER_REGEX',
'-?(([0-9]+[.,]?[0-9]*|[.,][0-9]+)([eE][-+]?[0-9]+)?)');
define('NUMERICAL_ALTERNATIVE_REGEX',
'^(' . NUMBER_REGEX . ')(:' . NUMBER_REGEX . ')?$');
// Parenthesis positions for NUMERICAL_FORMATED_ALTERNATIVE_ANSWER_REGEX.
define('NUMERICAL_CORRECT_ANSWER', 1);
define('NUMERICAL_ABS_ERROR_MARGIN', 6);
// Remaining ANSWER regexes.
define('ANSWER_TYPE_DEF_REGEX',
'(NUMERICAL|NM)|(MULTICHOICE|MC)|(MULTICHOICE_V|MCV)|(MULTICHOICE_H|MCH)|' .
'(SHORTANSWER|SA|MW)|(SHORTANSWER_C|SAC|MWC)|' .
'(MULTICHOICE_S|MCS)|(MULTICHOICE_VS|MCVS)|(MULTICHOICE_HS|MCHS)|'.
'(MULTIRESPONSE|MR)|(MULTIRESPONSE_H|MRH)|(MULTIRESPONSE_S|MRS)|(MULTIRESPONSE_HS|MRHS)');
define('ANSWER_START_REGEX',
'\{([0-9]*):(' . ANSWER_TYPE_DEF_REGEX . '):');
define('ANSWER_REGEX',
ANSWER_START_REGEX
. '(' . ANSWER_ALTERNATIVE_REGEX
. '(~'
. ANSWER_ALTERNATIVE_REGEX
. ')*)\}');
// Parenthesis positions for singulars in ANSWER_REGEX.
define('ANSWER_REGEX_NORM', 1);
define('ANSWER_REGEX_ANSWER_TYPE_NUMERICAL', 3);
define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE', 4);
define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_REGULAR', 5);
define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_HORIZONTAL', 6);
define('ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER', 7);
define('ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER_C', 8);
define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_SHUFFLED', 9);
define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_REGULAR_SHUFFLED', 10);
define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_HORIZONTAL_SHUFFLED', 11);
define('ANSWER_REGEX_ANSWER_TYPE_MULTIRESPONSE', 12);
define('ANSWER_REGEX_ANSWER_TYPE_MULTIRESPONSE_HORIZONTAL', 13);
define('ANSWER_REGEX_ANSWER_TYPE_MULTIRESPONSE_SHUFFLED', 14);
define('ANSWER_REGEX_ANSWER_TYPE_MULTIRESPONSE_HORIZONTAL_SHUFFLED', 15);
define('ANSWER_REGEX_ALTERNATIVES', 16);
/**
* Initialise subquestion fields that are constant across all MULTICHOICE
* types.
*
* @param objet $wrapped The subquestion to initialise
*
*/
function qtype_multianswer_initialise_multichoice_subquestion($wrapped) {
$wrapped->qtype = 'multichoice';
$wrapped->single = 1;
$wrapped->answernumbering = 0;
$wrapped->correctfeedback['text'] = '';
$wrapped->correctfeedback['format'] = FORMAT_HTML;
$wrapped->correctfeedback['itemid'] = '';
$wrapped->partiallycorrectfeedback['text'] = '';
$wrapped->partiallycorrectfeedback['format'] = FORMAT_HTML;
$wrapped->partiallycorrectfeedback['itemid'] = '';
$wrapped->incorrectfeedback['text'] = '';
$wrapped->incorrectfeedback['format'] = FORMAT_HTML;
$wrapped->incorrectfeedback['itemid'] = '';
}
function qtype_multianswer_extract_question($text) {
// Variable $text is an array [text][format][itemid].
$question = new stdClass();
$question->qtype = 'multianswer';
$question->questiontext = $text;
$question->generalfeedback['text'] = '';
$question->generalfeedback['format'] = FORMAT_HTML;
$question->generalfeedback['itemid'] = '';
$question->options = new stdClass();
$question->options->questions = array();
$question->defaultmark = 0; // Will be increased for each answer norm.
for ($positionkey = 1;
preg_match('/'.ANSWER_REGEX.'/s', $question->questiontext['text'], $answerregs);
++$positionkey) {
$wrapped = new stdClass();
$wrapped->generalfeedback['text'] = '';
$wrapped->generalfeedback['format'] = FORMAT_HTML;
$wrapped->generalfeedback['itemid'] = '';
if (isset($answerregs[ANSWER_REGEX_NORM]) && $answerregs[ANSWER_REGEX_NORM] !== '') {
$wrapped->defaultmark = $answerregs[ANSWER_REGEX_NORM];
} else {
$wrapped->defaultmark = '1';
}
if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_NUMERICAL])) {
$wrapped->qtype = 'numerical';
$wrapped->multiplier = array();
$wrapped->units = array();
$wrapped->instructions['text'] = '';
$wrapped->instructions['format'] = FORMAT_HTML;
$wrapped->instructions['itemid'] = '';
} else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER])) {
$wrapped->qtype = 'shortanswer';
$wrapped->usecase = 0;
} else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER_C])) {
$wrapped->qtype = 'shortanswer';
$wrapped->usecase = 1;
} else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE])) {
qtype_multianswer_initialise_multichoice_subquestion($wrapped);
$wrapped->shuffleanswers = 0;
$wrapped->layout = qtype_multichoice_base::LAYOUT_DROPDOWN;
} else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_SHUFFLED])) {
qtype_multianswer_initialise_multichoice_subquestion($wrapped);
$wrapped->shuffleanswers = 1;
$wrapped->layout = qtype_multichoice_base::LAYOUT_DROPDOWN;
} else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_REGULAR])) {
qtype_multianswer_initialise_multichoice_subquestion($wrapped);
$wrapped->shuffleanswers = 0;
$wrapped->layout = qtype_multichoice_base::LAYOUT_VERTICAL;
} else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_REGULAR_SHUFFLED])) {
qtype_multianswer_initialise_multichoice_subquestion($wrapped);
$wrapped->shuffleanswers = 1;
$wrapped->layout = qtype_multichoice_base::LAYOUT_VERTICAL;
} else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_HORIZONTAL])) {
qtype_multianswer_initialise_multichoice_subquestion($wrapped);
$wrapped->shuffleanswers = 0;
$wrapped->layout = qtype_multichoice_base::LAYOUT_HORIZONTAL;
} else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_HORIZONTAL_SHUFFLED])) {
qtype_multianswer_initialise_multichoice_subquestion($wrapped);
$wrapped->shuffleanswers = 1;
$wrapped->layout = qtype_multichoice_base::LAYOUT_HORIZONTAL;
} else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTIRESPONSE])) {
qtype_multianswer_initialise_multichoice_subquestion($wrapped);
$wrapped->single = 0;
$wrapped->shuffleanswers = 0;
$wrapped->layout = qtype_multichoice_base::LAYOUT_VERTICAL;
} else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTIRESPONSE_HORIZONTAL])) {
qtype_multianswer_initialise_multichoice_subquestion($wrapped);
$wrapped->single = 0;
$wrapped->shuffleanswers = 0;
$wrapped->layout = qtype_multichoice_base::LAYOUT_HORIZONTAL;
} else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTIRESPONSE_SHUFFLED])) {
qtype_multianswer_initialise_multichoice_subquestion($wrapped);
$wrapped->single = 0;
$wrapped->shuffleanswers = 1;
$wrapped->layout = qtype_multichoice_base::LAYOUT_VERTICAL;
} else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTIRESPONSE_HORIZONTAL_SHUFFLED])) {
qtype_multianswer_initialise_multichoice_subquestion($wrapped);
$wrapped->single = 0;
$wrapped->shuffleanswers = 1;
$wrapped->layout = qtype_multichoice_base::LAYOUT_HORIZONTAL;
} else {
< print_error('unknownquestiontype', 'question', '', $answerregs[2]);
> throw new \moodle_exception('unknownquestiontype', 'question', '', $answerregs[2]);
return false;
}
// Each $wrapped simulates a $form that can be processed by the
// respective save_question and save_question_options methods of the
// wrapped questiontypes.
$wrapped->answer = array();
$wrapped->fraction = array();
$wrapped->feedback = array();
$wrapped->questiontext['text'] = $answerregs[0];
$wrapped->questiontext['format'] = FORMAT_HTML;
$wrapped->questiontext['itemid'] = '';
$answerindex = 0;
$hasspecificfraction = false;
$remainingalts = $answerregs[ANSWER_REGEX_ALTERNATIVES];
while (preg_match('/~?'.ANSWER_ALTERNATIVE_REGEX.'/s', $remainingalts, $altregs)) {
if ('=' == $altregs[ANSWER_ALTERNATIVE_REGEX_FRACTION]) {
$wrapped->fraction["{$answerindex}"] = '1';
} else if ($percentile = $altregs[ANSWER_ALTERNATIVE_REGEX_PERCENTILE_FRACTION]) {
// Accept either decimal place character.
$wrapped->fraction["{$answerindex}"] = .01 * str_replace(',', '.', $percentile);
$hasspecificfraction = true;
} else {
$wrapped->fraction["{$answerindex}"] = '0';
}
if (isset($altregs[ANSWER_ALTERNATIVE_REGEX_FEEDBACK])) {
$feedback = html_entity_decode(
$altregs[ANSWER_ALTERNATIVE_REGEX_FEEDBACK], ENT_QUOTES, 'UTF-8');
$feedback = str_replace('\}', '}', $feedback);
$wrapped->feedback["{$answerindex}"]['text'] = str_replace('\#', '#', $feedback);
$wrapped->feedback["{$answerindex}"]['format'] = FORMAT_HTML;
$wrapped->feedback["{$answerindex}"]['itemid'] = '';
} else {
$wrapped->feedback["{$answerindex}"]['text'] = '';
$wrapped->feedback["{$answerindex}"]['format'] = FORMAT_HTML;
$wrapped->feedback["{$answerindex}"]['itemid'] = '';
}
if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_NUMERICAL])
&& preg_match('~'.NUMERICAL_ALTERNATIVE_REGEX.'~s',
$altregs[ANSWER_ALTERNATIVE_REGEX_ANSWER], $numregs)) {
$wrapped->answer[] = $numregs[NUMERICAL_CORRECT_ANSWER];
if (array_key_exists(NUMERICAL_ABS_ERROR_MARGIN, $numregs)) {
$wrapped->tolerance["{$answerindex}"] =
$numregs[NUMERICAL_ABS_ERROR_MARGIN];
} else {
$wrapped->tolerance["{$answerindex}"] = 0;
}
} else { // Tolerance can stay undefined for non numerical questions.
// Undo quoting done by the HTML editor.
$answer = html_entity_decode(
$altregs[ANSWER_ALTERNATIVE_REGEX_ANSWER], ENT_QUOTES, 'UTF-8');
$answer = str_replace('\}', '}', $answer);
$wrapped->answer["{$answerindex}"] = str_replace('\#', '#', $answer);
if ($wrapped->qtype == 'multichoice') {
$wrapped->answer["{$answerindex}"] = array(
'text' => $wrapped->answer["{$answerindex}"],
'format' => FORMAT_HTML,
'itemid' => '');
}
}
$tmp = explode($altregs[0], $remainingalts, 2);
$remainingalts = $tmp[1];
$answerindex++;
}
// Fix the score for multichoice_multi questions (as positive scores should add up to 1, not have a maximum of 1).
if (isset($wrapped->single) && $wrapped->single == 0) {
$total = 0;
foreach ($wrapped->fraction as $idx => $fraction) {
if ($fraction > 0) {
$total += $fraction;
}
}
if ($total) {
foreach ($wrapped->fraction as $idx => $fraction) {
if ($fraction > 0) {
$wrapped->fraction[$idx] = $fraction / $total;
} else if (!$hasspecificfraction) {
// If no specific fractions are given, set incorrect answers to each cancel out one correct answer.
$wrapped->fraction[$idx] = -(1.0 / $total);
}
}
}
}
$question->defaultmark += $wrapped->defaultmark;
$question->options->questions[$positionkey] = clone($wrapped);
$question->questiontext['text'] = implode("{#$positionkey}",
explode($answerregs[0], $question->questiontext['text'], 2));
}
return $question;
}
/**
* Validate a multianswer question.
*
* @param object $question The multianswer question to validate as returned by qtype_multianswer_extract_question
* @return array Array of error messages with questions field names as keys.
*/
function qtype_multianswer_validate_question(stdClass $question) : array {
$errors = array();
if (!isset($question->options->questions)) {
$errors['questiontext'] = get_string('questionsmissing', 'qtype_multianswer');
} else {
$subquestions = fullclone($question->options->questions);
if (count($subquestions)) {
$sub = 1;
foreach ($subquestions as $subquestion) {
$prefix = 'sub_'.$sub.'_';
$answercount = 0;
$maxgrade = false;
$maxfraction = -1;
foreach ($subquestion->answer as $key => $answer) {
if (is_array($answer)) {
$answer = $answer['text'];
}
$trimmedanswer = trim($answer);
if ($trimmedanswer !== '') {
$answercount++;
if ($subquestion->qtype == 'numerical' &&
!(qtype_numerical::is_valid_number($trimmedanswer) || $trimmedanswer == '*')) {
$errors[$prefix.'answer['.$key.']'] =
get_string('answermustbenumberorstar', 'qtype_numerical');
}
if ($subquestion->fraction[$key] == 1) {
$maxgrade = true;
}
if ($subquestion->fraction[$key] > $maxfraction) {
$maxfraction = $subquestion->fraction[$key];
}
// For 'multiresponse' we are OK if there is at least one fraction > 0.
if ($subquestion->qtype == 'multichoice' && $subquestion->single == 0 &&
$subquestion->fraction[$key] > 0) {
$maxgrade = true;
}
}
}
if ($subquestion->qtype == 'multichoice' && $answercount < 2) {
$errors[$prefix.'answer[0]'] = get_string('notenoughanswers', 'qtype_multichoice', 2);
} else if ($answercount == 0) {
$errors[$prefix.'answer[0]'] = get_string('notenoughanswers', 'question', 1);
}
if ($maxgrade == false) {
$errors[$prefix.'fraction[0]'] = get_string('fractionsnomax', 'question');
}
$sub++;
}
} else {
$errors['questiontext'] = get_string('questionsmissing', 'qtype_multianswer');
}
}
return $errors;
}