[ 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 * The default questiontype class. 19 * 20 * @package moodlecore 21 * @subpackage questiontypes 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/engine/lib.php'); 30 31 32 /** 33 * This is the base class for Moodle question types. 34 * 35 * There are detailed comments on each method, explaining what the method is 36 * for, and the circumstances under which you might need to override it. 37 * 38 * Note: the questiontype API should NOT be considered stable yet. Very few 39 * question types have been produced yet, so we do not yet know all the places 40 * where the current API is insufficient. I would rather learn from the 41 * experiences of the first few question type implementors, and improve the 42 * interface to meet their needs, rather the freeze the API prematurely and 43 * condem everyone to working round a clunky interface for ever afterwards. 44 * 45 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} 46 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 47 */ 48 class question_type { 49 protected $fileoptions = array( 50 'subdirs' => true, 51 'maxfiles' => -1, 52 'maxbytes' => 0, 53 ); 54 55 public function __construct() { 56 } 57 58 /** 59 * @return string the name of this question type. 60 */ 61 public function name() { 62 return substr(get_class($this), 6); 63 } 64 65 /** 66 * @return string the full frankenstyle name for this plugin. 67 */ 68 public function plugin_name() { 69 return get_class($this); 70 } 71 72 /** 73 * @return string the name of this question type in the user's language. 74 * You should not need to override this method, the default behaviour should be fine. 75 */ 76 public function local_name() { 77 return get_string('pluginname', $this->plugin_name()); 78 } 79 80 /** 81 * The name this question should appear as in the create new question 82 * dropdown. Override this method to return false if you don't want your 83 * question type to be createable, for example if it is an abstract base type, 84 * otherwise, you should not need to override this method. 85 * 86 * @return mixed the desired string, or false to hide this question type in the menu. 87 */ 88 public function menu_name() { 89 return $this->local_name(); 90 } 91 92 /** 93 * @return bool override this to return false if this is not really a 94 * question type, for example the description question type is not 95 * really a question type. 96 */ 97 public function is_real_question_type() { 98 return true; 99 } 100 101 /** 102 * @return bool true if this question type sometimes requires manual grading. 103 */ 104 public function is_manual_graded() { 105 return false; 106 } 107 108 /** 109 * @param object $question a question of this type. 110 * @param string $otherquestionsinuse comma-separate list of other question ids in this attempt. 111 * @return bool true if a particular instance of this question requires manual grading. 112 */ 113 public function is_question_manual_graded($question, $otherquestionsinuse) { 114 return $this->is_manual_graded(); 115 } 116 117 /** 118 * @return bool true if this question type can be used by the random question type. 119 */ 120 public function is_usable_by_random() { 121 return true; 122 } 123 124 /** 125 * Whether this question type can perform a frequency analysis of student 126 * responses. 127 * 128 * If this method returns true, you must implement the get_possible_responses 129 * method, and the question_definition class must implement the 130 * classify_response method. 131 * 132 * @return bool whether this report can analyse all the student responses 133 * for things like the quiz statistics report. 134 */ 135 public function can_analyse_responses() { 136 // This works in most cases. 137 return !$this->is_manual_graded(); 138 } 139 140 /** 141 * @return whether the question_answers.answer field needs to have 142 * restore_decode_content_links_worker called on it. 143 */ 144 public function has_html_answers() { 145 return false; 146 } 147 148 /** 149 * If your question type has a table that extends the question table, and 150 * you want the base class to automatically save, backup and restore the extra fields, 151 * override this method to return an array wherer the first element is the table name, 152 * and the subsequent entries are the column names (apart from id and questionid). 153 * 154 * @return mixed array as above, or null to tell the base class to do nothing. 155 */ 156 public function extra_question_fields() { 157 return null; 158 } 159 160 /** 161 * If you use extra_question_fields, overload this function to return question id field name 162 * in case you table use another name for this column 163 */ 164 public function questionid_column_name() { 165 return 'questionid'; 166 } 167 168 /** 169 * If your question type has a table that extends the question_answers table, 170 * make this method return an array wherer the first element is the table name, 171 * and the subsequent entries are the column names (apart from id and answerid). 172 * 173 * @return mixed array as above, or null to tell the base class to do nothing. 174 */ 175 public function extra_answer_fields() { 176 return null; 177 } 178 179 /** 180 * If the quetsion type uses files in responses, then this method should 181 * return an array of all the response variables that might have corresponding 182 * files. For example, the essay qtype returns array('attachments', 'answers'). 183 * 184 * @return array response variable names that may have associated files. 185 */ 186 public function response_file_areas() { 187 return array(); 188 } 189 190 /** 191 * Return an instance of the question editing form definition. This looks for a 192 * class called edit_{$this->name()}_question_form in the file 193 * question/type/{$this->name()}/edit_{$this->name()}_question_form.php 194 * and if it exists returns an instance of it. 195 * 196 * @param string $submiturl passed on to the constructor call. 197 * @return object an instance of the form definition, or null if one could not be found. 198 */ 199 public function create_editing_form($submiturl, $question, $category, 200 $contexts, $formeditable) { 201 global $CFG; 202 require_once($CFG->dirroot . '/question/type/edit_question_form.php'); 203 $definitionfile = $CFG->dirroot . '/question/type/' . $this->name() . 204 '/edit_' . $this->name() . '_form.php'; 205 if (!is_readable($definitionfile) || !is_file($definitionfile)) { 206 throw new coding_exception($this->plugin_name() . 207 ' is missing the definition of its editing formin file ' . 208 $definitionfile . '.'); 209 } 210 require_once($definitionfile); 211 $classname = $this->plugin_name() . '_edit_form'; 212 if (!class_exists($classname)) { 213 throw new coding_exception($this->plugin_name() . 214 ' does not define the class ' . $this->plugin_name() . 215 '_edit_form.'); 216 } 217 return new $classname($submiturl, $question, $category, $contexts, $formeditable); 218 } 219 220 /** 221 * @return string the full path of the folder this plugin's files live in. 222 */ 223 public function plugin_dir() { 224 global $CFG; 225 return $CFG->dirroot . '/question/type/' . $this->name(); 226 } 227 228 /** 229 * @return string the URL of the folder this plugin's files live in. 230 */ 231 public function plugin_baseurl() { 232 global $CFG; 233 return $CFG->wwwroot . '/question/type/' . $this->name(); 234 } 235 236 /** 237 * This method should be overriden if you want to include a special heading or some other 238 * html on a question editing page besides the question editing form. 239 * 240 * @param question_edit_form $mform a child of question_edit_form 241 * @param object $question 242 * @param string $wizardnow is '' for first page. 243 */ 244 public function display_question_editing_page($mform, $question, $wizardnow) { 245 global $OUTPUT; 246 $heading = $this->get_heading(empty($question->id)); 247 echo $OUTPUT->heading_with_help($heading, 'pluginname', $this->plugin_name()); 248 $mform->display(); 249 } 250 251 /** 252 * Method called by display_question_editing_page and by question.php to get 253 * heading for breadcrumbs. 254 * 255 * @return string the heading 256 */ 257 public function get_heading($adding = false) { 258 if ($adding) { 259 $string = 'pluginnameadding'; 260 } else { 261 $string = 'pluginnameediting'; 262 } 263 return get_string($string, $this->plugin_name()); 264 } 265 266 /** 267 * Set any missing settings for this question to the default values. This is 268 * called before displaying the question editing form. 269 * 270 * @param object $questiondata the question data, loaded from the databsae, 271 * or more likely a newly created question object that is only partially 272 * initialised. 273 */ 274 public function set_default_options($questiondata) { 275 } 276 277 /** 278 * Saves (creates or updates) a question. 279 * 280 * Given some question info and some data about the answers 281 * this function parses, organises and saves the question 282 * It is used by {@link question.php} when saving new data from 283 * a form, and also by {@link import.php} when importing questions 284 * This function in turn calls {@link save_question_options} 285 * to save question-type specific data. 286 * 287 * Whether we are saving a new question or updating an existing one can be 288 * determined by testing !empty($question->id). If it is not empty, we are updating. 289 * 290 * The question will be saved in category $form->category. 291 * 292 * @param object $question the question object which should be updated. For a 293 * new question will be mostly empty. 294 * @param object $form the object containing the information to save, as if 295 * from the question editing form. 296 * @param object $course not really used any more. 297 * @return object On success, return the new question object. On failure, 298 * return an object as follows. If the error object has an errors field, 299 * display that as an error message. Otherwise, the editing form will be 300 * redisplayed with validation errors, from validation_errors field, which 301 * is itself an object, shown next to the form fields. (I don't think this 302 * is accurate any more.) 303 */ 304 public function save_question($question, $form) { 305 global $USER, $DB, $OUTPUT; 306 307 list($question->category) = explode(',', $form->category); 308 $context = $this->get_context_by_category_id($question->category); 309 310 // This default implementation is suitable for most 311 // question types. 312 313 // First, save the basic question itself. 314 $question->name = trim($form->name); 315 $question->parent = isset($form->parent) ? $form->parent : 0; 316 $question->length = $this->actual_number_of_questions($question); 317 $question->penalty = isset($form->penalty) ? $form->penalty : 0; 318 319 // The trim call below has the effect of casting any strange values received, 320 // like null or false, to an appropriate string, so we only need to test for 321 // missing values. Be careful not to break the value '0' here. 322 if (!isset($form->questiontext['text'])) { 323 $question->questiontext = ''; 324 } else { 325 $question->questiontext = trim($form->questiontext['text']); 326 } 327 $question->questiontextformat = !empty($form->questiontext['format']) ? 328 $form->questiontext['format'] : 0; 329 330 if (empty($form->generalfeedback['text'])) { 331 $question->generalfeedback = ''; 332 } else { 333 $question->generalfeedback = trim($form->generalfeedback['text']); 334 } 335 $question->generalfeedbackformat = !empty($form->generalfeedback['format']) ? 336 $form->generalfeedback['format'] : 0; 337 338 if ($question->name === '') { 339 $question->name = shorten_text(strip_tags($form->questiontext['text']), 15); 340 if ($question->name === '') { 341 $question->name = '-'; 342 } 343 } 344 345 if ($question->penalty > 1 or $question->penalty < 0) { 346 $question->errors['penalty'] = get_string('invalidpenalty', 'question'); 347 } 348 349 if (isset($form->defaultmark)) { 350 $question->defaultmark = $form->defaultmark; 351 } 352 353 // If the question is new, create it. 354 if (empty($question->id)) { 355 // Set the unique code. 356 $question->stamp = make_unique_id_code(); 357 $question->createdby = $USER->id; 358 $question->timecreated = time(); 359 $question->id = $DB->insert_record('question', $question); 360 } 361 362 // Now, whether we are updating a existing question, or creating a new 363 // one, we have to do the files processing and update the record. 364 // Question already exists, update. 365 $question->modifiedby = $USER->id; 366 $question->timemodified = time(); 367 368 if (!empty($question->questiontext) && !empty($form->questiontext['itemid'])) { 369 $question->questiontext = file_save_draft_area_files($form->questiontext['itemid'], 370 $context->id, 'question', 'questiontext', (int)$question->id, 371 $this->fileoptions, $question->questiontext); 372 } 373 if (!empty($question->generalfeedback) && !empty($form->generalfeedback['itemid'])) { 374 $question->generalfeedback = file_save_draft_area_files( 375 $form->generalfeedback['itemid'], $context->id, 376 'question', 'generalfeedback', (int)$question->id, 377 $this->fileoptions, $question->generalfeedback); 378 } 379 $DB->update_record('question', $question); 380 381 // Now to save all the answers and type-specific options. 382 $form->id = $question->id; 383 $form->qtype = $question->qtype; 384 $form->category = $question->category; 385 $form->questiontext = $question->questiontext; 386 $form->questiontextformat = $question->questiontextformat; 387 // Current context. 388 $form->context = $context; 389 390 $result = $this->save_question_options($form); 391 392 if (!empty($result->error)) { 393 print_error($result->error); 394 } 395 396 if (!empty($result->notice)) { 397 notice($result->notice, "question.php?id={$question->id}"); 398 } 399 400 if (!empty($result->noticeyesno)) { 401 throw new coding_exception( 402 '$result->noticeyesno no longer supported in save_question.'); 403 } 404 405 // Give the question a unique version stamp determined by question_hash(). 406 $DB->set_field('question', 'version', question_hash($question), 407 array('id' => $question->id)); 408 409 return $question; 410 } 411 412 /** 413 * Saves question-type specific options 414 * 415 * This is called by {@link save_question()} to save the question-type specific data 416 * @return object $result->error or $result->notice 417 * @param object $question This holds the information from the editing form, 418 * it is not a standard question object. 419 */ 420 public function save_question_options($question) { 421 global $DB; 422 $extraquestionfields = $this->extra_question_fields(); 423 424 if (is_array($extraquestionfields)) { 425 $question_extension_table = array_shift($extraquestionfields); 426 427 $function = 'update_record'; 428 $questionidcolname = $this->questionid_column_name(); 429 $options = $DB->get_record($question_extension_table, 430 array($questionidcolname => $question->id)); 431 if (!$options) { 432 $function = 'insert_record'; 433 $options = new stdClass(); 434 $options->$questionidcolname = $question->id; 435 } 436 foreach ($extraquestionfields as $field) { 437 if (property_exists($question, $field)) { 438 $options->$field = $question->$field; 439 } 440 } 441 442 $DB->{$function}($question_extension_table, $options); 443 } 444 } 445 446 /** 447 * Save the answers, with any extra data. 448 * 449 * Questions that use answers will call it from {@link save_question_options()}. 450 * @param object $question This holds the information from the editing form, 451 * it is not a standard question object. 452 * @return object $result->error or $result->notice 453 */ 454 public function save_question_answers($question) { 455 global $DB; 456 457 $context = $question->context; 458 $oldanswers = $DB->get_records('question_answers', 459 array('question' => $question->id), 'id ASC'); 460 461 // We need separate arrays for answers and extra answer data, so no JOINS there. 462 $extraanswerfields = $this->extra_answer_fields(); 463 $isextraanswerfields = is_array($extraanswerfields); 464 $extraanswertable = ''; 465 $oldanswerextras = array(); 466 if ($isextraanswerfields) { 467 $extraanswertable = array_shift($extraanswerfields); 468 if (!empty($oldanswers)) { 469 $oldanswerextras = $DB->get_records_sql("SELECT * FROM {{$extraanswertable}} WHERE " . 470 'answerid IN (SELECT id FROM {question_answers} WHERE question = ' . $question->id . ')' ); 471 } 472 } 473 474 // Insert all the new answers. 475 foreach ($question->answer as $key => $answerdata) { 476 // Check for, and ignore, completely blank answer from the form. 477 if ($this->is_answer_empty($question, $key)) { 478 continue; 479 } 480 481 // Update an existing answer if possible. 482 $answer = array_shift($oldanswers); 483 if (!$answer) { 484 $answer = new stdClass(); 485 $answer->question = $question->id; 486 $answer->answer = ''; 487 $answer->feedback = ''; 488 $answer->id = $DB->insert_record('question_answers', $answer); 489 } 490 491 $answer = $this->fill_answer_fields($answer, $question, $key, $context); 492 $DB->update_record('question_answers', $answer); 493 494 if ($isextraanswerfields) { 495 // Check, if this answer contains some extra field data. 496 if ($this->is_extra_answer_fields_empty($question, $key)) { 497 continue; 498 } 499 500 $answerextra = array_shift($oldanswerextras); 501 if (!$answerextra) { 502 $answerextra = new stdClass(); 503 $answerextra->answerid = $answer->id; 504 // Avoid looking for correct default for any possible DB field type 505 // by setting real values. 506 $answerextra = $this->fill_extra_answer_fields($answerextra, $question, $key, $context, $extraanswerfields); 507 $answerextra->id = $DB->insert_record($extraanswertable, $answerextra); 508 } else { 509 // Update answerid, as record may be reused from another answer. 510 $answerextra->answerid = $answer->id; 511 $answerextra = $this->fill_extra_answer_fields($answerextra, $question, $key, $context, $extraanswerfields); 512 $DB->update_record($extraanswertable, $answerextra); 513 } 514 } 515 } 516 517 if ($isextraanswerfields) { 518 // Delete any left over extra answer fields records. 519 $oldanswerextraids = array(); 520 foreach ($oldanswerextras as $oldextra) { 521 $oldanswerextraids[] = $oldextra->id; 522 } 523 $DB->delete_records_list($extraanswertable, 'id', $oldanswerextraids); 524 } 525 526 // Delete any left over old answer records. 527 $fs = get_file_storage(); 528 foreach ($oldanswers as $oldanswer) { 529 $fs->delete_area_files($context->id, 'question', 'answerfeedback', $oldanswer->id); 530 $DB->delete_records('question_answers', array('id' => $oldanswer->id)); 531 } 532 } 533 534 /** 535 * Returns true is answer with the $key is empty in the question data and should not be saved in DB. 536 * 537 * The questions using question_answers table may want to overload this. Default code will work 538 * for shortanswer and similar question types. 539 * @param object $questiondata This holds the information from the question editing form or import. 540 * @param int $key A key of the answer in question. 541 * @return bool True if answer shouldn't be saved in DB. 542 */ 543 protected function is_answer_empty($questiondata, $key) { 544 return trim($questiondata->answer[$key]) == '' && $questiondata->fraction[$key] == 0 && 545 html_is_blank($questiondata->feedback[$key]['text']); 546 } 547 548 /** 549 * Return $answer, filling necessary fields for the question_answers table. 550 * 551 * The questions using question_answers table may want to overload this. Default code will work 552 * for shortanswer and similar question types. 553 * @param stdClass $answer Object to save data. 554 * @param object $questiondata This holds the information from the question editing form or import. 555 * @param int $key A key of the answer in question. 556 * @param object $context needed for working with files. 557 * @return $answer answer with filled data. 558 */ 559 protected function fill_answer_fields($answer, $questiondata, $key, $context) { 560 $answer->answer = $questiondata->answer[$key]; 561 $answer->fraction = $questiondata->fraction[$key]; 562 $answer->feedback = $this->import_or_save_files($questiondata->feedback[$key], 563 $context, 'question', 'answerfeedback', $answer->id); 564 $answer->feedbackformat = $questiondata->feedback[$key]['format']; 565 return $answer; 566 } 567 568 /** 569 * Returns true if extra answer fields for answer with the $key is empty 570 * in the question data and should not be saved in DB. 571 * 572 * Questions where extra answer fields are optional will want to overload this. 573 * @param object $questiondata This holds the information from the question editing form or import. 574 * @param int $key A key of the answer in question. 575 * @return bool True if extra answer data shouldn't be saved in DB. 576 */ 577 protected function is_extra_answer_fields_empty($questiondata, $key) { 578 // No extra answer data in base class. 579 return true; 580 } 581 582 /** 583 * Return $answerextra, filling necessary fields for the extra answer fields table. 584 * 585 * The questions may want to overload it to save files or do other data processing. 586 * @param stdClass $answerextra Object to save data. 587 * @param object $questiondata This holds the information from the question editing form or import. 588 * @param int $key A key of the answer in question. 589 * @param object $context needed for working with files. 590 * @param array $extraanswerfields extra answer fields (without table name). 591 * @return $answer answerextra with filled data. 592 */ 593 protected function fill_extra_answer_fields($answerextra, $questiondata, $key, $context, $extraanswerfields) { 594 foreach ($extraanswerfields as $field) { 595 // The $questiondata->$field[$key] won't work in PHP, break it down to two strings of code. 596 $fieldarray = $questiondata->$field; 597 $answerextra->$field = $fieldarray[$key]; 598 } 599 return $answerextra; 600 } 601 602 public function save_hints($formdata, $withparts = false) { 603 global $DB; 604 $context = $formdata->context; 605 606 $oldhints = $DB->get_records('question_hints', 607 array('questionid' => $formdata->id), 'id ASC'); 608 609 610 $numhints = $this->count_hints_on_form($formdata, $withparts); 611 612 for ($i = 0; $i < $numhints; $i += 1) { 613 if (html_is_blank($formdata->hint[$i]['text'])) { 614 $formdata->hint[$i]['text'] = ''; 615 } 616 617 if ($withparts) { 618 $clearwrong = !empty($formdata->hintclearwrong[$i]); 619 $shownumcorrect = !empty($formdata->hintshownumcorrect[$i]); 620 } 621 622 if ($this->is_hint_empty_in_form_data($formdata, $i, $withparts)) { 623 continue; 624 } 625 626 // Update an existing hint if possible. 627 $hint = array_shift($oldhints); 628 if (!$hint) { 629 $hint = new stdClass(); 630 $hint->questionid = $formdata->id; 631 $hint->hint = ''; 632 $hint->id = $DB->insert_record('question_hints', $hint); 633 } 634 635 $hint->hint = $this->import_or_save_files($formdata->hint[$i], 636 $context, 'question', 'hint', $hint->id); 637 $hint->hintformat = $formdata->hint[$i]['format']; 638 if ($withparts) { 639 $hint->clearwrong = $clearwrong; 640 $hint->shownumcorrect = $shownumcorrect; 641 } 642 $hint->options = $this->save_hint_options($formdata, $i, $withparts); 643 $DB->update_record('question_hints', $hint); 644 } 645 646 // Delete any remaining old hints. 647 $fs = get_file_storage(); 648 foreach ($oldhints as $oldhint) { 649 $fs->delete_area_files($context->id, 'question', 'hint', $oldhint->id); 650 $DB->delete_records('question_hints', array('id' => $oldhint->id)); 651 } 652 } 653 654 /** 655 * Count number of hints on the form. 656 * Overload if you use custom hint controls. 657 * @param object $formdata the data from the form. 658 * @param bool $withparts whether to take into account clearwrong and shownumcorrect options. 659 * @return int count of hints on the form. 660 */ 661 protected function count_hints_on_form($formdata, $withparts) { 662 if (!empty($formdata->hint)) { 663 $numhints = max(array_keys($formdata->hint)) + 1; 664 } else { 665 $numhints = 0; 666 } 667 668 if ($withparts) { 669 if (!empty($formdata->hintclearwrong)) { 670 $numclears = max(array_keys($formdata->hintclearwrong)) + 1; 671 } else { 672 $numclears = 0; 673 } 674 if (!empty($formdata->hintshownumcorrect)) { 675 $numshows = max(array_keys($formdata->hintshownumcorrect)) + 1; 676 } else { 677 $numshows = 0; 678 } 679 $numhints = max($numhints, $numclears, $numshows); 680 } 681 return $numhints; 682 } 683 684 /** 685 * Determine if the hint with specified number is not empty and should be saved. 686 * Overload if you use custom hint controls. 687 * @param object $formdata the data from the form. 688 * @param int $number number of hint under question. 689 * @param bool $withparts whether to take into account clearwrong and shownumcorrect options. 690 * @return bool is this particular hint data empty. 691 */ 692 protected function is_hint_empty_in_form_data($formdata, $number, $withparts) { 693 if ($withparts) { 694 return empty($formdata->hint[$number]['text']) && empty($formdata->hintclearwrong[$number]) && 695 empty($formdata->hintshownumcorrect[$number]); 696 } else { 697 return empty($formdata->hint[$number]['text']); 698 } 699 } 700 701 /** 702 * Save additional question type data into the hint optional field. 703 * Overload if you use custom hint information. 704 * @param object $formdata the data from the form. 705 * @param int $number number of hint to get options from. 706 * @param bool $withparts whether question have parts. 707 * @return string value to save into the options field of question_hints table. 708 */ 709 protected function save_hint_options($formdata, $number, $withparts) { 710 return null; // By default, options field is unused. 711 } 712 713 /** 714 * Can be used to {@link save_question_options()} to transfer the combined 715 * feedback fields from $formdata to $options. 716 * @param object $options the $question->options object being built. 717 * @param object $formdata the data from the form. 718 * @param object $context the context the quetsion is being saved into. 719 * @param bool $withparts whether $options->shownumcorrect should be set. 720 */ 721 protected function save_combined_feedback_helper($options, $formdata, 722 $context, $withparts = false) { 723 $options->correctfeedback = $this->import_or_save_files($formdata->correctfeedback, 724 $context, 'question', 'correctfeedback', $formdata->id); 725 $options->correctfeedbackformat = $formdata->correctfeedback['format']; 726 727 $options->partiallycorrectfeedback = $this->import_or_save_files( 728 $formdata->partiallycorrectfeedback, 729 $context, 'question', 'partiallycorrectfeedback', $formdata->id); 730 $options->partiallycorrectfeedbackformat = $formdata->partiallycorrectfeedback['format']; 731 732 $options->incorrectfeedback = $this->import_or_save_files($formdata->incorrectfeedback, 733 $context, 'question', 'incorrectfeedback', $formdata->id); 734 $options->incorrectfeedbackformat = $formdata->incorrectfeedback['format']; 735 736 if ($withparts) { 737 $options->shownumcorrect = !empty($formdata->shownumcorrect); 738 } 739 740 return $options; 741 } 742 743 /** 744 * Loads the question type specific options for the question. 745 * 746 * This function loads any question type specific options for the 747 * question from the database into the question object. This information 748 * is placed in the $question->options field. A question type is 749 * free, however, to decide on a internal structure of the options field. 750 * @return bool Indicates success or failure. 751 * @param object $question The question object for the question. This object 752 * should be updated to include the question type 753 * specific information (it is passed by reference). 754 */ 755 public function get_question_options($question) { 756 global $CFG, $DB, $OUTPUT; 757 758 if (!isset($question->options)) { 759 $question->options = new stdClass(); 760 } 761 762 $extraquestionfields = $this->extra_question_fields(); 763 if (is_array($extraquestionfields)) { 764 $question_extension_table = array_shift($extraquestionfields); 765 $extra_data = $DB->get_record($question_extension_table, 766 array($this->questionid_column_name() => $question->id), 767 implode(', ', $extraquestionfields)); 768 if ($extra_data) { 769 foreach ($extraquestionfields as $field) { 770 $question->options->$field = $extra_data->$field; 771 } 772 } else { 773 echo $OUTPUT->notification('Failed to load question options from the table ' . 774 $question_extension_table . ' for questionid ' . $question->id); 775 return false; 776 } 777 } 778 779 $extraanswerfields = $this->extra_answer_fields(); 780 if (is_array($extraanswerfields)) { 781 $answerextensiontable = array_shift($extraanswerfields); 782 // Use LEFT JOIN in case not every answer has extra data. 783 $question->options->answers = $DB->get_records_sql(" 784 SELECT qa.*, qax." . implode(', qax.', $extraanswerfields) . ' 785 FROM {question_answers} qa ' . " 786 LEFT JOIN {{$answerextensiontable}} qax ON qa.id = qax.answerid 787 WHERE qa.question = ? 788 ORDER BY qa.id", array($question->id)); 789 if (!$question->options->answers) { 790 echo $OUTPUT->notification('Failed to load question answers from the table ' . 791 $answerextensiontable . 'for questionid ' . $question->id); 792 return false; 793 } 794 } else { 795 // Don't check for success or failure because some question types do 796 // not use the answers table. 797 $question->options->answers = $DB->get_records('question_answers', 798 array('question' => $question->id), 'id ASC'); 799 } 800 801 $question->hints = $DB->get_records('question_hints', 802 array('questionid' => $question->id), 'id ASC'); 803 804 return true; 805 } 806 807 /** 808 * Create an appropriate question_definition for the question of this type 809 * using data loaded from the database. 810 * @param object $questiondata the question data loaded from the database. 811 * @return question_definition the corresponding question_definition. 812 */ 813 public function make_question($questiondata) { 814 $question = $this->make_question_instance($questiondata); 815 $this->initialise_question_instance($question, $questiondata); 816 return $question; 817 } 818 819 /** 820 * Create an appropriate question_definition for the question of this type 821 * using data loaded from the database. 822 * @param object $questiondata the question data loaded from the database. 823 * @return question_definition an instance of the appropriate question_definition subclass. 824 * Still needs to be initialised. 825 */ 826 protected function make_question_instance($questiondata) { 827 question_bank::load_question_definition_classes($this->name()); 828 $class = 'qtype_' . $this->name() . '_question'; 829 return new $class(); 830 } 831 832 /** 833 * Initialise the common question_definition fields. 834 * @param question_definition $question the question_definition we are creating. 835 * @param object $questiondata the question data loaded from the database. 836 */ 837 protected function initialise_question_instance(question_definition $question, $questiondata) { 838 $question->id = $questiondata->id; 839 $question->category = $questiondata->category; 840 $question->contextid = $questiondata->contextid; 841 $question->parent = $questiondata->parent; 842 $question->qtype = $this; 843 $question->name = $questiondata->name; 844 $question->questiontext = $questiondata->questiontext; 845 $question->questiontextformat = $questiondata->questiontextformat; 846 $question->generalfeedback = $questiondata->generalfeedback; 847 $question->generalfeedbackformat = $questiondata->generalfeedbackformat; 848 $question->defaultmark = $questiondata->defaultmark + 0; 849 $question->length = $questiondata->length; 850 $question->penalty = $questiondata->penalty; 851 $question->stamp = $questiondata->stamp; 852 $question->version = $questiondata->version; 853 $question->hidden = $questiondata->hidden; 854 $question->timecreated = $questiondata->timecreated; 855 $question->timemodified = $questiondata->timemodified; 856 $question->createdby = $questiondata->createdby; 857 $question->modifiedby = $questiondata->modifiedby; 858 859 // Fill extra question fields values. 860 $extraquestionfields = $this->extra_question_fields(); 861 if (is_array($extraquestionfields)) { 862 // Omit table name. 863 array_shift($extraquestionfields); 864 foreach ($extraquestionfields as $field) { 865 $question->$field = $questiondata->options->$field; 866 } 867 } 868 869 $this->initialise_question_hints($question, $questiondata); 870 } 871 872 /** 873 * Initialise question_definition::hints field. 874 * @param question_definition $question the question_definition we are creating. 875 * @param object $questiondata the question data loaded from the database. 876 */ 877 protected function initialise_question_hints(question_definition $question, $questiondata) { 878 if (empty($questiondata->hints)) { 879 return; 880 } 881 foreach ($questiondata->hints as $hint) { 882 $question->hints[] = $this->make_hint($hint); 883 } 884 } 885 886 /** 887 * Create a question_hint, or an appropriate subclass for this question, 888 * from a row loaded from the database. 889 * @param object $hint the DB row from the question hints table. 890 * @return question_hint 891 */ 892 protected function make_hint($hint) { 893 return question_hint::load_from_record($hint); 894 } 895 896 /** 897 * Initialise the combined feedback fields. 898 * @param question_definition $question the question_definition we are creating. 899 * @param object $questiondata the question data loaded from the database. 900 * @param bool $withparts whether to set the shownumcorrect field. 901 */ 902 protected function initialise_combined_feedback(question_definition $question, 903 $questiondata, $withparts = false) { 904 $question->correctfeedback = $questiondata->options->correctfeedback; 905 $question->correctfeedbackformat = $questiondata->options->correctfeedbackformat; 906 $question->partiallycorrectfeedback = $questiondata->options->partiallycorrectfeedback; 907 $question->partiallycorrectfeedbackformat = 908 $questiondata->options->partiallycorrectfeedbackformat; 909 $question->incorrectfeedback = $questiondata->options->incorrectfeedback; 910 $question->incorrectfeedbackformat = $questiondata->options->incorrectfeedbackformat; 911 if ($withparts) { 912 $question->shownumcorrect = $questiondata->options->shownumcorrect; 913 } 914 } 915 916 /** 917 * Initialise question_definition::answers field. 918 * @param question_definition $question the question_definition we are creating. 919 * @param object $questiondata the question data loaded from the database. 920 * @param bool $forceplaintextanswers most qtypes assume that answers are 921 * FORMAT_PLAIN, and dont use the answerformat DB column (it contains 922 * the default 0 = FORMAT_MOODLE). Therefore, by default this method 923 * ingores answerformat. Pass false here to use answerformat. For example 924 * multichoice does this. 925 */ 926 protected function initialise_question_answers(question_definition $question, 927 $questiondata, $forceplaintextanswers = true) { 928 $question->answers = array(); 929 if (empty($questiondata->options->answers)) { 930 return; 931 } 932 foreach ($questiondata->options->answers as $a) { 933 $question->answers[$a->id] = $this->make_answer($a); 934 if (!$forceplaintextanswers) { 935 $question->answers[$a->id]->answerformat = $a->answerformat; 936 } 937 } 938 } 939 940 /** 941 * Create a question_answer, or an appropriate subclass for this question, 942 * from a row loaded from the database. 943 * @param object $answer the DB row from the question_answers table plus extra answer fields. 944 * @return question_answer 945 */ 946 protected function make_answer($answer) { 947 return new question_answer($answer->id, $answer->answer, 948 $answer->fraction, $answer->feedback, $answer->feedbackformat); 949 } 950 951 /** 952 * Deletes the question-type specific data when a question is deleted. 953 * @param int $question the question being deleted. 954 * @param int $contextid the context this quesiotn belongs to. 955 */ 956 public function delete_question($questionid, $contextid) { 957 global $DB; 958 959 $this->delete_files($questionid, $contextid); 960 961 $extraquestionfields = $this->extra_question_fields(); 962 if (is_array($extraquestionfields)) { 963 $question_extension_table = array_shift($extraquestionfields); 964 $DB->delete_records($question_extension_table, 965 array($this->questionid_column_name() => $questionid)); 966 } 967 968 $extraanswerfields = $this->extra_answer_fields(); 969 if (is_array($extraanswerfields)) { 970 $answer_extension_table = array_shift($extraanswerfields); 971 $DB->delete_records_select($answer_extension_table, 972 'answerid IN (SELECT qa.id FROM {question_answers} qa WHERE qa.question = ?)', 973 array($questionid)); 974 } 975 976 $DB->delete_records('question_answers', array('question' => $questionid)); 977 978 $DB->delete_records('question_hints', array('questionid' => $questionid)); 979 } 980 981 /** 982 * Returns the number of question numbers which are used by the question 983 * 984 * This function returns the number of question numbers to be assigned 985 * to the question. Most question types will have length one; they will be 986 * assigned one number. The 'description' type, however does not use up a 987 * number and so has a length of zero. Other question types may wish to 988 * handle a bundle of questions and hence return a number greater than one. 989 * @return int The number of question numbers which should be 990 * assigned to the question. 991 * @param object $question The question whose length is to be determined. 992 * Question type specific information is included. 993 */ 994 public function actual_number_of_questions($question) { 995 // By default, each question is given one number. 996 return 1; 997 } 998 999 /** 1000 * @param object $question 1001 * @return number|null either a fraction estimating what the student would 1002 * score by guessing, or null, if it is not possible to estimate. 1003 */ 1004 public function get_random_guess_score($questiondata) { 1005 return 0; 1006 } 1007 1008 /** 1009 * Whether or not to break down question stats and response analysis, for a question defined by $questiondata. 1010 * 1011 * @param object $questiondata The full question definition data. 1012 * @return bool 1013 */ 1014 public function break_down_stats_and_response_analysis_by_variant($questiondata) { 1015 return true; 1016 } 1017 1018 /** 1019 * This method should return all the possible types of response that are 1020 * recognised for this question. 1021 * 1022 * The question is modelled as comprising one or more subparts. For each 1023 * subpart, there are one or more classes that that students response 1024 * might fall into, each of those classes earning a certain score. 1025 * 1026 * For example, in a shortanswer question, there is only one subpart, the 1027 * text entry field. The response the student gave will be classified according 1028 * to which of the possible $question->options->answers it matches. 1029 * 1030 * For the matching question type, there will be one subpart for each 1031 * question stem, and for each stem, each of the possible choices is a class 1032 * of student's response. 1033 * 1034 * A response is an object with two fields, ->responseclass is a string 1035 * presentation of that response, and ->fraction, the credit for a response 1036 * in that class. 1037 * 1038 * Array keys have no specific meaning, but must be unique, and must be 1039 * the same if this function is called repeatedly. 1040 * 1041 * @param object $question the question definition data. 1042 * @return array keys are subquestionid, values are arrays of possible 1043 * responses to that subquestion. 1044 */ 1045 public function get_possible_responses($questiondata) { 1046 return array(); 1047 } 1048 1049 /** 1050 * Utility method used by {@link qtype_renderer::head_code()}. It looks 1051 * for any of the files script.js or script.php that exist in the plugin 1052 * folder and ensures they get included. 1053 */ 1054 public function find_standard_scripts() { 1055 global $PAGE; 1056 1057 $plugindir = $this->plugin_dir(); 1058 $plugindirrel = 'question/type/' . $this->name(); 1059 1060 if (file_exists($plugindir . '/script.js')) { 1061 $PAGE->requires->js('/' . $plugindirrel . '/script.js'); 1062 } 1063 if (file_exists($plugindir . '/script.php')) { 1064 $PAGE->requires->js('/' . $plugindirrel . '/script.php'); 1065 } 1066 } 1067 1068 /** 1069 * Returns true if the editing wizard is finished, false otherwise. 1070 * 1071 * The default implementation returns true, which is suitable for all question- 1072 * types that only use one editing form. This function is used in 1073 * question.php to decide whether we can regrade any states of the edited 1074 * question and redirect to edit.php. 1075 * 1076 * The dataset dependent question-type, which is extended by the calculated 1077 * question-type, overwrites this method because it uses multiple pages (i.e. 1078 * a wizard) to set up the question and associated datasets. 1079 * 1080 * @param object $form The data submitted by the previous page. 1081 * 1082 * @return bool Whether the wizard's last page was submitted or not. 1083 */ 1084 public function finished_edit_wizard($form) { 1085 // In the default case there is only one edit page. 1086 return true; 1087 } 1088 1089 // IMPORT/EXPORT FUNCTIONS --------------------------------- . 1090 1091 /* 1092 * Imports question from the Moodle XML format 1093 * 1094 * Imports question using information from extra_question_fields function 1095 * If some of you fields contains id's you'll need to reimplement this 1096 */ 1097 public function import_from_xml($data, $question, qformat_xml $format, $extra=null) { 1098 $question_type = $data['@']['type']; 1099 if ($question_type != $this->name()) { 1100 return false; 1101 } 1102 1103 $extraquestionfields = $this->extra_question_fields(); 1104 if (!is_array($extraquestionfields)) { 1105 return false; 1106 } 1107 1108 // Omit table name. 1109 array_shift($extraquestionfields); 1110 $qo = $format->import_headers($data); 1111 $qo->qtype = $question_type; 1112 1113 foreach ($extraquestionfields as $field) { 1114 $qo->$field = $format->getpath($data, array('#', $field, 0, '#'), ''); 1115 } 1116 1117 // Run through the answers. 1118 $answers = $data['#']['answer']; 1119 $a_count = 0; 1120 $extraanswersfields = $this->extra_answer_fields(); 1121 if (is_array($extraanswersfields)) { 1122 array_shift($extraanswersfields); 1123 } 1124 foreach ($answers as $answer) { 1125 $ans = $format->import_answer($answer); 1126 if (!$this->has_html_answers()) { 1127 $qo->answer[$a_count] = $ans->answer['text']; 1128 } else { 1129 $qo->answer[$a_count] = $ans->answer; 1130 } 1131 $qo->fraction[$a_count] = $ans->fraction; 1132 $qo->feedback[$a_count] = $ans->feedback; 1133 if (is_array($extraanswersfields)) { 1134 foreach ($extraanswersfields as $field) { 1135 $qo->{$field}[$a_count] = 1136 $format->getpath($answer, array('#', $field, 0, '#'), ''); 1137 } 1138 } 1139 ++$a_count; 1140 } 1141 return $qo; 1142 } 1143 1144 /* 1145 * Export question to the Moodle XML format 1146 * 1147 * Export question using information from extra_question_fields function 1148 * If some of you fields contains id's you'll need to reimplement this 1149 */ 1150 public function export_to_xml($question, qformat_xml $format, $extra=null) { 1151 $extraquestionfields = $this->extra_question_fields(); 1152 if (!is_array($extraquestionfields)) { 1153 return false; 1154 } 1155 1156 // Omit table name. 1157 array_shift($extraquestionfields); 1158 $expout=''; 1159 foreach ($extraquestionfields as $field) { 1160 $exportedvalue = $format->xml_escape($question->options->$field); 1161 $expout .= " <{$field}>{$exportedvalue}</{$field}>\n"; 1162 } 1163 1164 $extraanswersfields = $this->extra_answer_fields(); 1165 if (is_array($extraanswersfields)) { 1166 array_shift($extraanswersfields); 1167 } 1168 foreach ($question->options->answers as $answer) { 1169 $extra = ''; 1170 if (is_array($extraanswersfields)) { 1171 foreach ($extraanswersfields as $field) { 1172 $exportedvalue = $format->xml_escape($answer->$field); 1173 $extra .= " <{$field}>{$exportedvalue}</{$field}>\n"; 1174 } 1175 } 1176 1177 $expout .= $format->write_answer($answer, $extra); 1178 } 1179 return $expout; 1180 } 1181 1182 /** 1183 * Abstract function implemented by each question type. It runs all the code 1184 * required to set up and save a question of any type for testing purposes. 1185 * Alternate DB table prefix may be used to facilitate data deletion. 1186 */ 1187 public function generate_test($name, $courseid=null) { 1188 $form = new stdClass(); 1189 $form->name = $name; 1190 $form->questiontextformat = 1; 1191 $form->questiontext = 'test question, generated by script'; 1192 $form->defaultmark = 1; 1193 $form->penalty = 0.3333333; 1194 $form->generalfeedback = "Well done"; 1195 1196 $context = context_course::instance($courseid); 1197 $newcategory = question_make_default_categories(array($context)); 1198 $form->category = $newcategory->id . ',1'; 1199 1200 $question = new stdClass(); 1201 $question->courseid = $courseid; 1202 $question->qtype = $this->qtype; 1203 return array($form, $question); 1204 } 1205 1206 /** 1207 * Get question context by category id 1208 * @param int $category 1209 * @return object $context 1210 */ 1211 protected function get_context_by_category_id($category) { 1212 global $DB; 1213 $contextid = $DB->get_field('question_categories', 'contextid', array('id'=>$category)); 1214 $context = context::instance_by_id($contextid, IGNORE_MISSING); 1215 return $context; 1216 } 1217 1218 /** 1219 * Save the file belonging to one text field. 1220 * 1221 * @param array $field the data from the form (or from import). This will 1222 * normally have come from the formslib editor element, so it will be an 1223 * array with keys 'text', 'format' and 'itemid'. However, when we are 1224 * importing, it will be an array with keys 'text', 'format' and 'files' 1225 * @param object $context the context the question is in. 1226 * @param string $component indentifies the file area question. 1227 * @param string $filearea indentifies the file area questiontext, 1228 * generalfeedback, answerfeedback, etc. 1229 * @param int $itemid identifies the file area. 1230 * 1231 * @return string the text for this field, after files have been processed. 1232 */ 1233 protected function import_or_save_files($field, $context, $component, $filearea, $itemid) { 1234 if (!empty($field['itemid'])) { 1235 // This is the normal case. We are safing the questions editing form. 1236 return file_save_draft_area_files($field['itemid'], $context->id, $component, 1237 $filearea, $itemid, $this->fileoptions, trim($field['text'])); 1238 1239 } else if (!empty($field['files'])) { 1240 // This is the case when we are doing an import. 1241 foreach ($field['files'] as $file) { 1242 $this->import_file($context, $component, $filearea, $itemid, $file); 1243 } 1244 } 1245 return trim($field['text']); 1246 } 1247 1248 /** 1249 * Move all the files belonging to this question from one context to another. 1250 * @param int $questionid the question being moved. 1251 * @param int $oldcontextid the context it is moving from. 1252 * @param int $newcontextid the context it is moving to. 1253 */ 1254 public function move_files($questionid, $oldcontextid, $newcontextid) { 1255 $fs = get_file_storage(); 1256 $fs->move_area_files_to_new_context($oldcontextid, 1257 $newcontextid, 'question', 'questiontext', $questionid); 1258 $fs->move_area_files_to_new_context($oldcontextid, 1259 $newcontextid, 'question', 'generalfeedback', $questionid); 1260 } 1261 1262 /** 1263 * Move all the files belonging to this question's answers when the question 1264 * is moved from one context to another. 1265 * @param int $questionid the question being moved. 1266 * @param int $oldcontextid the context it is moving from. 1267 * @param int $newcontextid the context it is moving to. 1268 * @param bool $answerstoo whether there is an 'answer' question area, 1269 * as well as an 'answerfeedback' one. Default false. 1270 */ 1271 protected function move_files_in_answers($questionid, $oldcontextid, 1272 $newcontextid, $answerstoo = false) { 1273 global $DB; 1274 $fs = get_file_storage(); 1275 1276 $answerids = $DB->get_records_menu('question_answers', 1277 array('question' => $questionid), 'id', 'id,1'); 1278 foreach ($answerids as $answerid => $notused) { 1279 if ($answerstoo) { 1280 $fs->move_area_files_to_new_context($oldcontextid, 1281 $newcontextid, 'question', 'answer', $answerid); 1282 } 1283 $fs->move_area_files_to_new_context($oldcontextid, 1284 $newcontextid, 'question', 'answerfeedback', $answerid); 1285 } 1286 } 1287 1288 /** 1289 * Move all the files belonging to this question's hints when the question 1290 * is moved from one context to another. 1291 * @param int $questionid the question being moved. 1292 * @param int $oldcontextid the context it is moving from. 1293 * @param int $newcontextid the context it is moving to. 1294 * @param bool $answerstoo whether there is an 'answer' question area, 1295 * as well as an 'answerfeedback' one. Default false. 1296 */ 1297 protected function move_files_in_hints($questionid, $oldcontextid, $newcontextid) { 1298 global $DB; 1299 $fs = get_file_storage(); 1300 1301 $hintids = $DB->get_records_menu('question_hints', 1302 array('questionid' => $questionid), 'id', 'id,1'); 1303 foreach ($hintids as $hintid => $notused) { 1304 $fs->move_area_files_to_new_context($oldcontextid, 1305 $newcontextid, 'question', 'hint', $hintid); 1306 } 1307 } 1308 1309 /** 1310 * Move all the files belonging to this question's answers when the question 1311 * is moved from one context to another. 1312 * @param int $questionid the question being moved. 1313 * @param int $oldcontextid the context it is moving from. 1314 * @param int $newcontextid the context it is moving to. 1315 * @param bool $answerstoo whether there is an 'answer' question area, 1316 * as well as an 'answerfeedback' one. Default false. 1317 */ 1318 protected function move_files_in_combined_feedback($questionid, $oldcontextid, 1319 $newcontextid) { 1320 global $DB; 1321 $fs = get_file_storage(); 1322 1323 $fs->move_area_files_to_new_context($oldcontextid, 1324 $newcontextid, 'question', 'correctfeedback', $questionid); 1325 $fs->move_area_files_to_new_context($oldcontextid, 1326 $newcontextid, 'question', 'partiallycorrectfeedback', $questionid); 1327 $fs->move_area_files_to_new_context($oldcontextid, 1328 $newcontextid, 'question', 'incorrectfeedback', $questionid); 1329 } 1330 1331 /** 1332 * Delete all the files belonging to this question. 1333 * @param int $questionid the question being deleted. 1334 * @param int $contextid the context the question is in. 1335 */ 1336 protected function delete_files($questionid, $contextid) { 1337 $fs = get_file_storage(); 1338 $fs->delete_area_files($contextid, 'question', 'questiontext', $questionid); 1339 $fs->delete_area_files($contextid, 'question', 'generalfeedback', $questionid); 1340 } 1341 1342 /** 1343 * Delete all the files belonging to this question's answers. 1344 * @param int $questionid the question being deleted. 1345 * @param int $contextid the context the question is in. 1346 * @param bool $answerstoo whether there is an 'answer' question area, 1347 * as well as an 'answerfeedback' one. Default false. 1348 */ 1349 protected function delete_files_in_answers($questionid, $contextid, $answerstoo = false) { 1350 global $DB; 1351 $fs = get_file_storage(); 1352 1353 $answerids = $DB->get_records_menu('question_answers', 1354 array('question' => $questionid), 'id', 'id,1'); 1355 foreach ($answerids as $answerid => $notused) { 1356 if ($answerstoo) { 1357 $fs->delete_area_files($contextid, 'question', 'answer', $answerid); 1358 } 1359 $fs->delete_area_files($contextid, 'question', 'answerfeedback', $answerid); 1360 } 1361 } 1362 1363 /** 1364 * Delete all the files belonging to this question's hints. 1365 * @param int $questionid the question being deleted. 1366 * @param int $contextid the context the question is in. 1367 */ 1368 protected function delete_files_in_hints($questionid, $contextid) { 1369 global $DB; 1370 $fs = get_file_storage(); 1371 1372 $hintids = $DB->get_records_menu('question_hints', 1373 array('questionid' => $questionid), 'id', 'id,1'); 1374 foreach ($hintids as $hintid => $notused) { 1375 $fs->delete_area_files($contextid, 'question', 'hint', $hintid); 1376 } 1377 } 1378 1379 /** 1380 * Delete all the files belonging to this question's answers. 1381 * @param int $questionid the question being deleted. 1382 * @param int $contextid the context the question is in. 1383 * @param bool $answerstoo whether there is an 'answer' question area, 1384 * as well as an 'answerfeedback' one. Default false. 1385 */ 1386 protected function delete_files_in_combined_feedback($questionid, $contextid) { 1387 global $DB; 1388 $fs = get_file_storage(); 1389 1390 $fs->delete_area_files($contextid, 1391 'question', 'correctfeedback', $questionid); 1392 $fs->delete_area_files($contextid, 1393 'question', 'partiallycorrectfeedback', $questionid); 1394 $fs->delete_area_files($contextid, 1395 'question', 'incorrectfeedback', $questionid); 1396 } 1397 1398 public function import_file($context, $component, $filearea, $itemid, $file) { 1399 $fs = get_file_storage(); 1400 $record = new stdClass(); 1401 if (is_object($context)) { 1402 $record->contextid = $context->id; 1403 } else { 1404 $record->contextid = $context; 1405 } 1406 $record->component = $component; 1407 $record->filearea = $filearea; 1408 $record->itemid = $itemid; 1409 $record->filename = $file->name; 1410 $record->filepath = '/'; 1411 return $fs->create_file_from_string($record, $this->decode_file($file)); 1412 } 1413 1414 protected function decode_file($file) { 1415 switch ($file->encoding) { 1416 case 'base64': 1417 default: 1418 return base64_decode($file->content); 1419 } 1420 } 1421 } 1422 1423 1424 /** 1425 * This class is used in the return value from 1426 * {@link question_type::get_possible_responses()}. 1427 * 1428 * @copyright 2010 The Open University 1429 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 1430 */ 1431 class question_possible_response { 1432 /** 1433 * @var string the classification of this response the student gave to this 1434 * part of the question. Must match one of the responseclasses returned by 1435 * {@link question_type::get_possible_responses()}. 1436 */ 1437 public $responseclass; 1438 1439 /** @var string the (partial) credit awarded for this responses. */ 1440 public $fraction; 1441 1442 /** 1443 * Constructor, just an easy way to set the fields. 1444 * @param string $responseclassid see the field descriptions above. 1445 * @param string $response see the field descriptions above. 1446 * @param number $fraction see the field descriptions above. 1447 */ 1448 public function __construct($responseclass, $fraction) { 1449 $this->responseclass = $responseclass; 1450 $this->fraction = $fraction; 1451 } 1452 1453 public static function no_response() { 1454 return new question_possible_response(get_string('noresponse', 'question'), 0); 1455 } 1456 }
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 |