[ Index ] |
PHP Cross Reference of Unnamed Project |
[Summary view] [Print] [Text view]
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 32 /** 33 * The multi-answer question type class. 34 * 35 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} 36 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 37 */ 38 class qtype_multianswer extends question_type { 39 40 public function can_analyse_responses() { 41 return false; 42 } 43 44 public function get_question_options($question) { 45 global $DB, $OUTPUT; 46 47 // Get relevant data indexed by positionkey from the multianswers table. 48 $sequence = $DB->get_field('question_multianswer', 'sequence', 49 array('question' => $question->id), '*', MUST_EXIST); 50 51 $wrappedquestions = $DB->get_records_list('question', 'id', 52 explode(',', $sequence), 'id ASC'); 53 54 // We want an array with question ids as index and the positions as values. 55 $sequence = array_flip(explode(',', $sequence)); 56 array_walk($sequence, create_function('&$val', '$val++;')); 57 58 // If a question is lost, the corresponding index is null 59 // so this null convention is used to test $question->options->questions 60 // before using the values. 61 // First all possible questions from sequence are nulled 62 // then filled with the data if available in $wrappedquestions. 63 foreach ($sequence as $seq) { 64 $question->options->questions[$seq] = ''; 65 } 66 67 foreach ($wrappedquestions as $wrapped) { 68 question_bank::get_qtype($wrapped->qtype)->get_question_options($wrapped); 69 // For wrapped questions the maxgrade is always equal to the defaultmark, 70 // there is no entry in the question_instances table for them. 71 $wrapped->maxmark = $wrapped->defaultmark; 72 $question->options->questions[$sequence[$wrapped->id]] = $wrapped; 73 } 74 $question->hints = $DB->get_records('question_hints', 75 array('questionid' => $question->id), 'id ASC'); 76 77 return true; 78 } 79 80 public function save_question_options($question) { 81 global $DB; 82 $result = new stdClass(); 83 84 // This function needs to be able to handle the case where the existing set of wrapped 85 // questions does not match the new set of wrapped questions so that some need to be 86 // created, some modified and some deleted. 87 // Unfortunately the code currently simply overwrites existing ones in sequence. This 88 // will make re-marking after a re-ordering of wrapped questions impossible and 89 // will also create difficulties if questiontype specific tables reference the id. 90 91 // First we get all the existing wrapped questions. 92 if (!$oldwrappedids = $DB->get_field('question_multianswer', 'sequence', 93 array('question' => $question->id))) { 94 $oldwrappedquestions = array(); 95 } else { 96 $oldwrappedquestions = $DB->get_records_list('question', 'id', 97 explode(',', $oldwrappedids), 'id ASC'); 98 } 99 100 $sequence = array(); 101 foreach ($question->options->questions as $wrapped) { 102 if (!empty($wrapped)) { 103 // If we still have some old wrapped question ids, reuse the next of them. 104 105 if (is_array($oldwrappedquestions) && 106 $oldwrappedquestion = array_shift($oldwrappedquestions)) { 107 $wrapped->id = $oldwrappedquestion->id; 108 if ($oldwrappedquestion->qtype != $wrapped->qtype) { 109 switch ($oldwrappedquestion->qtype) { 110 case 'multichoice': 111 $DB->delete_records('qtype_multichoice_options', 112 array('questionid' => $oldwrappedquestion->id)); 113 break; 114 case 'shortanswer': 115 $DB->delete_records('qtype_shortanswer_options', 116 array('questionid' => $oldwrappedquestion->id)); 117 break; 118 case 'numerical': 119 $DB->delete_records('question_numerical', 120 array('question' => $oldwrappedquestion->id)); 121 break; 122 default: 123 throw new moodle_exception('qtypenotrecognized', 124 'qtype_multianswer', '', $oldwrappedquestion->qtype); 125 $wrapped->id = 0; 126 } 127 } 128 } else { 129 $wrapped->id = 0; 130 } 131 } 132 $wrapped->name = $question->name; 133 $wrapped->parent = $question->id; 134 $previousid = $wrapped->id; 135 // Save_question strips this extra bit off the category again. 136 $wrapped->category = $question->category . ',1'; 137 $wrapped = question_bank::get_qtype($wrapped->qtype)->save_question( 138 $wrapped, clone($wrapped)); 139 $sequence[] = $wrapped->id; 140 if ($previousid != 0 && $previousid != $wrapped->id) { 141 // For some reasons a new question has been created 142 // so delete the old one. 143 question_delete_question($previousid); 144 } 145 } 146 147 // Delete redundant wrapped questions. 148 if (is_array($oldwrappedquestions) && count($oldwrappedquestions)) { 149 foreach ($oldwrappedquestions as $oldwrappedquestion) { 150 question_delete_question($oldwrappedquestion->id); 151 } 152 } 153 154 if (!empty($sequence)) { 155 $multianswer = new stdClass(); 156 $multianswer->question = $question->id; 157 $multianswer->sequence = implode(',', $sequence); 158 if ($oldid = $DB->get_field('question_multianswer', 'id', 159 array('question' => $question->id))) { 160 $multianswer->id = $oldid; 161 $DB->update_record('question_multianswer', $multianswer); 162 } else { 163 $DB->insert_record('question_multianswer', $multianswer); 164 } 165 } 166 167 $this->save_hints($question, true); 168 } 169 170 public function save_question($authorizedquestion, $form) { 171 $question = qtype_multianswer_extract_question($form->questiontext); 172 if (isset($authorizedquestion->id)) { 173 $question->id = $authorizedquestion->id; 174 } 175 176 $question->category = $authorizedquestion->category; 177 $form->defaultmark = $question->defaultmark; 178 $form->questiontext = $question->questiontext; 179 $form->questiontextformat = 0; 180 $form->options = clone($question->options); 181 unset($question->options); 182 return parent::save_question($question, $form); 183 } 184 185 protected function make_hint($hint) { 186 return question_hint_with_parts::load_from_record($hint); 187 } 188 189 public function delete_question($questionid, $contextid) { 190 global $DB; 191 $DB->delete_records('question_multianswer', array('question' => $questionid)); 192 193 parent::delete_question($questionid, $contextid); 194 } 195 196 protected function initialise_question_instance(question_definition $question, $questiondata) { 197 parent::initialise_question_instance($question, $questiondata); 198 199 $bits = preg_split('/\{#(\d+)\}/', $question->questiontext, 200 null, PREG_SPLIT_DELIM_CAPTURE); 201 $question->textfragments[0] = array_shift($bits); 202 $i = 1; 203 while (!empty($bits)) { 204 $question->places[$i] = array_shift($bits); 205 $question->textfragments[$i] = array_shift($bits); 206 $i += 1; 207 } 208 foreach ($questiondata->options->questions as $key => $subqdata) { 209 $subqdata->contextid = $questiondata->contextid; 210 if ($subqdata->qtype == 'multichoice') { 211 $answerregs = array(); 212 if ($subqdata->options->shuffleanswers == 1 && isset($questiondata->options->shuffleanswers) 213 && $questiondata->options->shuffleanswers == 0 ) { 214 $subqdata->options->shuffleanswers = 0; 215 } 216 } 217 $question->subquestions[$key] = question_bank::make_question($subqdata); 218 $question->subquestions[$key]->maxmark = $subqdata->defaultmark; 219 if (isset($subqdata->options->layout)) { 220 $question->subquestions[$key]->layout = $subqdata->options->layout; 221 } 222 } 223 } 224 225 public function get_random_guess_score($questiondata) { 226 $fractionsum = 0; 227 $fractionmax = 0; 228 foreach ($questiondata->options->questions as $key => $subqdata) { 229 $fractionmax += $subqdata->defaultmark; 230 $fractionsum += question_bank::get_qtype( 231 $subqdata->qtype)->get_random_guess_score($subqdata); 232 } 233 return $fractionsum / $fractionmax; 234 } 235 236 public function move_files($questionid, $oldcontextid, $newcontextid) { 237 parent::move_files($questionid, $oldcontextid, $newcontextid); 238 $this->move_files_in_hints($questionid, $oldcontextid, $newcontextid); 239 } 240 241 protected function delete_files($questionid, $contextid) { 242 parent::delete_files($questionid, $contextid); 243 $this->delete_files_in_hints($questionid, $contextid); 244 } 245 } 246 247 248 // ANSWER_ALTERNATIVE regexes. 249 define('ANSWER_ALTERNATIVE_FRACTION_REGEX', 250 '=|%(-?[0-9]+)%'); 251 // For the syntax '(?<!' see http://www.perl.com/doc/manual/html/pod/perlre.html#item_C. 252 define('ANSWER_ALTERNATIVE_ANSWER_REGEX', 253 '.+?(?<!\\\\|&|&)(?=[~#}]|$)'); 254 define('ANSWER_ALTERNATIVE_FEEDBACK_REGEX', 255 '.*?(?<!\\\\)(?=[~}]|$)'); 256 define('ANSWER_ALTERNATIVE_REGEX', 257 '(' . ANSWER_ALTERNATIVE_FRACTION_REGEX .')?' . 258 '(' . ANSWER_ALTERNATIVE_ANSWER_REGEX . ')' . 259 '(#(' . ANSWER_ALTERNATIVE_FEEDBACK_REGEX .'))?'); 260 261 // Parenthesis positions for ANSWER_ALTERNATIVE_REGEX. 262 define('ANSWER_ALTERNATIVE_REGEX_PERCENTILE_FRACTION', 2); 263 define('ANSWER_ALTERNATIVE_REGEX_FRACTION', 1); 264 define('ANSWER_ALTERNATIVE_REGEX_ANSWER', 3); 265 define('ANSWER_ALTERNATIVE_REGEX_FEEDBACK', 5); 266 267 // NUMBER_FORMATED_ALTERNATIVE_ANSWER_REGEX is used 268 // for identifying numerical answers in ANSWER_ALTERNATIVE_REGEX_ANSWER. 269 define('NUMBER_REGEX', 270 '-?(([0-9]+[.,]?[0-9]*|[.,][0-9]+)([eE][-+]?[0-9]+)?)'); 271 define('NUMERICAL_ALTERNATIVE_REGEX', 272 '^(' . NUMBER_REGEX . ')(:' . NUMBER_REGEX . ')?$'); 273 274 // Parenthesis positions for NUMERICAL_FORMATED_ALTERNATIVE_ANSWER_REGEX. 275 define('NUMERICAL_CORRECT_ANSWER', 1); 276 define('NUMERICAL_ABS_ERROR_MARGIN', 6); 277 278 // Remaining ANSWER regexes. 279 define('ANSWER_TYPE_DEF_REGEX', 280 '(NUMERICAL|NM)|(MULTICHOICE|MC)|(MULTICHOICE_V|MCV)|(MULTICHOICE_H|MCH)|' . 281 '(SHORTANSWER|SA|MW)|(SHORTANSWER_C|SAC|MWC)|' . 282 '(MULTICHOICE_S|MCS)|(MULTICHOICE_VS|MCVS)|(MULTICHOICE_HS|MCHS)'); 283 define('ANSWER_START_REGEX', 284 '\{([0-9]*):(' . ANSWER_TYPE_DEF_REGEX . '):'); 285 286 define('ANSWER_REGEX', 287 ANSWER_START_REGEX 288 . '(' . ANSWER_ALTERNATIVE_REGEX 289 . '(~' 290 . ANSWER_ALTERNATIVE_REGEX 291 . ')*)\}'); 292 293 // Parenthesis positions for singulars in ANSWER_REGEX. 294 define('ANSWER_REGEX_NORM', 1); 295 define('ANSWER_REGEX_ANSWER_TYPE_NUMERICAL', 3); 296 define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE', 4); 297 define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_REGULAR', 5); 298 define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_HORIZONTAL', 6); 299 define('ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER', 7); 300 define('ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER_C', 8); 301 define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_SHUFFLED', 9); 302 define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_REGULAR_SHUFFLED', 10); 303 define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_HORIZONTAL_SHUFFLED', 11); 304 define('ANSWER_REGEX_ALTERNATIVES', 12); 305 306 /** 307 * Initialise subquestion fields that are constant across all MULTICHOICE 308 * types. 309 * 310 * @param objet $wrapped The subquestion to initialise 311 * 312 */ 313 function qtype_multianswer_initialise_multichoice_subquestion($wrapped) { 314 $wrapped->qtype = 'multichoice'; 315 $wrapped->single = 1; 316 $wrapped->answernumbering = 0; 317 $wrapped->correctfeedback['text'] = ''; 318 $wrapped->correctfeedback['format'] = FORMAT_HTML; 319 $wrapped->correctfeedback['itemid'] = ''; 320 $wrapped->partiallycorrectfeedback['text'] = ''; 321 $wrapped->partiallycorrectfeedback['format'] = FORMAT_HTML; 322 $wrapped->partiallycorrectfeedback['itemid'] = ''; 323 $wrapped->incorrectfeedback['text'] = ''; 324 $wrapped->incorrectfeedback['format'] = FORMAT_HTML; 325 $wrapped->incorrectfeedback['itemid'] = ''; 326 } 327 328 function qtype_multianswer_extract_question($text) { 329 // Variable $text is an array [text][format][itemid]. 330 $question = new stdClass(); 331 $question->qtype = 'multianswer'; 332 $question->questiontext = $text; 333 $question->generalfeedback['text'] = ''; 334 $question->generalfeedback['format'] = FORMAT_HTML; 335 $question->generalfeedback['itemid'] = ''; 336 337 $question->options = new stdClass(); 338 $question->options->questions = array(); 339 $question->defaultmark = 0; // Will be increased for each answer norm. 340 341 for ($positionkey = 1; 342 preg_match('/'.ANSWER_REGEX.'/s', $question->questiontext['text'], $answerregs); 343 ++$positionkey) { 344 $wrapped = new stdClass(); 345 $wrapped->generalfeedback['text'] = ''; 346 $wrapped->generalfeedback['format'] = FORMAT_HTML; 347 $wrapped->generalfeedback['itemid'] = ''; 348 if (isset($answerregs[ANSWER_REGEX_NORM]) && $answerregs[ANSWER_REGEX_NORM] !== '') { 349 $wrapped->defaultmark = $answerregs[ANSWER_REGEX_NORM]; 350 } else { 351 $wrapped->defaultmark = '1'; 352 } 353 if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_NUMERICAL])) { 354 $wrapped->qtype = 'numerical'; 355 $wrapped->multiplier = array(); 356 $wrapped->units = array(); 357 $wrapped->instructions['text'] = ''; 358 $wrapped->instructions['format'] = FORMAT_HTML; 359 $wrapped->instructions['itemid'] = ''; 360 } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER])) { 361 $wrapped->qtype = 'shortanswer'; 362 $wrapped->usecase = 0; 363 } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER_C])) { 364 $wrapped->qtype = 'shortanswer'; 365 $wrapped->usecase = 1; 366 } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE])) { 367 qtype_multianswer_initialise_multichoice_subquestion($wrapped); 368 $wrapped->shuffleanswers = 0; 369 $wrapped->layout = qtype_multichoice_base::LAYOUT_DROPDOWN; 370 } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_SHUFFLED])) { 371 qtype_multianswer_initialise_multichoice_subquestion($wrapped); 372 $wrapped->shuffleanswers = 1; 373 $wrapped->layout = qtype_multichoice_base::LAYOUT_DROPDOWN; 374 } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_REGULAR])) { 375 qtype_multianswer_initialise_multichoice_subquestion($wrapped); 376 $wrapped->shuffleanswers = 0; 377 $wrapped->layout = qtype_multichoice_base::LAYOUT_VERTICAL; 378 } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_REGULAR_SHUFFLED])) { 379 qtype_multianswer_initialise_multichoice_subquestion($wrapped); 380 $wrapped->shuffleanswers = 1; 381 $wrapped->layout = qtype_multichoice_base::LAYOUT_VERTICAL; 382 } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_HORIZONTAL])) { 383 qtype_multianswer_initialise_multichoice_subquestion($wrapped); 384 $wrapped->shuffleanswers = 0; 385 $wrapped->layout = qtype_multichoice_base::LAYOUT_HORIZONTAL; 386 } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_HORIZONTAL_SHUFFLED])) { 387 qtype_multianswer_initialise_multichoice_subquestion($wrapped); 388 $wrapped->shuffleanswers = 1; 389 $wrapped->layout = qtype_multichoice_base::LAYOUT_HORIZONTAL; 390 } else { 391 print_error('unknownquestiontype', 'question', '', $answerregs[2]); 392 return false; 393 } 394 395 // Each $wrapped simulates a $form that can be processed by the 396 // respective save_question and save_question_options methods of the 397 // wrapped questiontypes. 398 $wrapped->answer = array(); 399 $wrapped->fraction = array(); 400 $wrapped->feedback = array(); 401 $wrapped->questiontext['text'] = $answerregs[0]; 402 $wrapped->questiontext['format'] = FORMAT_HTML; 403 $wrapped->questiontext['itemid'] = ''; 404 $answerindex = 0; 405 406 $remainingalts = $answerregs[ANSWER_REGEX_ALTERNATIVES]; 407 while (preg_match('/~?'.ANSWER_ALTERNATIVE_REGEX.'/s', $remainingalts, $altregs)) { 408 if ('=' == $altregs[ANSWER_ALTERNATIVE_REGEX_FRACTION]) { 409 $wrapped->fraction["{$answerindex}"] = '1'; 410 } else if ($percentile = $altregs[ANSWER_ALTERNATIVE_REGEX_PERCENTILE_FRACTION]) { 411 $wrapped->fraction["{$answerindex}"] = .01 * $percentile; 412 } else { 413 $wrapped->fraction["{$answerindex}"] = '0'; 414 } 415 if (isset($altregs[ANSWER_ALTERNATIVE_REGEX_FEEDBACK])) { 416 $feedback = html_entity_decode( 417 $altregs[ANSWER_ALTERNATIVE_REGEX_FEEDBACK], ENT_QUOTES, 'UTF-8'); 418 $feedback = str_replace('\}', '}', $feedback); 419 $wrapped->feedback["{$answerindex}"]['text'] = str_replace('\#', '#', $feedback); 420 $wrapped->feedback["{$answerindex}"]['format'] = FORMAT_HTML; 421 $wrapped->feedback["{$answerindex}"]['itemid'] = ''; 422 } else { 423 $wrapped->feedback["{$answerindex}"]['text'] = ''; 424 $wrapped->feedback["{$answerindex}"]['format'] = FORMAT_HTML; 425 $wrapped->feedback["{$answerindex}"]['itemid'] = ''; 426 427 } 428 if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_NUMERICAL]) 429 && preg_match('~'.NUMERICAL_ALTERNATIVE_REGEX.'~s', 430 $altregs[ANSWER_ALTERNATIVE_REGEX_ANSWER], $numregs)) { 431 $wrapped->answer[] = $numregs[NUMERICAL_CORRECT_ANSWER]; 432 if (array_key_exists(NUMERICAL_ABS_ERROR_MARGIN, $numregs)) { 433 $wrapped->tolerance["{$answerindex}"] = 434 $numregs[NUMERICAL_ABS_ERROR_MARGIN]; 435 } else { 436 $wrapped->tolerance["{$answerindex}"] = 0; 437 } 438 } else { // Tolerance can stay undefined for non numerical questions. 439 // Undo quoting done by the HTML editor. 440 $answer = html_entity_decode( 441 $altregs[ANSWER_ALTERNATIVE_REGEX_ANSWER], ENT_QUOTES, 'UTF-8'); 442 $answer = str_replace('\}', '}', $answer); 443 $wrapped->answer["{$answerindex}"] = str_replace('\#', '#', $answer); 444 if ($wrapped->qtype == 'multichoice') { 445 $wrapped->answer["{$answerindex}"] = array( 446 'text' => $wrapped->answer["{$answerindex}"], 447 'format' => FORMAT_HTML, 448 'itemid' => ''); 449 } 450 } 451 $tmp = explode($altregs[0], $remainingalts, 2); 452 $remainingalts = $tmp[1]; 453 $answerindex++; 454 } 455 456 $question->defaultmark += $wrapped->defaultmark; 457 $question->options->questions[$positionkey] = clone($wrapped); 458 $question->questiontext['text'] = implode("{#$positionkey}", 459 explode($answerregs[0], $question->questiontext['text'], 2)); 460 } 461 return $question; 462 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Thu Aug 11 10:00:09 2016 | Cross-referenced by PHPXref 0.7.1 |