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