Differences Between: [Versions 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * 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 * Get extra actions for a question of this type to add to the question bank edit menu. 238 * 239 * This method is called if the {@link edit_menu_column} is being used in the 240 * question bank, which it is by default since Moodle 3.8. If applicable for 241 * your question type, you can return arn array of {@link action_menu_link}s. 242 * These will be added at the end of the Edit menu for this question. 243 * 244 * The $question object passed in will have a hard-to-predict set of fields, 245 * because the fields present depend on which columns are included in the 246 * question bank view. However, you can rely on 'id', 'createdby', 247 * 'contextid', 'hidden' and 'category' (id) being present, and so you 248 * can call question_has_capability_on without causing performance problems. 249 * 250 * @param stdClass $question the available information about the particular question the action is for. 251 * @return action_menu_link[] any actions you want to add to the Edit menu for this question. 252 */ 253 public function get_extra_question_bank_actions(stdClass $question): array { 254 return []; 255 } 256 257 /** 258 * This method should be overriden if you want to include a special heading or some other 259 * html on a question editing page besides the question editing form. 260 * 261 * @param question_edit_form $mform a child of question_edit_form 262 * @param object $question 263 * @param string $wizardnow is '' for first page. 264 */ 265 public function display_question_editing_page($mform, $question, $wizardnow) { 266 global $OUTPUT; 267 $heading = $this->get_heading(empty($question->id)); 268 echo $OUTPUT->heading_with_help($heading, 'pluginname', $this->plugin_name()); 269 $mform->display(); 270 } 271 272 /** 273 * Method called by display_question_editing_page and by question.php to get 274 * heading for breadcrumbs. 275 * 276 * @return string the heading 277 */ 278 public function get_heading($adding = false) { 279 if ($adding) { 280 $string = 'pluginnameadding'; 281 } else { 282 $string = 'pluginnameediting'; 283 } 284 return get_string($string, $this->plugin_name()); 285 } 286 287 /** 288 * Set any missing settings for this question to the default values. This is 289 * called before displaying the question editing form. 290 * 291 * @param object $questiondata the question data, loaded from the databsae, 292 * or more likely a newly created question object that is only partially 293 * initialised. 294 */ 295 public function set_default_options($questiondata) { 296 } 297 298 /** 299 * Saves (creates or updates) a question. 300 * 301 * Given some question info and some data about the answers 302 * this function parses, organises and saves the question 303 * It is used by {@link question.php} when saving new data from 304 * a form, and also by {@link import.php} when importing questions 305 * This function in turn calls {@link save_question_options} 306 * to save question-type specific data. 307 * 308 * Whether we are saving a new question or updating an existing one can be 309 * determined by testing !empty($question->id). If it is not empty, we are updating. 310 * 311 * The question will be saved in category $form->category. 312 * 313 * @param object $question the question object which should be updated. For a 314 * new question will be mostly empty. 315 * @param object $form the object containing the information to save, as if 316 * from the question editing form. 317 * @param object $course not really used any more. 318 * @return object On success, return the new question object. On failure, 319 * return an object as follows. If the error object has an errors field, 320 * display that as an error message. Otherwise, the editing form will be 321 * redisplayed with validation errors, from validation_errors field, which 322 * is itself an object, shown next to the form fields. (I don't think this 323 * is accurate any more.) 324 */ 325 public function save_question($question, $form) { 326 global $USER, $DB; 327 328 // The actual update/insert done with multiple DB access, so we do it in a transaction. 329 $transaction = $DB->start_delegated_transaction (); 330 331 list($question->category) = explode(',', $form->category); 332 $context = $this->get_context_by_category_id($question->category); 333 334 // This default implementation is suitable for most 335 // question types. 336 337 // First, save the basic question itself. 338 $question->name = trim($form->name); 339 $question->parent = isset($form->parent) ? $form->parent : 0; 340 $question->length = $this->actual_number_of_questions($question); 341 $question->penalty = isset($form->penalty) ? $form->penalty : 0; 342 343 // The trim call below has the effect of casting any strange values received, 344 // like null or false, to an appropriate string, so we only need to test for 345 // missing values. Be careful not to break the value '0' here. 346 if (!isset($form->questiontext['text'])) { 347 $question->questiontext = ''; 348 } else { 349 $question->questiontext = trim($form->questiontext['text']); 350 } 351 $question->questiontextformat = !empty($form->questiontext['format']) ? 352 $form->questiontext['format'] : 0; 353 354 if (empty($form->generalfeedback['text'])) { 355 $question->generalfeedback = ''; 356 } else { 357 $question->generalfeedback = trim($form->generalfeedback['text']); 358 } 359 $question->generalfeedbackformat = !empty($form->generalfeedback['format']) ? 360 $form->generalfeedback['format'] : 0; 361 362 if ($question->name === '') { 363 $question->name = shorten_text(strip_tags($form->questiontext['text']), 15); 364 if ($question->name === '') { 365 $question->name = '-'; 366 } 367 } 368 369 if ($question->penalty > 1 or $question->penalty < 0) { 370 $question->errors['penalty'] = get_string('invalidpenalty', 'question'); 371 } 372 373 if (isset($form->defaultmark)) { 374 $question->defaultmark = $form->defaultmark; 375 } 376 377 if (isset($form->idnumber)) { 378 if ((string) $form->idnumber === '') { 379 $question->idnumber = null; 380 } else { 381 // While this check already exists in the form validation, 382 // this is a backstop preventing unnecessary errors. 383 // Only set the idnumber if it has changed and will not cause a unique index violation. 384 if (strpos($form->category, ',') !== false) { 385 list($category, $categorycontextid) = explode(',', $form->category); 386 } else { 387 $category = $form->category; 388 } 389 if (!$DB->record_exists('question', 390 ['idnumber' => $form->idnumber, 'category' => $category])) { 391 $question->idnumber = $form->idnumber; 392 } 393 } 394 } 395 396 // If the question is new, create it. 397 $newquestion = false; 398 if (empty($question->id)) { 399 // Set the unique code. 400 $question->stamp = make_unique_id_code(); 401 $question->createdby = $USER->id; 402 $question->timecreated = time(); 403 $question->id = $DB->insert_record('question', $question); 404 $newquestion = true; 405 } 406 407 // Now, whether we are updating a existing question, or creating a new 408 // one, we have to do the files processing and update the record. 409 // Question already exists, update. 410 $question->modifiedby = $USER->id; 411 $question->timemodified = time(); 412 413 if (!empty($question->questiontext) && !empty($form->questiontext['itemid'])) { 414 $question->questiontext = file_save_draft_area_files($form->questiontext['itemid'], 415 $context->id, 'question', 'questiontext', (int)$question->id, 416 $this->fileoptions, $question->questiontext); 417 } 418 if (!empty($question->generalfeedback) && !empty($form->generalfeedback['itemid'])) { 419 $question->generalfeedback = file_save_draft_area_files( 420 $form->generalfeedback['itemid'], $context->id, 421 'question', 'generalfeedback', (int)$question->id, 422 $this->fileoptions, $question->generalfeedback); 423 } 424 $DB->update_record('question', $question); 425 426 // Now to save all the answers and type-specific options. 427 $form->id = $question->id; 428 $form->qtype = $question->qtype; 429 $form->category = $question->category; 430 $form->questiontext = $question->questiontext; 431 $form->questiontextformat = $question->questiontextformat; 432 // Current context. 433 $form->context = $context; 434 435 $result = $this->save_question_options($form); 436 437 if (!empty($result->error)) { 438 print_error($result->error); 439 } 440 441 if (!empty($result->notice)) { 442 notice($result->notice, "question.php?id={$question->id}"); 443 } 444 445 if (!empty($result->noticeyesno)) { 446 throw new coding_exception( 447 '$result->noticeyesno no longer supported in save_question.'); 448 } 449 450 // Give the question a unique version stamp determined by question_hash(). 451 $DB->set_field('question', 'version', question_hash($question), 452 array('id' => $question->id)); 453 454 if ($newquestion) { 455 // Log the creation of this question. 456 $event = \core\event\question_created::create_from_question_instance($question, $context); 457 $event->trigger(); 458 } else { 459 // Log the update of this question. 460 $event = \core\event\question_updated::create_from_question_instance($question, $context); 461 $event->trigger(); 462 } 463 464 $transaction->allow_commit(); 465 466 return $question; 467 } 468 469 /** 470 * Saves question-type specific options 471 * 472 * This is called by {@link save_question()} to save the question-type specific data 473 * @return object $result->error or $result->notice 474 * @param object $question This holds the information from the editing form, 475 * it is not a standard question object. 476 */ 477 public function save_question_options($question) { 478 global $DB; 479 $extraquestionfields = $this->extra_question_fields(); 480 481 if (is_array($extraquestionfields)) { 482 $question_extension_table = array_shift($extraquestionfields); 483 484 $function = 'update_record'; 485 $questionidcolname = $this->questionid_column_name(); 486 $options = $DB->get_record($question_extension_table, 487 array($questionidcolname => $question->id)); 488 if (!$options) { 489 $function = 'insert_record'; 490 $options = new stdClass(); 491 $options->$questionidcolname = $question->id; 492 } 493 foreach ($extraquestionfields as $field) { 494 if (property_exists($question, $field)) { 495 $options->$field = $question->$field; 496 } 497 } 498 499 $DB->{$function}($question_extension_table, $options); 500 } 501 } 502 503 /** 504 * Save the answers, with any extra data. 505 * 506 * Questions that use answers will call it from {@link save_question_options()}. 507 * @param object $question This holds the information from the editing form, 508 * it is not a standard question object. 509 * @return object $result->error or $result->notice 510 */ 511 public function save_question_answers($question) { 512 global $DB; 513 514 $context = $question->context; 515 $oldanswers = $DB->get_records('question_answers', 516 array('question' => $question->id), 'id ASC'); 517 518 // We need separate arrays for answers and extra answer data, so no JOINS there. 519 $extraanswerfields = $this->extra_answer_fields(); 520 $isextraanswerfields = is_array($extraanswerfields); 521 $extraanswertable = ''; 522 $oldanswerextras = array(); 523 if ($isextraanswerfields) { 524 $extraanswertable = array_shift($extraanswerfields); 525 if (!empty($oldanswers)) { 526 $oldanswerextras = $DB->get_records_sql("SELECT * FROM {{$extraanswertable}} WHERE " . 527 'answerid IN (SELECT id FROM {question_answers} WHERE question = ' . $question->id . ')' ); 528 } 529 } 530 531 // Insert all the new answers. 532 foreach ($question->answer as $key => $answerdata) { 533 // Check for, and ignore, completely blank answer from the form. 534 if ($this->is_answer_empty($question, $key)) { 535 continue; 536 } 537 538 // Update an existing answer if possible. 539 $answer = array_shift($oldanswers); 540 if (!$answer) { 541 $answer = new stdClass(); 542 $answer->question = $question->id; 543 $answer->answer = ''; 544 $answer->feedback = ''; 545 $answer->id = $DB->insert_record('question_answers', $answer); 546 } 547 548 $answer = $this->fill_answer_fields($answer, $question, $key, $context); 549 $DB->update_record('question_answers', $answer); 550 551 if ($isextraanswerfields) { 552 // Check, if this answer contains some extra field data. 553 if ($this->is_extra_answer_fields_empty($question, $key)) { 554 continue; 555 } 556 557 $answerextra = array_shift($oldanswerextras); 558 if (!$answerextra) { 559 $answerextra = new stdClass(); 560 $answerextra->answerid = $answer->id; 561 // Avoid looking for correct default for any possible DB field type 562 // by setting real values. 563 $answerextra = $this->fill_extra_answer_fields($answerextra, $question, $key, $context, $extraanswerfields); 564 $answerextra->id = $DB->insert_record($extraanswertable, $answerextra); 565 } else { 566 // Update answerid, as record may be reused from another answer. 567 $answerextra->answerid = $answer->id; 568 $answerextra = $this->fill_extra_answer_fields($answerextra, $question, $key, $context, $extraanswerfields); 569 $DB->update_record($extraanswertable, $answerextra); 570 } 571 } 572 } 573 574 if ($isextraanswerfields) { 575 // Delete any left over extra answer fields records. 576 $oldanswerextraids = array(); 577 foreach ($oldanswerextras as $oldextra) { 578 $oldanswerextraids[] = $oldextra->id; 579 } 580 $DB->delete_records_list($extraanswertable, 'id', $oldanswerextraids); 581 } 582 583 // Delete any left over old answer records. 584 $fs = get_file_storage(); 585 foreach ($oldanswers as $oldanswer) { 586 $fs->delete_area_files($context->id, 'question', 'answerfeedback', $oldanswer->id); 587 $DB->delete_records('question_answers', array('id' => $oldanswer->id)); 588 } 589 } 590 591 /** 592 * Returns true is answer with the $key is empty in the question data and should not be saved in DB. 593 * 594 * The questions using question_answers table may want to overload this. Default code will work 595 * for shortanswer and similar question types. 596 * @param object $questiondata This holds the information from the question editing form or import. 597 * @param int $key A key of the answer in question. 598 * @return bool True if answer shouldn't be saved in DB. 599 */ 600 protected function is_answer_empty($questiondata, $key) { 601 return trim($questiondata->answer[$key]) == '' && $questiondata->fraction[$key] == 0 && 602 html_is_blank($questiondata->feedback[$key]['text']); 603 } 604 605 /** 606 * Return $answer, filling necessary fields for the question_answers table. 607 * 608 * The questions using question_answers table may want to overload this. Default code will work 609 * for shortanswer and similar question types. 610 * @param stdClass $answer Object to save data. 611 * @param object $questiondata This holds the information from the question editing form or import. 612 * @param int $key A key of the answer in question. 613 * @param object $context needed for working with files. 614 * @return $answer answer with filled data. 615 */ 616 protected function fill_answer_fields($answer, $questiondata, $key, $context) { 617 $answer->answer = $questiondata->answer[$key]; 618 $answer->fraction = $questiondata->fraction[$key]; 619 $answer->feedback = $this->import_or_save_files($questiondata->feedback[$key], 620 $context, 'question', 'answerfeedback', $answer->id); 621 $answer->feedbackformat = $questiondata->feedback[$key]['format']; 622 return $answer; 623 } 624 625 /** 626 * Returns true if extra answer fields for answer with the $key is empty 627 * in the question data and should not be saved in DB. 628 * 629 * Questions where extra answer fields are optional will want to overload this. 630 * @param object $questiondata This holds the information from the question editing form or import. 631 * @param int $key A key of the answer in question. 632 * @return bool True if extra answer data shouldn't be saved in DB. 633 */ 634 protected function is_extra_answer_fields_empty($questiondata, $key) { 635 // No extra answer data in base class. 636 return true; 637 } 638 639 /** 640 * Return $answerextra, filling necessary fields for the extra answer fields table. 641 * 642 * The questions may want to overload it to save files or do other data processing. 643 * @param stdClass $answerextra Object to save data. 644 * @param object $questiondata This holds the information from the question editing form or import. 645 * @param int $key A key of the answer in question. 646 * @param object $context needed for working with files. 647 * @param array $extraanswerfields extra answer fields (without table name). 648 * @return $answer answerextra with filled data. 649 */ 650 protected function fill_extra_answer_fields($answerextra, $questiondata, $key, $context, $extraanswerfields) { 651 foreach ($extraanswerfields as $field) { 652 // The $questiondata->$field[$key] won't work in PHP, break it down to two strings of code. 653 $fieldarray = $questiondata->$field; 654 $answerextra->$field = $fieldarray[$key]; 655 } 656 return $answerextra; 657 } 658 659 public function save_hints($formdata, $withparts = false) { 660 global $DB; 661 $context = $formdata->context; 662 663 $oldhints = $DB->get_records('question_hints', 664 array('questionid' => $formdata->id), 'id ASC'); 665 666 667 $numhints = $this->count_hints_on_form($formdata, $withparts); 668 669 for ($i = 0; $i < $numhints; $i += 1) { 670 if (html_is_blank($formdata->hint[$i]['text'])) { 671 $formdata->hint[$i]['text'] = ''; 672 } 673 674 if ($withparts) { 675 $clearwrong = !empty($formdata->hintclearwrong[$i]); 676 $shownumcorrect = !empty($formdata->hintshownumcorrect[$i]); 677 } 678 679 if ($this->is_hint_empty_in_form_data($formdata, $i, $withparts)) { 680 continue; 681 } 682 683 // Update an existing hint if possible. 684 $hint = array_shift($oldhints); 685 if (!$hint) { 686 $hint = new stdClass(); 687 $hint->questionid = $formdata->id; 688 $hint->hint = ''; 689 $hint->id = $DB->insert_record('question_hints', $hint); 690 } 691 692 $hint->hint = $this->import_or_save_files($formdata->hint[$i], 693 $context, 'question', 'hint', $hint->id); 694 $hint->hintformat = $formdata->hint[$i]['format']; 695 if ($withparts) { 696 $hint->clearwrong = $clearwrong; 697 $hint->shownumcorrect = $shownumcorrect; 698 } 699 $hint->options = $this->save_hint_options($formdata, $i, $withparts); 700 $DB->update_record('question_hints', $hint); 701 } 702 703 // Delete any remaining old hints. 704 $fs = get_file_storage(); 705 foreach ($oldhints as $oldhint) { 706 $fs->delete_area_files($context->id, 'question', 'hint', $oldhint->id); 707 $DB->delete_records('question_hints', array('id' => $oldhint->id)); 708 } 709 } 710 711 /** 712 * Count number of hints on the form. 713 * Overload if you use custom hint controls. 714 * @param object $formdata the data from the form. 715 * @param bool $withparts whether to take into account clearwrong and shownumcorrect options. 716 * @return int count of hints on the form. 717 */ 718 protected function count_hints_on_form($formdata, $withparts) { 719 if (!empty($formdata->hint)) { 720 $numhints = max(array_keys($formdata->hint)) + 1; 721 } else { 722 $numhints = 0; 723 } 724 725 if ($withparts) { 726 if (!empty($formdata->hintclearwrong)) { 727 $numclears = max(array_keys($formdata->hintclearwrong)) + 1; 728 } else { 729 $numclears = 0; 730 } 731 if (!empty($formdata->hintshownumcorrect)) { 732 $numshows = max(array_keys($formdata->hintshownumcorrect)) + 1; 733 } else { 734 $numshows = 0; 735 } 736 $numhints = max($numhints, $numclears, $numshows); 737 } 738 return $numhints; 739 } 740 741 /** 742 * Determine if the hint with specified number is not empty and should be saved. 743 * Overload if you use custom hint controls. 744 * @param object $formdata the data from the form. 745 * @param int $number number of hint under question. 746 * @param bool $withparts whether to take into account clearwrong and shownumcorrect options. 747 * @return bool is this particular hint data empty. 748 */ 749 protected function is_hint_empty_in_form_data($formdata, $number, $withparts) { 750 if ($withparts) { 751 return empty($formdata->hint[$number]['text']) && empty($formdata->hintclearwrong[$number]) && 752 empty($formdata->hintshownumcorrect[$number]); 753 } else { 754 return empty($formdata->hint[$number]['text']); 755 } 756 } 757 758 /** 759 * Save additional question type data into the hint optional field. 760 * Overload if you use custom hint information. 761 * @param object $formdata the data from the form. 762 * @param int $number number of hint to get options from. 763 * @param bool $withparts whether question have parts. 764 * @return string value to save into the options field of question_hints table. 765 */ 766 protected function save_hint_options($formdata, $number, $withparts) { 767 return null; // By default, options field is unused. 768 } 769 770 /** 771 * Can be used to {@link save_question_options()} to transfer the combined 772 * feedback fields from $formdata to $options. 773 * @param object $options the $question->options object being built. 774 * @param object $formdata the data from the form. 775 * @param object $context the context the quetsion is being saved into. 776 * @param bool $withparts whether $options->shownumcorrect should be set. 777 */ 778 protected function save_combined_feedback_helper($options, $formdata, 779 $context, $withparts = false) { 780 $options->correctfeedback = $this->import_or_save_files($formdata->correctfeedback, 781 $context, 'question', 'correctfeedback', $formdata->id); 782 $options->correctfeedbackformat = $formdata->correctfeedback['format']; 783 784 $options->partiallycorrectfeedback = $this->import_or_save_files( 785 $formdata->partiallycorrectfeedback, 786 $context, 'question', 'partiallycorrectfeedback', $formdata->id); 787 $options->partiallycorrectfeedbackformat = $formdata->partiallycorrectfeedback['format']; 788 789 $options->incorrectfeedback = $this->import_or_save_files($formdata->incorrectfeedback, 790 $context, 'question', 'incorrectfeedback', $formdata->id); 791 $options->incorrectfeedbackformat = $formdata->incorrectfeedback['format']; 792 793 if ($withparts) { 794 $options->shownumcorrect = !empty($formdata->shownumcorrect); 795 } 796 797 return $options; 798 } 799 800 /** 801 * Loads the question type specific options for the question. 802 * 803 * This function loads any question type specific options for the 804 * question from the database into the question object. This information 805 * is placed in the $question->options field. A question type is 806 * free, however, to decide on a internal structure of the options field. 807 * @return bool Indicates success or failure. 808 * @param object $question The question object for the question. This object 809 * should be updated to include the question type 810 * specific information (it is passed by reference). 811 */ 812 public function get_question_options($question) { 813 global $CFG, $DB, $OUTPUT; 814 815 if (!isset($question->options)) { 816 $question->options = new stdClass(); 817 } 818 819 $extraquestionfields = $this->extra_question_fields(); 820 if (is_array($extraquestionfields)) { 821 $question_extension_table = array_shift($extraquestionfields); 822 $extra_data = $DB->get_record($question_extension_table, 823 array($this->questionid_column_name() => $question->id), 824 implode(', ', $extraquestionfields)); 825 if ($extra_data) { 826 foreach ($extraquestionfields as $field) { 827 $question->options->$field = $extra_data->$field; 828 } 829 } else { 830 echo $OUTPUT->notification('Failed to load question options from the table ' . 831 $question_extension_table . ' for questionid ' . $question->id); 832 return false; 833 } 834 } 835 836 $extraanswerfields = $this->extra_answer_fields(); 837 if (is_array($extraanswerfields)) { 838 $answerextensiontable = array_shift($extraanswerfields); 839 // Use LEFT JOIN in case not every answer has extra data. 840 $question->options->answers = $DB->get_records_sql(" 841 SELECT qa.*, qax." . implode(', qax.', $extraanswerfields) . ' 842 FROM {question_answers} qa ' . " 843 LEFT JOIN {{$answerextensiontable}} qax ON qa.id = qax.answerid 844 WHERE qa.question = ? 845 ORDER BY qa.id", array($question->id)); 846 if (!$question->options->answers) { 847 echo $OUTPUT->notification('Failed to load question answers from the table ' . 848 $answerextensiontable . 'for questionid ' . $question->id); 849 return false; 850 } 851 } else { 852 // Don't check for success or failure because some question types do 853 // not use the answers table. 854 $question->options->answers = $DB->get_records('question_answers', 855 array('question' => $question->id), 'id ASC'); 856 } 857 858 $question->hints = $DB->get_records('question_hints', 859 array('questionid' => $question->id), 'id ASC'); 860 861 return true; 862 } 863 864 /** 865 * Create an appropriate question_definition for the question of this type 866 * using data loaded from the database. 867 * @param object $questiondata the question data loaded from the database. 868 * @return question_definition the corresponding question_definition. 869 */ 870 public function make_question($questiondata) { 871 $question = $this->make_question_instance($questiondata); 872 $this->initialise_question_instance($question, $questiondata); 873 return $question; 874 } 875 876 /** 877 * Create an appropriate question_definition for the question of this type 878 * using data loaded from the database. 879 * @param object $questiondata the question data loaded from the database. 880 * @return question_definition an instance of the appropriate question_definition subclass. 881 * Still needs to be initialised. 882 */ 883 protected function make_question_instance($questiondata) { 884 question_bank::load_question_definition_classes($this->name()); 885 $class = 'qtype_' . $this->name() . '_question'; 886 return new $class(); 887 } 888 889 /** 890 * Initialise the common question_definition fields. 891 * @param question_definition $question the question_definition we are creating. 892 * @param object $questiondata the question data loaded from the database. 893 */ 894 protected function initialise_question_instance(question_definition $question, $questiondata) { 895 $question->id = $questiondata->id; 896 $question->category = $questiondata->category; 897 $question->contextid = $questiondata->contextid; 898 $question->parent = $questiondata->parent; 899 $question->qtype = $this; 900 $question->name = $questiondata->name; 901 $question->questiontext = $questiondata->questiontext; 902 $question->questiontextformat = $questiondata->questiontextformat; 903 $question->generalfeedback = $questiondata->generalfeedback; 904 $question->generalfeedbackformat = $questiondata->generalfeedbackformat; 905 $question->defaultmark = $questiondata->defaultmark + 0; 906 $question->length = $questiondata->length; 907 $question->penalty = $questiondata->penalty; 908 $question->stamp = $questiondata->stamp; 909 $question->version = $questiondata->version; 910 $question->hidden = $questiondata->hidden; 911 $question->idnumber = $questiondata->idnumber; 912 $question->timecreated = $questiondata->timecreated; 913 $question->timemodified = $questiondata->timemodified; 914 $question->createdby = $questiondata->createdby; 915 $question->modifiedby = $questiondata->modifiedby; 916 917 // Fill extra question fields values. 918 $extraquestionfields = $this->extra_question_fields(); 919 if (is_array($extraquestionfields)) { 920 // Omit table name. 921 array_shift($extraquestionfields); 922 foreach ($extraquestionfields as $field) { 923 $question->$field = $questiondata->options->$field; 924 } 925 } 926 927 $this->initialise_question_hints($question, $questiondata); 928 } 929 930 /** 931 * Initialise question_definition::hints field. 932 * @param question_definition $question the question_definition we are creating. 933 * @param object $questiondata the question data loaded from the database. 934 */ 935 protected function initialise_question_hints(question_definition $question, $questiondata) { 936 if (empty($questiondata->hints)) { 937 return; 938 } 939 foreach ($questiondata->hints as $hint) { 940 $question->hints[] = $this->make_hint($hint); 941 } 942 } 943 944 /** 945 * Create a question_hint, or an appropriate subclass for this question, 946 * from a row loaded from the database. 947 * @param object $hint the DB row from the question hints table. 948 * @return question_hint 949 */ 950 protected function make_hint($hint) { 951 return question_hint::load_from_record($hint); 952 } 953 954 /** 955 * Initialise the combined feedback fields. 956 * @param question_definition $question the question_definition we are creating. 957 * @param object $questiondata the question data loaded from the database. 958 * @param bool $withparts whether to set the shownumcorrect field. 959 */ 960 protected function initialise_combined_feedback(question_definition $question, 961 $questiondata, $withparts = false) { 962 $question->correctfeedback = $questiondata->options->correctfeedback; 963 $question->correctfeedbackformat = $questiondata->options->correctfeedbackformat; 964 $question->partiallycorrectfeedback = $questiondata->options->partiallycorrectfeedback; 965 $question->partiallycorrectfeedbackformat = 966 $questiondata->options->partiallycorrectfeedbackformat; 967 $question->incorrectfeedback = $questiondata->options->incorrectfeedback; 968 $question->incorrectfeedbackformat = $questiondata->options->incorrectfeedbackformat; 969 if ($withparts) { 970 $question->shownumcorrect = $questiondata->options->shownumcorrect; 971 } 972 } 973 974 /** 975 * Initialise question_definition::answers field. 976 * @param question_definition $question the question_definition we are creating. 977 * @param object $questiondata the question data loaded from the database. 978 * @param bool $forceplaintextanswers most qtypes assume that answers are 979 * FORMAT_PLAIN, and dont use the answerformat DB column (it contains 980 * the default 0 = FORMAT_MOODLE). Therefore, by default this method 981 * ingores answerformat. Pass false here to use answerformat. For example 982 * multichoice does this. 983 */ 984 protected function initialise_question_answers(question_definition $question, 985 $questiondata, $forceplaintextanswers = true) { 986 $question->answers = array(); 987 if (empty($questiondata->options->answers)) { 988 return; 989 } 990 foreach ($questiondata->options->answers as $a) { 991 $question->answers[$a->id] = $this->make_answer($a); 992 if (!$forceplaintextanswers) { 993 $question->answers[$a->id]->answerformat = $a->answerformat; 994 } 995 } 996 } 997 998 /** 999 * Create a question_answer, or an appropriate subclass for this question, 1000 * from a row loaded from the database. 1001 * @param object $answer the DB row from the question_answers table plus extra answer fields. 1002 * @return question_answer 1003 */ 1004 protected function make_answer($answer) { 1005 return new question_answer($answer->id, $answer->answer, 1006 $answer->fraction, $answer->feedback, $answer->feedbackformat); 1007 } 1008 1009 /** 1010 * Deletes the question-type specific data when a question is deleted. 1011 * @param int $question the question being deleted. 1012 * @param int $contextid the context this quesiotn belongs to. 1013 */ 1014 public function delete_question($questionid, $contextid) { 1015 global $DB; 1016 1017 $this->delete_files($questionid, $contextid); 1018 1019 $extraquestionfields = $this->extra_question_fields(); 1020 if (is_array($extraquestionfields)) { 1021 $question_extension_table = array_shift($extraquestionfields); 1022 $DB->delete_records($question_extension_table, 1023 array($this->questionid_column_name() => $questionid)); 1024 } 1025 1026 $extraanswerfields = $this->extra_answer_fields(); 1027 if (is_array($extraanswerfields)) { 1028 $answer_extension_table = array_shift($extraanswerfields); 1029 $DB->delete_records_select($answer_extension_table, 1030 'answerid IN (SELECT qa.id FROM {question_answers} qa WHERE qa.question = ?)', 1031 array($questionid)); 1032 } 1033 1034 $DB->delete_records('question_answers', array('question' => $questionid)); 1035 1036 $DB->delete_records('question_hints', array('questionid' => $questionid)); 1037 } 1038 1039 /** 1040 * Returns the number of question numbers which are used by the question 1041 * 1042 * This function returns the number of question numbers to be assigned 1043 * to the question. Most question types will have length one; they will be 1044 * assigned one number. The 'description' type, however does not use up a 1045 * number and so has a length of zero. Other question types may wish to 1046 * handle a bundle of questions and hence return a number greater than one. 1047 * @return int The number of question numbers which should be 1048 * assigned to the question. 1049 * @param object $question The question whose length is to be determined. 1050 * Question type specific information is included. 1051 */ 1052 public function actual_number_of_questions($question) { 1053 // By default, each question is given one number. 1054 return 1; 1055 } 1056 1057 /** 1058 * Calculate the score a monkey would get on a question by clicking randomly. 1059 * 1060 * Some question types have significant non-zero average expected score 1061 * of the response is just selected randomly. For example 50% for a 1062 * true-false question. It is useful to know what this is. For example 1063 * it gets shown in the quiz statistics report. 1064 * 1065 * For almost any open-ended question type (E.g. shortanswer or numerical) 1066 * this should be 0. 1067 * 1068 * For selective response question types (e.g. multiple choice), you can probably compute this. 1069 * 1070 * For particularly complicated question types the may be impossible or very 1071 * difficult to compute. In this case return null. (Or, if the expected score 1072 * is very tiny even though the exact value is unknown, it may appropriate 1073 * to return 0.) 1074 * 1075 * @param stdClass $questiondata data defining a question, as returned by 1076 * question_bank::load_question_data(). 1077 * @return number|null either a fraction estimating what the student would 1078 * score by guessing, or null, if it is not possible to estimate. 1079 */ 1080 public function get_random_guess_score($questiondata) { 1081 return 0; 1082 } 1083 1084 /** 1085 * Whether or not to break down question stats and response analysis, for a question defined by $questiondata. 1086 * 1087 * @param object $questiondata The full question definition data. 1088 * @return bool 1089 */ 1090 public function break_down_stats_and_response_analysis_by_variant($questiondata) { 1091 return true; 1092 } 1093 1094 /** 1095 * This method should return all the possible types of response that are 1096 * recognised for this question. 1097 * 1098 * The question is modelled as comprising one or more subparts. For each 1099 * subpart, there are one or more classes that that students response 1100 * might fall into, each of those classes earning a certain score. 1101 * 1102 * For example, in a shortanswer question, there is only one subpart, the 1103 * text entry field. The response the student gave will be classified according 1104 * to which of the possible $question->options->answers it matches. 1105 * 1106 * For the matching question type, there will be one subpart for each 1107 * question stem, and for each stem, each of the possible choices is a class 1108 * of student's response. 1109 * 1110 * A response is an object with two fields, ->responseclass is a string 1111 * presentation of that response, and ->fraction, the credit for a response 1112 * in that class. 1113 * 1114 * Array keys have no specific meaning, but must be unique, and must be 1115 * the same if this function is called repeatedly. 1116 * 1117 * @param object $question the question definition data. 1118 * @return array keys are subquestionid, values are arrays of possible 1119 * responses to that subquestion. 1120 */ 1121 public function get_possible_responses($questiondata) { 1122 return array(); 1123 } 1124 1125 /** 1126 * Utility method used by {@link qtype_renderer::head_code()}. It looks 1127 * for any of the files script.js or script.php that exist in the plugin 1128 * folder and ensures they get included. 1129 */ 1130 public function find_standard_scripts() { 1131 global $PAGE; 1132 1133 $plugindir = $this->plugin_dir(); 1134 $plugindirrel = 'question/type/' . $this->name(); 1135 1136 if (file_exists($plugindir . '/script.js')) { 1137 $PAGE->requires->js('/' . $plugindirrel . '/script.js'); 1138 } 1139 if (file_exists($plugindir . '/script.php')) { 1140 $PAGE->requires->js('/' . $plugindirrel . '/script.php'); 1141 } 1142 } 1143 1144 /** 1145 * Returns true if the editing wizard is finished, false otherwise. 1146 * 1147 * The default implementation returns true, which is suitable for all question- 1148 * types that only use one editing form. This function is used in 1149 * question.php to decide whether we can regrade any states of the edited 1150 * question and redirect to edit.php. 1151 * 1152 * The dataset dependent question-type, which is extended by the calculated 1153 * question-type, overwrites this method because it uses multiple pages (i.e. 1154 * a wizard) to set up the question and associated datasets. 1155 * 1156 * @param object $form The data submitted by the previous page. 1157 * 1158 * @return bool Whether the wizard's last page was submitted or not. 1159 */ 1160 public function finished_edit_wizard($form) { 1161 // In the default case there is only one edit page. 1162 return true; 1163 } 1164 1165 // IMPORT/EXPORT FUNCTIONS --------------------------------- . 1166 1167 /* 1168 * Imports question from the Moodle XML format 1169 * 1170 * Imports question using information from extra_question_fields function 1171 * If some of you fields contains id's you'll need to reimplement this 1172 */ 1173 public function import_from_xml($data, $question, qformat_xml $format, $extra=null) { 1174 $question_type = $data['@']['type']; 1175 if ($question_type != $this->name()) { 1176 return false; 1177 } 1178 1179 $extraquestionfields = $this->extra_question_fields(); 1180 if (!is_array($extraquestionfields)) { 1181 return false; 1182 } 1183 1184 // Omit table name. 1185 array_shift($extraquestionfields); 1186 $qo = $format->import_headers($data); 1187 $qo->qtype = $question_type; 1188 1189 foreach ($extraquestionfields as $field) { 1190 $qo->$field = $format->getpath($data, array('#', $field, 0, '#'), ''); 1191 } 1192 1193 // Run through the answers. 1194 $answers = $data['#']['answer']; 1195 $a_count = 0; 1196 $extraanswersfields = $this->extra_answer_fields(); 1197 if (is_array($extraanswersfields)) { 1198 array_shift($extraanswersfields); 1199 } 1200 foreach ($answers as $answer) { 1201 $ans = $format->import_answer($answer); 1202 if (!$this->has_html_answers()) { 1203 $qo->answer[$a_count] = $ans->answer['text']; 1204 } else { 1205 $qo->answer[$a_count] = $ans->answer; 1206 } 1207 $qo->fraction[$a_count] = $ans->fraction; 1208 $qo->feedback[$a_count] = $ans->feedback; 1209 if (is_array($extraanswersfields)) { 1210 foreach ($extraanswersfields as $field) { 1211 $qo->{$field}[$a_count] = 1212 $format->getpath($answer, array('#', $field, 0, '#'), ''); 1213 } 1214 } 1215 ++$a_count; 1216 } 1217 return $qo; 1218 } 1219 1220 /* 1221 * Export question to the Moodle XML format 1222 * 1223 * Export question using information from extra_question_fields function 1224 * If some of you fields contains id's you'll need to reimplement this 1225 */ 1226 public function export_to_xml($question, qformat_xml $format, $extra=null) { 1227 $extraquestionfields = $this->extra_question_fields(); 1228 if (!is_array($extraquestionfields)) { 1229 return false; 1230 } 1231 1232 // Omit table name. 1233 array_shift($extraquestionfields); 1234 $expout=''; 1235 foreach ($extraquestionfields as $field) { 1236 $exportedvalue = $format->xml_escape($question->options->$field); 1237 $expout .= " <{$field}>{$exportedvalue}</{$field}>\n"; 1238 } 1239 1240 $extraanswersfields = $this->extra_answer_fields(); 1241 if (is_array($extraanswersfields)) { 1242 array_shift($extraanswersfields); 1243 } 1244 foreach ($question->options->answers as $answer) { 1245 $extra = ''; 1246 if (is_array($extraanswersfields)) { 1247 foreach ($extraanswersfields as $field) { 1248 $exportedvalue = $format->xml_escape($answer->$field); 1249 $extra .= " <{$field}>{$exportedvalue}</{$field}>\n"; 1250 } 1251 } 1252 1253 $expout .= $format->write_answer($answer, $extra); 1254 } 1255 return $expout; 1256 } 1257 1258 /** 1259 * Abstract function implemented by each question type. It runs all the code 1260 * required to set up and save a question of any type for testing purposes. 1261 * Alternate DB table prefix may be used to facilitate data deletion. 1262 */ 1263 public function generate_test($name, $courseid=null) { 1264 $form = new stdClass(); 1265 $form->name = $name; 1266 $form->questiontextformat = 1; 1267 $form->questiontext = 'test question, generated by script'; 1268 $form->defaultmark = 1; 1269 $form->penalty = 0.3333333; 1270 $form->generalfeedback = "Well done"; 1271 1272 $context = context_course::instance($courseid); 1273 $newcategory = question_make_default_categories(array($context)); 1274 $form->category = $newcategory->id . ',1'; 1275 1276 $question = new stdClass(); 1277 $question->courseid = $courseid; 1278 $question->qtype = $this->qtype; 1279 return array($form, $question); 1280 } 1281 1282 /** 1283 * Get question context by category id 1284 * @param int $category 1285 * @return object $context 1286 */ 1287 protected function get_context_by_category_id($category) { 1288 global $DB; 1289 $contextid = $DB->get_field('question_categories', 'contextid', array('id'=>$category)); 1290 $context = context::instance_by_id($contextid, IGNORE_MISSING); 1291 return $context; 1292 } 1293 1294 /** 1295 * Save the file belonging to one text field. 1296 * 1297 * @param array $field the data from the form (or from import). This will 1298 * normally have come from the formslib editor element, so it will be an 1299 * array with keys 'text', 'format' and 'itemid'. However, when we are 1300 * importing, it will be an array with keys 'text', 'format' and 'files' 1301 * @param object $context the context the question is in. 1302 * @param string $component indentifies the file area question. 1303 * @param string $filearea indentifies the file area questiontext, 1304 * generalfeedback, answerfeedback, etc. 1305 * @param int $itemid identifies the file area. 1306 * 1307 * @return string the text for this field, after files have been processed. 1308 */ 1309 protected function import_or_save_files($field, $context, $component, $filearea, $itemid) { 1310 if (!empty($field['itemid'])) { 1311 // This is the normal case. We are safing the questions editing form. 1312 return file_save_draft_area_files($field['itemid'], $context->id, $component, 1313 $filearea, $itemid, $this->fileoptions, trim($field['text'])); 1314 1315 } else if (!empty($field['files'])) { 1316 // This is the case when we are doing an import. 1317 foreach ($field['files'] as $file) { 1318 $this->import_file($context, $component, $filearea, $itemid, $file); 1319 } 1320 } 1321 return trim($field['text']); 1322 } 1323 1324 /** 1325 * Move all the files belonging to this question from one context to another. 1326 * @param int $questionid the question being moved. 1327 * @param int $oldcontextid the context it is moving from. 1328 * @param int $newcontextid the context it is moving to. 1329 */ 1330 public function move_files($questionid, $oldcontextid, $newcontextid) { 1331 $fs = get_file_storage(); 1332 $fs->move_area_files_to_new_context($oldcontextid, 1333 $newcontextid, 'question', 'questiontext', $questionid); 1334 $fs->move_area_files_to_new_context($oldcontextid, 1335 $newcontextid, 'question', 'generalfeedback', $questionid); 1336 } 1337 1338 /** 1339 * Move all the files belonging to this question's answers when the question 1340 * is moved from one context to another. 1341 * @param int $questionid the question being moved. 1342 * @param int $oldcontextid the context it is moving from. 1343 * @param int $newcontextid the context it is moving to. 1344 * @param bool $answerstoo whether there is an 'answer' question area, 1345 * as well as an 'answerfeedback' one. Default false. 1346 */ 1347 protected function move_files_in_answers($questionid, $oldcontextid, 1348 $newcontextid, $answerstoo = false) { 1349 global $DB; 1350 $fs = get_file_storage(); 1351 1352 $answerids = $DB->get_records_menu('question_answers', 1353 array('question' => $questionid), 'id', 'id,1'); 1354 foreach ($answerids as $answerid => $notused) { 1355 if ($answerstoo) { 1356 $fs->move_area_files_to_new_context($oldcontextid, 1357 $newcontextid, 'question', 'answer', $answerid); 1358 } 1359 $fs->move_area_files_to_new_context($oldcontextid, 1360 $newcontextid, 'question', 'answerfeedback', $answerid); 1361 } 1362 } 1363 1364 /** 1365 * Move all the files belonging to this question's hints when the question 1366 * is moved from one context to another. 1367 * @param int $questionid the question being moved. 1368 * @param int $oldcontextid the context it is moving from. 1369 * @param int $newcontextid the context it is moving to. 1370 * @param bool $answerstoo whether there is an 'answer' question area, 1371 * as well as an 'answerfeedback' one. Default false. 1372 */ 1373 protected function move_files_in_hints($questionid, $oldcontextid, $newcontextid) { 1374 global $DB; 1375 $fs = get_file_storage(); 1376 1377 $hintids = $DB->get_records_menu('question_hints', 1378 array('questionid' => $questionid), 'id', 'id,1'); 1379 foreach ($hintids as $hintid => $notused) { 1380 $fs->move_area_files_to_new_context($oldcontextid, 1381 $newcontextid, 'question', 'hint', $hintid); 1382 } 1383 } 1384 1385 /** 1386 * Move all the files belonging to this question's answers when the question 1387 * is moved from one context to another. 1388 * @param int $questionid the question being moved. 1389 * @param int $oldcontextid the context it is moving from. 1390 * @param int $newcontextid the context it is moving to. 1391 * @param bool $answerstoo whether there is an 'answer' question area, 1392 * as well as an 'answerfeedback' one. Default false. 1393 */ 1394 protected function move_files_in_combined_feedback($questionid, $oldcontextid, 1395 $newcontextid) { 1396 global $DB; 1397 $fs = get_file_storage(); 1398 1399 $fs->move_area_files_to_new_context($oldcontextid, 1400 $newcontextid, 'question', 'correctfeedback', $questionid); 1401 $fs->move_area_files_to_new_context($oldcontextid, 1402 $newcontextid, 'question', 'partiallycorrectfeedback', $questionid); 1403 $fs->move_area_files_to_new_context($oldcontextid, 1404 $newcontextid, 'question', 'incorrectfeedback', $questionid); 1405 } 1406 1407 /** 1408 * Delete all the files belonging to this question. 1409 * @param int $questionid the question being deleted. 1410 * @param int $contextid the context the question is in. 1411 */ 1412 protected function delete_files($questionid, $contextid) { 1413 $fs = get_file_storage(); 1414 $fs->delete_area_files($contextid, 'question', 'questiontext', $questionid); 1415 $fs->delete_area_files($contextid, 'question', 'generalfeedback', $questionid); 1416 } 1417 1418 /** 1419 * Delete all the files belonging to this question's answers. 1420 * @param int $questionid the question being deleted. 1421 * @param int $contextid the context the question is in. 1422 * @param bool $answerstoo whether there is an 'answer' question area, 1423 * as well as an 'answerfeedback' one. Default false. 1424 */ 1425 protected function delete_files_in_answers($questionid, $contextid, $answerstoo = false) { 1426 global $DB; 1427 $fs = get_file_storage(); 1428 1429 $answerids = $DB->get_records_menu('question_answers', 1430 array('question' => $questionid), 'id', 'id,1'); 1431 foreach ($answerids as $answerid => $notused) { 1432 if ($answerstoo) { 1433 $fs->delete_area_files($contextid, 'question', 'answer', $answerid); 1434 } 1435 $fs->delete_area_files($contextid, 'question', 'answerfeedback', $answerid); 1436 } 1437 } 1438 1439 /** 1440 * Delete all the files belonging to this question's hints. 1441 * @param int $questionid the question being deleted. 1442 * @param int $contextid the context the question is in. 1443 */ 1444 protected function delete_files_in_hints($questionid, $contextid) { 1445 global $DB; 1446 $fs = get_file_storage(); 1447 1448 $hintids = $DB->get_records_menu('question_hints', 1449 array('questionid' => $questionid), 'id', 'id,1'); 1450 foreach ($hintids as $hintid => $notused) { 1451 $fs->delete_area_files($contextid, 'question', 'hint', $hintid); 1452 } 1453 } 1454 1455 /** 1456 * Delete all the files belonging to this question's answers. 1457 * @param int $questionid the question being deleted. 1458 * @param int $contextid the context the question is in. 1459 * @param bool $answerstoo whether there is an 'answer' question area, 1460 * as well as an 'answerfeedback' one. Default false. 1461 */ 1462 protected function delete_files_in_combined_feedback($questionid, $contextid) { 1463 global $DB; 1464 $fs = get_file_storage(); 1465 1466 $fs->delete_area_files($contextid, 1467 'question', 'correctfeedback', $questionid); 1468 $fs->delete_area_files($contextid, 1469 'question', 'partiallycorrectfeedback', $questionid); 1470 $fs->delete_area_files($contextid, 1471 'question', 'incorrectfeedback', $questionid); 1472 } 1473 1474 public function import_file($context, $component, $filearea, $itemid, $file) { 1475 $fs = get_file_storage(); 1476 $record = new stdClass(); 1477 if (is_object($context)) { 1478 $record->contextid = $context->id; 1479 } else { 1480 $record->contextid = $context; 1481 } 1482 $record->component = $component; 1483 $record->filearea = $filearea; 1484 $record->itemid = $itemid; 1485 $record->filename = $file->name; 1486 $record->filepath = '/'; 1487 return $fs->create_file_from_string($record, $this->decode_file($file)); 1488 } 1489 1490 protected function decode_file($file) { 1491 switch ($file->encoding) { 1492 case 'base64': 1493 default: 1494 return base64_decode($file->content); 1495 } 1496 } 1497 } 1498 1499 1500 /** 1501 * This class is used in the return value from 1502 * {@link question_type::get_possible_responses()}. 1503 * 1504 * @copyright 2010 The Open University 1505 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 1506 */ 1507 class question_possible_response { 1508 /** 1509 * @var string the classification of this response the student gave to this 1510 * part of the question. Must match one of the responseclasses returned by 1511 * {@link question_type::get_possible_responses()}. 1512 */ 1513 public $responseclass; 1514 1515 /** @var string the (partial) credit awarded for this responses. */ 1516 public $fraction; 1517 1518 /** 1519 * Constructor, just an easy way to set the fields. 1520 * @param string $responseclassid see the field descriptions above. 1521 * @param string $response see the field descriptions above. 1522 * @param number $fraction see the field descriptions above. 1523 */ 1524 public function __construct($responseclass, $fraction) { 1525 $this->responseclass = $responseclass; 1526 $this->fraction = $fraction; 1527 } 1528 1529 public static function no_response() { 1530 return new question_possible_response(get_string('noresponse', 'question'), 0); 1531 } 1532 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body