Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.
<?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' => [] > ] > ];
if (is_array($oldwrappedquestions) && $oldwrappedquestion = array_shift($oldwrappedquestions)) { $wrapped->id = $oldwrappedquestion->id; if ($oldwrappedquestion->qtype != $wrapped->qtype) { switch ($oldwrappedquestion->qtype) { case 'multichoice': $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; $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); $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; 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) { $fractionmax += $subqdata->defaultmark; $fractionsum += question_bank::get_qtype( $subqdata->qtype)->get_random_guess_score($subqdata); } return $fractionsum / $fractionmax; } 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', '.+?(?<!\\\\|&|&amp;)(?=[~#}]|$)'); 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]); 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; }