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