See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 401 and 402] [Versions 401 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 * A base class for question editing forms. 19 * 20 * @package moodlecore 21 * @subpackage questiontypes 22 * @copyright 2006 The Open University 23 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License 24 */ 25 26 27 defined('MOODLE_INTERNAL') || die(); 28 29 global $CFG; 30 require_once($CFG->libdir.'/formslib.php'); 31 32 33 abstract class question_wizard_form extends moodleform { 34 /** 35 * Add all the hidden form fields used by question/question.php. 36 */ 37 protected function add_hidden_fields() { 38 $mform = $this->_form; 39 40 $mform->addElement('hidden', 'id'); 41 $mform->setType('id', PARAM_INT); 42 43 $mform->addElement('hidden', 'inpopup'); 44 $mform->setType('inpopup', PARAM_INT); 45 46 $mform->addElement('hidden', 'cmid'); 47 $mform->setType('cmid', PARAM_INT); 48 49 $mform->addElement('hidden', 'courseid'); 50 $mform->setType('courseid', PARAM_INT); 51 52 $mform->addElement('hidden', 'returnurl'); 53 $mform->setType('returnurl', PARAM_LOCALURL); 54 55 $mform->addElement('hidden', 'scrollpos'); 56 $mform->setType('scrollpos', PARAM_INT); 57 58 $mform->addElement('hidden', 'appendqnumstring'); 59 $mform->setType('appendqnumstring', PARAM_ALPHA); 60 } 61 } 62 63 /** 64 * Form definition base class. This defines the common fields that 65 * all question types need. Question types should define their own 66 * class that inherits from this one, and implements the definition_inner() 67 * method. 68 * 69 * @copyright 2006 The Open University 70 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License 71 */ 72 abstract class question_edit_form extends question_wizard_form { 73 const DEFAULT_NUM_HINTS = 2; 74 75 /** 76 * Question object with options and answers already loaded by get_question_options 77 * Be careful how you use this it is needed sometimes to set up the structure of the 78 * form in definition_inner but data is always loaded into the form with set_data. 79 * @var object 80 */ 81 protected $question; 82 83 protected $contexts; 84 protected $category; 85 protected $categorycontext; 86 87 /** @var object current context */ 88 public $context; 89 /** @var array html editor options */ 90 public $editoroptions; 91 /** @var array options to preapre draft area */ 92 public $fileoptions; 93 /** @var object instance of question type */ 94 public $instance; 95 /** @var object instance of custom field */ 96 protected $customfieldhandler; 97 /** @var bool custom field plugin enabled or disabled*/ 98 protected $customfieldpluginenabled = true; 99 100 public function __construct($submiturl, $question, $category, $contexts, $formeditable = true) { 101 global $DB; 102 103 $this->question = $question; 104 $this->contexts = $contexts; 105 106 // Get the question category id. 107 if (isset($question->id)) { 108 $qcategory = $question->categoryobject->id ?? get_question_bank_entry($question->id)->questioncategoryid; 109 } else { 110 $qcategory = $question->category; 111 } 112 113 $record = $DB->get_record('question_categories', 114 array('id' => $qcategory), 'contextid'); 115 $this->context = context::instance_by_id($record->contextid); 116 117 $this->editoroptions = array('subdirs' => 1, 'maxfiles' => EDITOR_UNLIMITED_FILES, 118 'context' => $this->context); 119 $this->fileoptions = array('subdirs' => 1, 'maxfiles' => -1, 'maxbytes' => -1); 120 121 $this->category = $category; 122 $this->categorycontext = context::instance_by_id($category->contextid); 123 124 if (!\core\plugininfo\qbank::is_plugin_enabled('qbank_customfields')) { 125 $this->customfieldpluginenabled = false; 126 } 127 128 parent::__construct($submiturl, null, 'post', '', ['data-qtype' => $this->qtype()], $formeditable); 129 } 130 131 /** 132 * Return default value for a given form element either from user_preferences table or $default. 133 * 134 * To make use of user_preferences in your qtype default settings, you need to replace 135 * $mform->setDefault({elementname}, {defaultvalue}); in edit_{qtypename}_form.php with 136 * $mform->setDefault({elementname}, $this->get_default_value({elementname}, {defaultvalue})); 137 * 138 * @param string $name the name of the form field. 139 * @param mixed $default default value. 140 * @return string|null default value for a given form element. 141 */ 142 protected function get_default_value(string $name, $default): ?string { 143 return question_bank::get_qtype($this->qtype())->get_default_value($name, $default); 144 } 145 146 /** 147 * Build the form definition. 148 * 149 * This adds all the form fields that the default question type supports. 150 * If your question type does not support all these fields, then you can 151 * override this method and remove the ones you don't want with $mform->removeElement(). 152 */ 153 protected function definition() { 154 global $DB, $PAGE; 155 156 $mform = $this->_form; 157 158 // Standard fields at the start of the form. 159 $mform->addElement('header', 'generalheader', get_string("general", 'form')); 160 161 if (!isset($this->question->id)) { 162 if (!empty($this->question->formoptions->mustbeusable)) { 163 $contexts = $this->contexts->having_add_and_use(); 164 } else { 165 $contexts = $this->contexts->having_cap('moodle/question:add'); 166 } 167 168 // Adding question. 169 $mform->addElement('questioncategory', 'category', get_string('category', 'question'), 170 array('contexts' => $contexts)); 171 } else if (!($this->question->formoptions->canmove || 172 $this->question->formoptions->cansaveasnew)) { 173 // Editing question with no permission to move from category. 174 $mform->addElement('questioncategory', 'category', get_string('category', 'question'), 175 array('contexts' => array($this->categorycontext))); 176 $mform->addElement('hidden', 'usecurrentcat', 1); 177 $mform->setType('usecurrentcat', PARAM_BOOL); 178 $mform->setConstant('usecurrentcat', 1); 179 } else { 180 // Editing question with permission to move from category or save as new q. 181 $currentgrp = array(); 182 $currentgrp[0] = $mform->createElement('questioncategory', 'category', 183 get_string('categorycurrent', 'question'), 184 array('contexts' => array($this->categorycontext))); 185 // Validate if the question is being duplicated. 186 $beingcopied = false; 187 if (isset($this->question->beingcopied)) { 188 $beingcopied = $this->question->beingcopied; 189 } 190 if (($this->question->formoptions->canedit || 191 $this->question->formoptions->cansaveasnew) && ($beingcopied)) { 192 // Not move only form. 193 $currentgrp[1] = $mform->createElement('checkbox', 'usecurrentcat', '', 194 get_string('categorycurrentuse', 'question')); 195 $mform->setDefault('usecurrentcat', 1); 196 } 197 $currentgrp[0]->freeze(); 198 $currentgrp[0]->setPersistantFreeze(false); 199 $mform->addGroup($currentgrp, 'currentgrp', 200 get_string('categorycurrent', 'question'), null, false); 201 202 if (($beingcopied)) { 203 $mform->addElement('questioncategory', 'categorymoveto', 204 get_string('categorymoveto', 'question'), 205 array('contexts' => array($this->categorycontext))); 206 if ($this->question->formoptions->canedit || 207 $this->question->formoptions->cansaveasnew) { 208 // Not move only form. 209 $mform->disabledIf('categorymoveto', 'usecurrentcat', 'checked'); 210 } 211 } 212 } 213 214 if (!empty($this->question->id) && !$this->question->beingcopied) { 215 // Add extra information from plugins when editing a question (e.g.: Authors, version control and usage). 216 $functionname = 'edit_form_display'; 217 $questiondata = []; 218 $plugins = get_plugin_list_with_function('qbank', $functionname); 219 foreach ($plugins as $componentname => $plugin) { 220 $element = new StdClass(); 221 $element->pluginhtml = component_callback($componentname, $functionname, [$this->question]); 222 $questiondata['editelements'][] = $element; 223 } 224 $mform->addElement('static', 'versioninfo', get_string('versioninfo', 'qbank_editquestion'), 225 $PAGE->get_renderer('qbank_editquestion')->render_question_info($questiondata)); 226 } 227 228 $mform->addElement('text', 'name', get_string('questionname', 'question'), 229 array('size' => 50, 'maxlength' => 255)); 230 $mform->setType('name', PARAM_TEXT); 231 $mform->addRule('name', null, 'required', null, 'client'); 232 233 $mform->addElement('editor', 'questiontext', get_string('questiontext', 'question'), 234 array('rows' => 15), $this->editoroptions); 235 $mform->setType('questiontext', PARAM_RAW); 236 $mform->addRule('questiontext', null, 'required', null, 'client'); 237 238 $mform->addElement('select', 'status', get_string('status', 'qbank_editquestion'), 239 \qbank_editquestion\editquestion_helper::get_question_status_list()); 240 241 $mform->addElement('float', 'defaultmark', get_string('defaultmark', 'question'), 242 array('size' => 7)); 243 $mform->setDefault('defaultmark', $this->get_default_value('defaultmark', 1)); 244 $mform->addRule('defaultmark', null, 'required', null, 'client'); 245 246 $mform->addElement('editor', 'generalfeedback', get_string('generalfeedback', 'question'), 247 array('rows' => 10), $this->editoroptions); 248 $mform->setType('generalfeedback', PARAM_RAW); 249 $mform->addHelpButton('generalfeedback', 'generalfeedback', 'question'); 250 251 $mform->addElement('text', 'idnumber', get_string('idnumber', 'question'), 'maxlength="100" size="10"'); 252 $mform->addHelpButton('idnumber', 'idnumber', 'question'); 253 $mform->setType('idnumber', PARAM_RAW); 254 255 // Any questiontype specific fields. 256 $this->definition_inner($mform); 257 258 if (core_tag_tag::is_enabled('core_question', 'question') 259 && class_exists('qbank_tagquestion\\tags_action_column') 260 && \core\plugininfo\qbank::is_plugin_enabled('qbank_tagquestion')) { 261 $this->add_tag_fields($mform); 262 } 263 264 if ($this->customfieldpluginenabled) { 265 // Add custom fields to the form. 266 $this->customfieldhandler = qbank_customfields\customfield\question_handler::create(); 267 $this->customfieldhandler->set_parent_context($this->categorycontext); // For question handler only. 268 $this->customfieldhandler->instance_form_definition($mform, empty($this->question->id) ? 0 : $this->question->id); 269 } 270 271 $this->add_hidden_fields(); 272 273 $mform->addElement('hidden', 'qtype'); 274 $mform->setType('qtype', PARAM_ALPHA); 275 276 $mform->addElement('hidden', 'makecopy'); 277 $mform->setType('makecopy', PARAM_INT); 278 279 $buttonarray = array(); 280 $buttonarray[] = $mform->createElement('submit', 'updatebutton', 281 get_string('savechangesandcontinueediting', 'question')); 282 if ($this->can_preview()) { 283 if (\core\plugininfo\qbank::is_plugin_enabled('qbank_previewquestion')) { 284 $previewlink = $PAGE->get_renderer('qbank_previewquestion')->question_preview_link( 285 $this->question->id, $this->context, true); 286 $buttonarray[] = $mform->createElement('static', 'previewlink', '', $previewlink); 287 } 288 } 289 290 $mform->addGroup($buttonarray, 'updatebuttonar', '', array(' '), false); 291 $mform->closeHeaderBefore('updatebuttonar'); 292 293 $this->add_action_buttons(true, get_string('savechanges')); 294 295 if ((!empty($this->question->id)) && (!($this->question->formoptions->canedit || 296 $this->question->formoptions->cansaveasnew))) { 297 $mform->hardFreezeAllVisibleExcept(array('categorymoveto', 'buttonar', 'currentgrp')); 298 } 299 } 300 301 /** 302 * Add any question-type specific form fields. 303 * 304 * @param object $mform the form being built. 305 */ 306 protected function definition_inner($mform) { 307 // By default, do nothing. 308 } 309 310 /** 311 * Tweak the form with values provided by custom fields in use. 312 */ 313 public function definition_after_data() { 314 $mform = $this->_form; 315 if ($this->customfieldpluginenabled) { 316 $this->customfieldhandler->instance_form_definition_after_data($mform, 317 empty($this->question->id) ? 0 : $this->question->id); 318 } 319 } 320 321 /** 322 * Is the question being edited in a state where it can be previewed? 323 * @return bool whether to show the preview link. 324 */ 325 protected function can_preview() { 326 return empty($this->question->beingcopied) && !empty($this->question->id) && 327 $this->question->formoptions->canedit; 328 } 329 330 /** 331 * Get the list of form elements to repeat, one for each answer. 332 * @param object $mform the form being built. 333 * @param $label the label to use for each option. 334 * @param $gradeoptions the possible grades for each answer. 335 * @param $repeatedoptions reference to array of repeated options to fill 336 * @param $answersoption reference to return the name of $question->options 337 * field holding an array of answers 338 * @return array of form fields. 339 */ 340 protected function get_per_answer_fields($mform, $label, $gradeoptions, 341 &$repeatedoptions, &$answersoption) { 342 $repeated = array(); 343 $answeroptions = array(); 344 $answeroptions[] = $mform->createElement('text', 'answer', 345 $label, array('size' => 40)); 346 $answeroptions[] = $mform->createElement('select', 'fraction', 347 get_string('gradenoun'), $gradeoptions); 348 $repeated[] = $mform->createElement('group', 'answeroptions', 349 $label, $answeroptions, null, false); 350 $repeated[] = $mform->createElement('editor', 'feedback', 351 get_string('feedback', 'question'), array('rows' => 5), $this->editoroptions); 352 $repeatedoptions['answer']['type'] = PARAM_RAW; 353 $repeatedoptions['fraction']['default'] = 0; 354 $answersoption = 'answers'; 355 return $repeated; 356 } 357 358 /** 359 * Add the tag and course tag fields to the mform. 360 * 361 * If the form is being built in a course context then add the field 362 * for course tags. 363 * 364 * If the question category doesn't belong to a course context or we 365 * aren't editing in a course context then add the tags element to allow 366 * tags to be added to the question category context. 367 * 368 * @param object $mform The form being built 369 */ 370 protected function add_tag_fields($mform) { 371 global $CFG, $DB; 372 373 $hastagcapability = question_has_capability_on($this->question, 'tag'); 374 // Is the question category in a course context? 375 $qcontext = $this->categorycontext; 376 $qcoursecontext = $qcontext->get_course_context(false); 377 $iscourseoractivityquestion = !empty($qcoursecontext); 378 // Is the current context we're editing in a course context? 379 $editingcontext = $this->contexts->lowest(); 380 $editingcoursecontext = $editingcontext->get_course_context(false); 381 $iseditingcontextcourseoractivity = !empty($editingcoursecontext); 382 383 $mform->addElement('header', 'tagsheader', get_string('tags')); 384 $tags = \core_tag_tag::get_tags_by_area_in_contexts('core_question', 'question', $this->contexts->all()); 385 $tagstrings = []; 386 foreach ($tags as $tag) { 387 $tagstrings[$tag->name] = $tag->name; 388 } 389 390 $showstandard = core_tag_area::get_showstandard('core_question', 'question'); 391 if ($showstandard != core_tag_tag::HIDE_STANDARD) { 392 $namefield = empty($CFG->keeptagnamecase) ? 'name' : 'rawname'; 393 $standardtags = $DB->get_records('tag', 394 array('isstandard' => 1, 'tagcollid' => core_tag_area::get_collection('core', 'question')), 395 $namefield, 'id,' . $namefield); 396 foreach ($standardtags as $standardtag) { 397 $tagstrings[$standardtag->$namefield] = $standardtag->$namefield; 398 } 399 } 400 401 $options = [ 402 'tags' => true, 403 'multiple' => true, 404 'noselectionstring' => get_string('anytags', 'quiz'), 405 ]; 406 $mform->addElement('autocomplete', 'tags', get_string('tags'), $tagstrings, $options); 407 408 if (!$hastagcapability) { 409 $mform->hardFreeze('tags'); 410 } 411 412 if ($iseditingcontextcourseoractivity && !$iscourseoractivityquestion) { 413 // If the question is being edited in a course or activity context 414 // and the question isn't a course or activity level question then 415 // allow course tags to be added to the course. 416 $coursetagheader = get_string('questionformtagheader', 'core_question', 417 $editingcoursecontext->get_context_name(true)); 418 $mform->addElement('header', 'coursetagsheader', $coursetagheader); 419 $mform->addElement('autocomplete', 'coursetags', get_string('tags'), $tagstrings, $options); 420 421 if (!$hastagcapability) { 422 $mform->hardFreeze('coursetags'); 423 } 424 } 425 } 426 427 /** 428 * Add a set of form fields, obtained from get_per_answer_fields, to the form, 429 * one for each existing answer, with some blanks for some new ones. 430 * @param object $mform the form being built. 431 * @param $label the label to use for each option. 432 * @param $gradeoptions the possible grades for each answer. 433 * @param $minoptions the minimum number of answer blanks to display. 434 * Default QUESTION_NUMANS_START. 435 * @param $addoptions the number of answer blanks to add. Default QUESTION_NUMANS_ADD. 436 */ 437 protected function add_per_answer_fields(&$mform, $label, $gradeoptions, 438 $minoptions = QUESTION_NUMANS_START, $addoptions = QUESTION_NUMANS_ADD) { 439 $mform->addElement('header', 'answerhdr', 440 get_string('answers', 'question'), ''); 441 $mform->setExpanded('answerhdr', 1); 442 $answersoption = ''; 443 $repeatedoptions = array(); 444 $repeated = $this->get_per_answer_fields($mform, $label, $gradeoptions, 445 $repeatedoptions, $answersoption); 446 447 if (isset($this->question->options)) { 448 $repeatsatstart = count($this->question->options->$answersoption); 449 } else { 450 $repeatsatstart = $minoptions; 451 } 452 453 $this->repeat_elements($repeated, $repeatsatstart, $repeatedoptions, 454 'noanswers', 'addanswers', $addoptions, 455 $this->get_more_choices_string(), true); 456 } 457 458 /** 459 * Language string to use for 'Add {no} more {whatever we call answers}'. 460 */ 461 protected function get_more_choices_string() { 462 return get_string('addmorechoiceblanks', 'question'); 463 } 464 465 protected function add_combined_feedback_fields($withshownumpartscorrect = false) { 466 $mform = $this->_form; 467 468 $mform->addElement('header', 'combinedfeedbackhdr', 469 get_string('combinedfeedback', 'question')); 470 471 $fields = array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback'); 472 foreach ($fields as $feedbackname) { 473 $element = $mform->addElement('editor', $feedbackname, 474 get_string($feedbackname, 'question'), 475 array('rows' => 5), $this->editoroptions); 476 $mform->setType($feedbackname, PARAM_RAW); 477 // Using setValue() as setDefault() does not work for the editor class. 478 $element->setValue(array('text' => get_string($feedbackname.'default', 'question'))); 479 480 if ($withshownumpartscorrect && $feedbackname == 'partiallycorrectfeedback') { 481 $mform->addElement('advcheckbox', 'shownumcorrect', 482 get_string('options', 'question'), 483 get_string('shownumpartscorrectwhenfinished', 'question')); 484 $mform->setDefault('shownumcorrect', true); 485 } 486 } 487 } 488 489 /** 490 * Create the form elements required by one hint. 491 * @param string $withclearwrong whether this quesiton type uses the 'Clear wrong' option on hints. 492 * @param string $withshownumpartscorrect whether this quesiton type uses the 'Show num parts correct' option on hints. 493 * @return array form field elements for one hint. 494 */ 495 protected function get_hint_fields($withclearwrong = false, $withshownumpartscorrect = false) { 496 $mform = $this->_form; 497 498 $repeatedoptions = array(); 499 $repeated = array(); 500 $repeated[] = $mform->createElement('editor', 'hint', get_string('hintn', 'question'), 501 array('rows' => 5), $this->editoroptions); 502 $repeatedoptions['hint']['type'] = PARAM_RAW; 503 504 $optionelements = array(); 505 if ($withclearwrong) { 506 $optionelements[] = $mform->createElement('advcheckbox', 'hintclearwrong', 507 get_string('options', 'question'), get_string('clearwrongparts', 'question')); 508 } 509 if ($withshownumpartscorrect) { 510 $optionelements[] = $mform->createElement('advcheckbox', 'hintshownumcorrect', '', 511 get_string('shownumpartscorrect', 'question')); 512 } 513 514 if (count($optionelements)) { 515 $repeated[] = $mform->createElement('group', 'hintoptions', 516 get_string('hintnoptions', 'question'), $optionelements, null, false); 517 } 518 519 return array($repeated, $repeatedoptions); 520 } 521 522 protected function add_interactive_settings($withclearwrong = false, 523 $withshownumpartscorrect = false) { 524 $mform = $this->_form; 525 526 $mform->addElement('header', 'multitriesheader', 527 get_string('settingsformultipletries', 'question')); 528 529 $penalties = array( 530 1.0000000, 531 0.5000000, 532 0.3333333, 533 0.2500000, 534 0.2000000, 535 0.1000000, 536 0.0000000 537 ); 538 if (!empty($this->question->penalty) && !in_array($this->question->penalty, $penalties)) { 539 $penalties[] = $this->question->penalty; 540 sort($penalties); 541 } 542 $penaltyoptions = array(); 543 foreach ($penalties as $penalty) { 544 $penaltyoptions["{$penalty}"] = format_float(100 * $penalty, 5, true, true) . '%'; 545 } 546 $mform->addElement('select', 'penalty', 547 get_string('penaltyforeachincorrecttry', 'question'), $penaltyoptions); 548 $mform->addHelpButton('penalty', 'penaltyforeachincorrecttry', 'question'); 549 $mform->setDefault('penalty', $this->get_default_value('penalty', 0.3333333)); 550 551 if (isset($this->question->hints)) { 552 $counthints = count($this->question->hints); 553 } else { 554 $counthints = 0; 555 } 556 557 if ($this->question->formoptions->repeatelements) { 558 $repeatsatstart = max(self::DEFAULT_NUM_HINTS, $counthints); 559 } else { 560 $repeatsatstart = $counthints; 561 } 562 563 list($repeated, $repeatedoptions) = $this->get_hint_fields( 564 $withclearwrong, $withshownumpartscorrect); 565 $this->repeat_elements($repeated, $repeatsatstart, $repeatedoptions, 566 'numhints', 'addhint', 1, get_string('addanotherhint', 'question'), true); 567 } 568 569 public function set_data($question) { 570 question_bank::get_qtype($question->qtype)->set_default_options($question); 571 572 // Prepare question text. 573 $draftid = file_get_submitted_draft_itemid('questiontext'); 574 575 if (!empty($question->questiontext)) { 576 $questiontext = $question->questiontext; 577 } else { 578 $questiontext = $this->_form->getElement('questiontext')->getValue(); 579 $questiontext = $questiontext['text']; 580 } 581 $questiontext = file_prepare_draft_area($draftid, $this->context->id, 582 'question', 'questiontext', empty($question->id) ? null : (int) $question->id, 583 $this->fileoptions, $questiontext); 584 585 $question->questiontext = array(); 586 $question->questiontext['text'] = $questiontext; 587 $question->questiontext['format'] = empty($question->questiontextformat) ? 588 editors_get_preferred_format() : $question->questiontextformat; 589 $question->questiontext['itemid'] = $draftid; 590 591 // Prepare general feedback. 592 $draftid = file_get_submitted_draft_itemid('generalfeedback'); 593 594 if (empty($question->generalfeedback)) { 595 $generalfeedback = $this->_form->getElement('generalfeedback')->getValue(); 596 $question->generalfeedback = $generalfeedback['text']; 597 } 598 599 $feedback = file_prepare_draft_area($draftid, $this->context->id, 600 'question', 'generalfeedback', empty($question->id) ? null : (int) $question->id, 601 $this->fileoptions, $question->generalfeedback); 602 $question->generalfeedback = array(); 603 $question->generalfeedback['text'] = $feedback; 604 $question->generalfeedback['format'] = empty($question->generalfeedbackformat) ? 605 editors_get_preferred_format() : $question->generalfeedbackformat; 606 $question->generalfeedback['itemid'] = $draftid; 607 608 // Remove unnecessary trailing 0s form grade fields. 609 if (isset($question->defaultgrade)) { 610 $question->defaultgrade = 0 + $question->defaultgrade; 611 } 612 if (isset($question->penalty)) { 613 $question->penalty = 0 + $question->penalty; 614 } 615 616 // Set any options. 617 $extraquestionfields = question_bank::get_qtype($question->qtype)->extra_question_fields(); 618 if (is_array($extraquestionfields) && !empty($question->options)) { 619 array_shift($extraquestionfields); 620 foreach ($extraquestionfields as $field) { 621 if (property_exists($question->options, $field)) { 622 $question->$field = $question->options->$field; 623 } 624 } 625 } 626 627 // Subclass adds data_preprocessing code here. 628 $question = $this->data_preprocessing($question); 629 630 parent::set_data($question); 631 } 632 633 /** 634 * Perform an preprocessing needed on the data passed to {@link set_data()} 635 * before it is used to initialise the form. 636 * @param object $question the data being passed to the form. 637 * @return object $question the modified data. 638 */ 639 protected function data_preprocessing($question) { 640 return $question; 641 } 642 643 /** 644 * Perform the necessary preprocessing for the fields added by 645 * {@link add_per_answer_fields()}. 646 * @param object $question the data being passed to the form. 647 * @return object $question the modified data. 648 */ 649 protected function data_preprocessing_answers($question, $withanswerfiles = false) { 650 if (empty($question->options->answers)) { 651 return $question; 652 } 653 654 $key = 0; 655 foreach ($question->options->answers as $answer) { 656 if ($withanswerfiles) { 657 // Prepare the feedback editor to display files in draft area. 658 $draftitemid = file_get_submitted_draft_itemid('answer['.$key.']'); 659 $question->answer[$key]['text'] = file_prepare_draft_area( 660 $draftitemid, // Draftid. 661 $this->context->id, // Context. 662 'question', // Component. 663 'answer', // Filarea. 664 !empty($answer->id) ? (int) $answer->id : null, // Itemid. 665 $this->fileoptions, // Options. 666 $answer->answer // Text. 667 ); 668 $question->answer[$key]['itemid'] = $draftitemid; 669 $question->answer[$key]['format'] = $answer->answerformat; 670 } else { 671 $question->answer[$key] = $answer->answer; 672 } 673 674 $question->fraction[$key] = 0 + $answer->fraction; 675 $question->feedback[$key] = array(); 676 677 // Evil hack alert. Formslib can store defaults in two ways for 678 // repeat elements: 679 // ->_defaultValues['fraction[0]'] and 680 // ->_defaultValues['fraction'][0]. 681 // The $repeatedoptions['fraction']['default'] = 0 bit above means 682 // that ->_defaultValues['fraction[0]'] has already been set, but we 683 // are using object notation here, so we will be setting 684 // ->_defaultValues['fraction'][0]. That does not work, so we have 685 // to unset ->_defaultValues['fraction[0]']. 686 unset($this->_form->_defaultValues["fraction[{$key}]"]); 687 688 // Prepare the feedback editor to display files in draft area. 689 $draftitemid = file_get_submitted_draft_itemid('feedback['.$key.']'); 690 $question->feedback[$key]['text'] = file_prepare_draft_area( 691 $draftitemid, // Draftid. 692 $this->context->id, // Context. 693 'question', // Component. 694 'answerfeedback', // Filarea. 695 !empty($answer->id) ? (int) $answer->id : null, // Itemid. 696 $this->fileoptions, // Options. 697 $answer->feedback // Text. 698 ); 699 $question->feedback[$key]['itemid'] = $draftitemid; 700 $question->feedback[$key]['format'] = $answer->feedbackformat; 701 $key++; 702 } 703 704 // Now process extra answer fields. 705 $extraanswerfields = question_bank::get_qtype($question->qtype)->extra_answer_fields(); 706 if (is_array($extraanswerfields)) { 707 // Omit table name. 708 array_shift($extraanswerfields); 709 $question = $this->data_preprocessing_extra_answer_fields($question, $extraanswerfields); 710 } 711 712 return $question; 713 } 714 715 /** 716 * Perform the necessary preprocessing for the extra answer fields. 717 * 718 * Questions that do something not trivial when editing extra answer fields 719 * will want to override this. 720 * @param object $question the data being passed to the form. 721 * @param array $extraanswerfields extra answer fields (without table name). 722 * @return object $question the modified data. 723 */ 724 protected function data_preprocessing_extra_answer_fields($question, $extraanswerfields) { 725 // Setting $question->$field[$key] won't work in PHP, so we need set an array of answer values to $question->$field. 726 // As we may have several extra fields with data for several answers in each, we use an array of arrays. 727 // Index in $extrafieldsdata is an extra answer field name, value - array of it's data for each answer. 728 $extrafieldsdata = array(); 729 // First, prepare an array if empty arrays for each extra answer fields data. 730 foreach ($extraanswerfields as $field) { 731 $extrafieldsdata[$field] = array(); 732 } 733 734 // Fill arrays with data from $question->options->answers. 735 $key = 0; 736 foreach ($question->options->answers as $answer) { 737 foreach ($extraanswerfields as $field) { 738 // See hack comment in {@link data_preprocessing_answers()}. 739 unset($this->_form->_defaultValues["{$field}[{$key}]"]); 740 $extrafieldsdata[$field][$key] = $this->data_preprocessing_extra_answer_field($answer, $field); 741 } 742 $key++; 743 } 744 745 // Set this data in the $question object. 746 foreach ($extraanswerfields as $field) { 747 $question->$field = $extrafieldsdata[$field]; 748 } 749 return $question; 750 } 751 752 /** 753 * Perfmorm preprocessing for particular extra answer field. 754 * 755 * Questions with non-trivial DB - form element relationship will 756 * want to override this. 757 * @param object $answer an answer object to get extra field from. 758 * @param string $field extra answer field name. 759 * @return field value to be set to the form. 760 */ 761 protected function data_preprocessing_extra_answer_field($answer, $field) { 762 return $answer->$field; 763 } 764 765 /** 766 * Perform the necessary preprocessing for the fields added by 767 * {@link add_combined_feedback_fields()}. 768 * @param object $question the data being passed to the form. 769 * @return object $question the modified data. 770 */ 771 protected function data_preprocessing_combined_feedback($question, 772 $withshownumcorrect = false) { 773 if (empty($question->options)) { 774 return $question; 775 } 776 777 $fields = array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback'); 778 foreach ($fields as $feedbackname) { 779 $draftid = file_get_submitted_draft_itemid($feedbackname); 780 $feedback = array(); 781 $feedback['text'] = file_prepare_draft_area( 782 $draftid, // Draftid. 783 $this->context->id, // Context. 784 'question', // Component. 785 $feedbackname, // Filarea. 786 !empty($question->id) ? (int) $question->id : null, // Itemid. 787 $this->fileoptions, // Options. 788 $question->options->$feedbackname // Text. 789 ); 790 $feedbackformat = $feedbackname . 'format'; 791 $feedback['format'] = $question->options->$feedbackformat; 792 $feedback['itemid'] = $draftid; 793 794 $question->$feedbackname = $feedback; 795 } 796 797 if ($withshownumcorrect) { 798 $question->shownumcorrect = $question->options->shownumcorrect; 799 } 800 801 return $question; 802 } 803 804 /** 805 * Perform the necessary preprocessing for the hint fields. 806 * @param object $question the data being passed to the form. 807 * @return object $question the modified data. 808 */ 809 protected function data_preprocessing_hints($question, $withclearwrong = false, 810 $withshownumpartscorrect = false) { 811 if (empty($question->hints)) { 812 return $question; 813 } 814 815 $key = 0; 816 foreach ($question->hints as $hint) { 817 $question->hint[$key] = array(); 818 819 // Prepare feedback editor to display files in draft area. 820 $draftitemid = file_get_submitted_draft_itemid('hint['.$key.']'); 821 $question->hint[$key]['text'] = file_prepare_draft_area( 822 $draftitemid, // Draftid. 823 $this->context->id, // Context. 824 'question', // Component. 825 'hint', // Filarea. 826 !empty($hint->id) ? (int) $hint->id : null, // Itemid. 827 $this->fileoptions, // Options. 828 $hint->hint // Text. 829 ); 830 $question->hint[$key]['itemid'] = $draftitemid; 831 $question->hint[$key]['format'] = $hint->hintformat; 832 $key++; 833 834 if ($withclearwrong) { 835 $question->hintclearwrong[] = $hint->clearwrong; 836 } 837 if ($withshownumpartscorrect) { 838 $question->hintshownumcorrect[] = $hint->shownumcorrect; 839 } 840 } 841 842 return $question; 843 } 844 845 public function validation($fromform, $files) { 846 global $DB; 847 848 $errors = parent::validation($fromform, $files); 849 850 // Make sure that the user can edit the question. 851 if (empty($fromform['makecopy']) && isset($this->question->id) 852 && !$this->question->formoptions->canedit) { 853 $errors['currentgrp'] = get_string('nopermissionedit', 'question'); 854 } 855 856 // Category. 857 if (empty($fromform['category'])) { 858 // User has provided an invalid category. 859 $errors['category'] = get_string('required'); 860 } 861 862 // Default mark. 863 if (array_key_exists('defaultmark', $fromform) && $fromform['defaultmark'] < 0) { 864 $errors['defaultmark'] = get_string('defaultmarkmustbepositive', 'question'); 865 } 866 867 // Can only have one idnumber per category. 868 if (strpos($fromform['category'], ',') !== false) { 869 list($category, $categorycontextid) = explode(',', $fromform['category']); 870 } else { 871 $category = $fromform['category']; 872 } 873 if (isset($fromform['idnumber']) && ((string) $fromform['idnumber'] !== '')) { 874 if (empty($fromform['usecurrentcat']) && !empty($fromform['categorymoveto'])) { 875 $categoryinfo = $fromform['categorymoveto']; 876 } else { 877 $categoryinfo = $fromform['category']; 878 } 879 list($categoryid, $notused) = explode(',', $categoryinfo); 880 $conditions = 'questioncategoryid = ? AND idnumber = ?'; 881 $params = [$categoryid, $fromform['idnumber']]; 882 if (!empty($this->question->id)) { 883 // Get the question bank entry id to not check the idnumber for the same bank entry. 884 $sql = "SELECT DISTINCT qbe.id 885 FROM {question_versions} qv 886 JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid 887 WHERE qv.questionid = ?"; 888 $bankentry = $DB->get_record_sql($sql, ['id' => $this->question->id]); 889 $conditions .= ' AND id <> ?'; 890 $params[] = $bankentry->id; 891 } 892 893 if ($DB->record_exists_select('question_bank_entries', $conditions, $params)) { 894 $errors['idnumber'] = get_string('idnumbertaken', 'error'); 895 } 896 } 897 898 if ($this->customfieldpluginenabled) { 899 // Add the custom field validation. 900 $errors = array_merge($errors, $this->customfieldhandler->instance_form_validation($fromform, $files)); 901 } 902 return $errors; 903 } 904 905 /** 906 * Override this in the subclass to question type name. 907 * @return the question type name, should be the same as the name() method 908 * in the question type class. 909 */ 910 public abstract function qtype(); 911 912 /** 913 * Returns an array of editor options with collapsed options turned off. 914 * @deprecated since 2.6 915 * @return array 916 */ 917 protected function get_non_collabsible_editor_options() { 918 debugging('get_non_collabsible_editor_options() is deprecated, use $this->editoroptions instead.', DEBUG_DEVELOPER); 919 return $this->editoroptions; 920 } 921 922 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body