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