Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403] [Versions 402 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', 'mdlscrollto'); 56 $mform->setType('mdlscrollto', 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 && \core\plugininfo\qbank::is_plugin_enabled('qbank_tagquestion')) { 260 $this->add_tag_fields($mform); 261 } 262 263 if ($this->customfieldpluginenabled) { 264 // Add custom fields to the form. 265 $this->customfieldhandler = qbank_customfields\customfield\question_handler::create(); 266 $this->customfieldhandler->set_parent_context($this->categorycontext); // For question handler only. 267 $this->customfieldhandler->instance_form_definition($mform, empty($this->question->id) ? 0 : $this->question->id); 268 } 269 270 $this->add_hidden_fields(); 271 272 $mform->addElement('hidden', 'qtype'); 273 $mform->setType('qtype', PARAM_ALPHA); 274 275 $mform->addElement('hidden', 'makecopy'); 276 $mform->setType('makecopy', PARAM_INT); 277 278 $buttonarray = array(); 279 $buttonarray[] = $mform->createElement('submit', 'updatebutton', 280 get_string('savechangesandcontinueediting', 'question')); 281 if ($this->can_preview()) { 282 if (\core\plugininfo\qbank::is_plugin_enabled('qbank_previewquestion')) { 283 $previewlink = $PAGE->get_renderer('qbank_previewquestion')->question_preview_link( 284 $this->question->id, $this->context, true); 285 $buttonarray[] = $mform->createElement('static', 'previewlink', '', $previewlink); 286 } 287 } 288 289 $mform->addGroup($buttonarray, 'updatebuttonar', '', array(' '), false); 290 $mform->closeHeaderBefore('updatebuttonar'); 291 292 $this->add_action_buttons(true, get_string('savechanges')); 293 294 if ((!empty($this->question->id)) && (!($this->question->formoptions->canedit || 295 $this->question->formoptions->cansaveasnew))) { 296 $mform->hardFreezeAllVisibleExcept(array('categorymoveto', 'buttonar', 'currentgrp')); 297 } 298 } 299 300 /** 301 * Add any question-type specific form fields. 302 * 303 * @param object $mform the form being built. 304 */ 305 protected function definition_inner($mform) { 306 // By default, do nothing. 307 } 308 309 /** 310 * Tweak the form with values provided by custom fields in use. 311 */ 312 public function definition_after_data() { 313 $mform = $this->_form; 314 if ($this->customfieldpluginenabled) { 315 $this->customfieldhandler->instance_form_definition_after_data($mform, 316 empty($this->question->id) ? 0 : $this->question->id); 317 } 318 } 319 320 /** 321 * Is the question being edited in a state where it can be previewed? 322 * @return bool whether to show the preview link. 323 */ 324 protected function can_preview() { 325 return empty($this->question->beingcopied) && !empty($this->question->id) && 326 $this->question->formoptions->canedit; 327 } 328 329 /** 330 * Get the list of form elements to repeat, one for each answer. 331 * @param object $mform the form being built. 332 * @param $label the label to use for each option. 333 * @param $gradeoptions the possible grades for each answer. 334 * @param $repeatedoptions reference to array of repeated options to fill 335 * @param $answersoption reference to return the name of $question->options 336 * field holding an array of answers 337 * @return array of form fields. 338 */ 339 protected function get_per_answer_fields($mform, $label, $gradeoptions, 340 &$repeatedoptions, &$answersoption) { 341 $repeated = array(); 342 $answeroptions = array(); 343 $answeroptions[] = $mform->createElement('text', 'answer', 344 $label, array('size' => 40)); 345 $answeroptions[] = $mform->createElement('select', 'fraction', 346 get_string('gradenoun'), $gradeoptions); 347 $repeated[] = $mform->createElement('group', 'answeroptions', 348 $label, $answeroptions, null, false); 349 $repeated[] = $mform->createElement('editor', 'feedback', 350 get_string('feedback', 'question'), array('rows' => 5), $this->editoroptions); 351 $repeatedoptions['answer']['type'] = PARAM_RAW; 352 $repeatedoptions['fraction']['default'] = 0; 353 $answersoption = 'answers'; 354 return $repeated; 355 } 356 357 /** 358 * Add the tag and course tag fields to the mform. 359 * 360 * If the form is being built in a course context then add the field 361 * for course tags. 362 * 363 * If the question category doesn't belong to a course context or we 364 * aren't editing in a course context then add the tags element to allow 365 * tags to be added to the question category context. 366 * 367 * @param object $mform The form being built 368 */ 369 protected function add_tag_fields($mform) { 370 global $CFG, $DB; 371 372 $hastagcapability = question_has_capability_on($this->question, 'tag'); 373 // Is the question category in a course context? 374 $qcontext = $this->categorycontext; 375 $qcoursecontext = $qcontext->get_course_context(false); 376 $iscourseoractivityquestion = !empty($qcoursecontext); 377 // Is the current context we're editing in a course context? 378 $editingcontext = $this->contexts->lowest(); 379 $editingcoursecontext = $editingcontext->get_course_context(false); 380 $iseditingcontextcourseoractivity = !empty($editingcoursecontext); 381 382 $mform->addElement('header', 'tagsheader', get_string('tags')); 383 $tags = \core_tag_tag::get_tags_by_area_in_contexts('core_question', 'question', $this->contexts->all()); 384 $tagstrings = []; 385 foreach ($tags as $tag) { 386 $tagstrings[$tag->name] = $tag->name; 387 } 388 389 $showstandard = core_tag_area::get_showstandard('core_question', 'question'); 390 if ($showstandard != core_tag_tag::HIDE_STANDARD) { 391 $namefield = empty($CFG->keeptagnamecase) ? 'name' : 'rawname'; 392 $standardtags = $DB->get_records('tag', 393 array('isstandard' => 1, 'tagcollid' => core_tag_area::get_collection('core', 'question')), 394 $namefield, 'id,' . $namefield); 395 foreach ($standardtags as $standardtag) { 396 $tagstrings[$standardtag->$namefield] = $standardtag->$namefield; 397 } 398 } 399 400 $options = [ 401 'tags' => true, 402 'multiple' => true, 403 'noselectionstring' => get_string('anytags', 'quiz'), 404 ]; 405 $mform->addElement('autocomplete', 'tags', get_string('tags'), $tagstrings, $options); 406 407 if (!$hastagcapability) { 408 $mform->hardFreeze('tags'); 409 } 410 411 if ($iseditingcontextcourseoractivity && !$iscourseoractivityquestion) { 412 // If the question is being edited in a course or activity context 413 // and the question isn't a course or activity level question then 414 // allow course tags to be added to the course. 415 $coursetagheader = get_string('questionformtagheader', 'core_question', 416 $editingcoursecontext->get_context_name(true)); 417 $mform->addElement('header', 'coursetagsheader', $coursetagheader); 418 $mform->addElement('autocomplete', 'coursetags', get_string('tags'), $tagstrings, $options); 419 420 if (!$hastagcapability) { 421 $mform->hardFreeze('coursetags'); 422 } 423 } 424 } 425 426 /** 427 * Add a set of form fields, obtained from get_per_answer_fields, to the form, 428 * one for each existing answer, with some blanks for some new ones. 429 * @param object $mform the form being built. 430 * @param $label the label to use for each option. 431 * @param $gradeoptions the possible grades for each answer. 432 * @param $minoptions the minimum number of answer blanks to display. 433 * Default QUESTION_NUMANS_START. 434 * @param $addoptions the number of answer blanks to add. Default QUESTION_NUMANS_ADD. 435 */ 436 protected function add_per_answer_fields(&$mform, $label, $gradeoptions, 437 $minoptions = QUESTION_NUMANS_START, $addoptions = QUESTION_NUMANS_ADD) { 438 $mform->addElement('header', 'answerhdr', 439 get_string('answers', 'question'), ''); 440 $mform->setExpanded('answerhdr', 1); 441 $answersoption = ''; 442 $repeatedoptions = array(); 443 $repeated = $this->get_per_answer_fields($mform, $label, $gradeoptions, 444 $repeatedoptions, $answersoption); 445 446 if (isset($this->question->options)) { 447 $repeatsatstart = count($this->question->options->$answersoption); 448 } else { 449 $repeatsatstart = $minoptions; 450 } 451 452 $this->repeat_elements($repeated, $repeatsatstart, $repeatedoptions, 453 'noanswers', 'addanswers', $addoptions, 454 $this->get_more_choices_string(), true); 455 } 456 457 /** 458 * Language string to use for 'Add {no} more {whatever we call answers}'. 459 */ 460 protected function get_more_choices_string() { 461 return get_string('addmorechoiceblanks', 'question'); 462 } 463 464 protected function add_combined_feedback_fields($withshownumpartscorrect = false) { 465 $mform = $this->_form; 466 467 $mform->addElement('header', 'combinedfeedbackhdr', 468 get_string('combinedfeedback', 'question')); 469 470 $fields = array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback'); 471 foreach ($fields as $feedbackname) { 472 $element = $mform->addElement('editor', $feedbackname, 473 get_string($feedbackname, 'question'), 474 array('rows' => 5), $this->editoroptions); 475 $mform->setType($feedbackname, PARAM_RAW); 476 // Using setValue() as setDefault() does not work for the editor class. 477 $element->setValue(array('text' => get_string($feedbackname.'default', 'question'))); 478 479 if ($withshownumpartscorrect && $feedbackname == 'partiallycorrectfeedback') { 480 $mform->addElement('advcheckbox', 'shownumcorrect', 481 get_string('options', 'question'), 482 get_string('shownumpartscorrectwhenfinished', 'question')); 483 $mform->setDefault('shownumcorrect', true); 484 } 485 } 486 } 487 488 /** 489 * Create the form elements required by one hint. 490 * @param string $withclearwrong whether this quesiton type uses the 'Clear wrong' option on hints. 491 * @param string $withshownumpartscorrect whether this quesiton type uses the 'Show num parts correct' option on hints. 492 * @return array form field elements for one hint. 493 */ 494 protected function get_hint_fields($withclearwrong = false, $withshownumpartscorrect = false) { 495 $mform = $this->_form; 496 497 $repeatedoptions = array(); 498 $repeated = array(); 499 $repeated[] = $mform->createElement('editor', 'hint', get_string('hintn', 'question'), 500 array('rows' => 5), $this->editoroptions); 501 $repeatedoptions['hint']['type'] = PARAM_RAW; 502 503 $optionelements = array(); 504 if ($withclearwrong) { 505 $optionelements[] = $mform->createElement('advcheckbox', 'hintclearwrong', 506 get_string('options', 'question'), get_string('clearwrongparts', 'question')); 507 } 508 if ($withshownumpartscorrect) { 509 $optionelements[] = $mform->createElement('advcheckbox', 'hintshownumcorrect', '', 510 get_string('shownumpartscorrect', 'question')); 511 } 512 513 if (count($optionelements)) { 514 $repeated[] = $mform->createElement('group', 'hintoptions', 515 get_string('hintnoptions', 'question'), $optionelements, null, false); 516 } 517 518 return array($repeated, $repeatedoptions); 519 } 520 521 protected function add_interactive_settings($withclearwrong = false, 522 $withshownumpartscorrect = false) { 523 $mform = $this->_form; 524 525 $mform->addElement('header', 'multitriesheader', 526 get_string('settingsformultipletries', 'question')); 527 528 $penalties = array( 529 1.0000000, 530 0.5000000, 531 0.3333333, 532 0.2500000, 533 0.2000000, 534 0.1000000, 535 0.0000000 536 ); 537 if (!empty($this->question->penalty) && !in_array($this->question->penalty, $penalties)) { 538 $penalties[] = $this->question->penalty; 539 sort($penalties); 540 } 541 $penaltyoptions = array(); 542 foreach ($penalties as $penalty) { 543 $penaltyoptions["{$penalty}"] = format_float(100 * $penalty, 5, true, true) . '%'; 544 } 545 $mform->addElement('select', 'penalty', 546 get_string('penaltyforeachincorrecttry', 'question'), $penaltyoptions); 547 $mform->addHelpButton('penalty', 'penaltyforeachincorrecttry', 'question'); 548 $mform->setDefault('penalty', $this->get_default_value('penalty', 0.3333333)); 549 550 if (isset($this->question->hints)) { 551 $counthints = count($this->question->hints); 552 } else { 553 $counthints = 0; 554 } 555 556 if ($this->question->formoptions->repeatelements) { 557 $repeatsatstart = max(self::DEFAULT_NUM_HINTS, $counthints); 558 } else { 559 $repeatsatstart = $counthints; 560 } 561 562 list($repeated, $repeatedoptions) = $this->get_hint_fields( 563 $withclearwrong, $withshownumpartscorrect); 564 $this->repeat_elements($repeated, $repeatsatstart, $repeatedoptions, 565 'numhints', 'addhint', 1, get_string('addanotherhint', 'question'), true); 566 } 567 568 public function set_data($question) { 569 question_bank::get_qtype($question->qtype)->set_default_options($question); 570 571 // Prepare question text. 572 $draftid = file_get_submitted_draft_itemid('questiontext'); 573 574 if (!empty($question->questiontext)) { 575 $questiontext = $question->questiontext; 576 } else { 577 $questiontext = $this->_form->getElement('questiontext')->getValue(); 578 $questiontext = $questiontext['text']; 579 } 580 $questiontext = file_prepare_draft_area($draftid, $this->context->id, 581 'question', 'questiontext', empty($question->id) ? null : (int) $question->id, 582 $this->fileoptions, $questiontext); 583 584 $question->questiontext = array(); 585 $question->questiontext['text'] = $questiontext; 586 $question->questiontext['format'] = empty($question->questiontextformat) ? 587 editors_get_preferred_format() : $question->questiontextformat; 588 $question->questiontext['itemid'] = $draftid; 589 590 // Prepare general feedback. 591 $draftid = file_get_submitted_draft_itemid('generalfeedback'); 592 593 if (empty($question->generalfeedback)) { 594 $generalfeedback = $this->_form->getElement('generalfeedback')->getValue(); 595 $question->generalfeedback = $generalfeedback['text']; 596 } 597 598 $feedback = file_prepare_draft_area($draftid, $this->context->id, 599 'question', 'generalfeedback', empty($question->id) ? null : (int) $question->id, 600 $this->fileoptions, $question->generalfeedback); 601 $question->generalfeedback = array(); 602 $question->generalfeedback['text'] = $feedback; 603 $question->generalfeedback['format'] = empty($question->generalfeedbackformat) ? 604 editors_get_preferred_format() : $question->generalfeedbackformat; 605 $question->generalfeedback['itemid'] = $draftid; 606 607 // Remove unnecessary trailing 0s form grade fields. 608 if (isset($question->defaultgrade)) { 609 $question->defaultgrade = 0 + $question->defaultgrade; 610 } 611 if (isset($question->penalty)) { 612 $question->penalty = 0 + $question->penalty; 613 } 614 615 // Set any options. 616 $extraquestionfields = question_bank::get_qtype($question->qtype)->extra_question_fields(); 617 if (is_array($extraquestionfields) && !empty($question->options)) { 618 array_shift($extraquestionfields); 619 foreach ($extraquestionfields as $field) { 620 if (property_exists($question->options, $field)) { 621 $question->$field = $question->options->$field; 622 } 623 } 624 } 625 626 // Subclass adds data_preprocessing code here. 627 $question = $this->data_preprocessing($question); 628 629 parent::set_data($question); 630 } 631 632 /** 633 * Perform an preprocessing needed on the data passed to {@link set_data()} 634 * before it is used to initialise the form. 635 * @param object $question the data being passed to the form. 636 * @return object $question the modified data. 637 */ 638 protected function data_preprocessing($question) { 639 return $question; 640 } 641 642 /** 643 * Perform the necessary preprocessing for the fields added by 644 * {@link add_per_answer_fields()}. 645 * @param object $question the data being passed to the form. 646 * @return object $question the modified data. 647 */ 648 protected function data_preprocessing_answers($question, $withanswerfiles = false) { 649 if (empty($question->options->answers)) { 650 return $question; 651 } 652 653 $key = 0; 654 foreach ($question->options->answers as $answer) { 655 if ($withanswerfiles) { 656 // Prepare the feedback editor to display files in draft area. 657 $draftitemid = file_get_submitted_draft_itemid('answer['.$key.']'); 658 $question->answer[$key]['text'] = file_prepare_draft_area( 659 $draftitemid, // Draftid. 660 $this->context->id, // Context. 661 'question', // Component. 662 'answer', // Filarea. 663 !empty($answer->id) ? (int) $answer->id : null, // Itemid. 664 $this->fileoptions, // Options. 665 $answer->answer // Text. 666 ); 667 $question->answer[$key]['itemid'] = $draftitemid; 668 $question->answer[$key]['format'] = $answer->answerformat; 669 } else { 670 $question->answer[$key] = $answer->answer; 671 } 672 673 $question->fraction[$key] = 0 + $answer->fraction; 674 $question->feedback[$key] = array(); 675 676 // Evil hack alert. Formslib can store defaults in two ways for 677 // repeat elements: 678 // ->_defaultValues['fraction[0]'] and 679 // ->_defaultValues['fraction'][0]. 680 // The $repeatedoptions['fraction']['default'] = 0 bit above means 681 // that ->_defaultValues['fraction[0]'] has already been set, but we 682 // are using object notation here, so we will be setting 683 // ->_defaultValues['fraction'][0]. That does not work, so we have 684 // to unset ->_defaultValues['fraction[0]']. 685 unset($this->_form->_defaultValues["fraction[{$key}]"]); 686 687 // Prepare the feedback editor to display files in draft area. 688 $draftitemid = file_get_submitted_draft_itemid('feedback['.$key.']'); 689 $question->feedback[$key]['text'] = file_prepare_draft_area( 690 $draftitemid, // Draftid. 691 $this->context->id, // Context. 692 'question', // Component. 693 'answerfeedback', // Filarea. 694 !empty($answer->id) ? (int) $answer->id : null, // Itemid. 695 $this->fileoptions, // Options. 696 $answer->feedback // Text. 697 ); 698 $question->feedback[$key]['itemid'] = $draftitemid; 699 $question->feedback[$key]['format'] = $answer->feedbackformat; 700 $key++; 701 } 702 703 // Now process extra answer fields. 704 $extraanswerfields = question_bank::get_qtype($question->qtype)->extra_answer_fields(); 705 if (is_array($extraanswerfields)) { 706 // Omit table name. 707 array_shift($extraanswerfields); 708 $question = $this->data_preprocessing_extra_answer_fields($question, $extraanswerfields); 709 } 710 711 return $question; 712 } 713 714 /** 715 * Perform the necessary preprocessing for the extra answer fields. 716 * 717 * Questions that do something not trivial when editing extra answer fields 718 * will want to override this. 719 * @param object $question the data being passed to the form. 720 * @param array $extraanswerfields extra answer fields (without table name). 721 * @return object $question the modified data. 722 */ 723 protected function data_preprocessing_extra_answer_fields($question, $extraanswerfields) { 724 // Setting $question->$field[$key] won't work in PHP, so we need set an array of answer values to $question->$field. 725 // As we may have several extra fields with data for several answers in each, we use an array of arrays. 726 // Index in $extrafieldsdata is an extra answer field name, value - array of it's data for each answer. 727 $extrafieldsdata = array(); 728 // First, prepare an array if empty arrays for each extra answer fields data. 729 foreach ($extraanswerfields as $field) { 730 $extrafieldsdata[$field] = array(); 731 } 732 733 // Fill arrays with data from $question->options->answers. 734 $key = 0; 735 foreach ($question->options->answers as $answer) { 736 foreach ($extraanswerfields as $field) { 737 // See hack comment in {@link data_preprocessing_answers()}. 738 unset($this->_form->_defaultValues["{$field}[{$key}]"]); 739 $extrafieldsdata[$field][$key] = $this->data_preprocessing_extra_answer_field($answer, $field); 740 } 741 $key++; 742 } 743 744 // Set this data in the $question object. 745 foreach ($extraanswerfields as $field) { 746 $question->$field = $extrafieldsdata[$field]; 747 } 748 return $question; 749 } 750 751 /** 752 * Perfmorm preprocessing for particular extra answer field. 753 * 754 * Questions with non-trivial DB - form element relationship will 755 * want to override this. 756 * @param object $answer an answer object to get extra field from. 757 * @param string $field extra answer field name. 758 * @return field value to be set to the form. 759 */ 760 protected function data_preprocessing_extra_answer_field($answer, $field) { 761 return $answer->$field; 762 } 763 764 /** 765 * Perform the necessary preprocessing for the fields added by 766 * {@link add_combined_feedback_fields()}. 767 * @param object $question the data being passed to the form. 768 * @return object $question the modified data. 769 */ 770 protected function data_preprocessing_combined_feedback($question, 771 $withshownumcorrect = false) { 772 if (empty($question->options)) { 773 return $question; 774 } 775 776 $fields = array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback'); 777 foreach ($fields as $feedbackname) { 778 $draftid = file_get_submitted_draft_itemid($feedbackname); 779 $feedback = array(); 780 $feedback['text'] = file_prepare_draft_area( 781 $draftid, // Draftid. 782 $this->context->id, // Context. 783 'question', // Component. 784 $feedbackname, // Filarea. 785 !empty($question->id) ? (int) $question->id : null, // Itemid. 786 $this->fileoptions, // Options. 787 $question->options->$feedbackname // Text. 788 ); 789 $feedbackformat = $feedbackname . 'format'; 790 $feedback['format'] = $question->options->$feedbackformat; 791 $feedback['itemid'] = $draftid; 792 793 $question->$feedbackname = $feedback; 794 } 795 796 if ($withshownumcorrect) { 797 $question->shownumcorrect = $question->options->shownumcorrect; 798 } 799 800 return $question; 801 } 802 803 /** 804 * Perform the necessary preprocessing for the hint fields. 805 * @param object $question the data being passed to the form. 806 * @return object $question the modified data. 807 */ 808 protected function data_preprocessing_hints($question, $withclearwrong = false, 809 $withshownumpartscorrect = false) { 810 if (empty($question->hints)) { 811 return $question; 812 } 813 814 $key = 0; 815 foreach ($question->hints as $hint) { 816 $question->hint[$key] = array(); 817 818 // Prepare feedback editor to display files in draft area. 819 $draftitemid = file_get_submitted_draft_itemid('hint['.$key.']'); 820 $question->hint[$key]['text'] = file_prepare_draft_area( 821 $draftitemid, // Draftid. 822 $this->context->id, // Context. 823 'question', // Component. 824 'hint', // Filarea. 825 !empty($hint->id) ? (int) $hint->id : null, // Itemid. 826 $this->fileoptions, // Options. 827 $hint->hint // Text. 828 ); 829 $question->hint[$key]['itemid'] = $draftitemid; 830 $question->hint[$key]['format'] = $hint->hintformat; 831 $key++; 832 833 if ($withclearwrong) { 834 $question->hintclearwrong[] = $hint->clearwrong; 835 } 836 if ($withshownumpartscorrect) { 837 $question->hintshownumcorrect[] = $hint->shownumcorrect; 838 } 839 } 840 841 return $question; 842 } 843 844 public function validation($fromform, $files) { 845 global $DB; 846 847 $errors = parent::validation($fromform, $files); 848 849 // Make sure that the user can edit the question. 850 if (empty($fromform['makecopy']) && isset($this->question->id) 851 && !$this->question->formoptions->canedit) { 852 $errors['currentgrp'] = get_string('nopermissionedit', 'question'); 853 } 854 855 // Category. 856 if (empty($fromform['category'])) { 857 // User has provided an invalid category. 858 $errors['category'] = get_string('required'); 859 } 860 861 // Default mark. 862 if (array_key_exists('defaultmark', $fromform) && $fromform['defaultmark'] < 0) { 863 $errors['defaultmark'] = get_string('defaultmarkmustbepositive', 'question'); 864 } 865 866 // Can only have one idnumber per category. 867 if (strpos($fromform['category'], ',') !== false) { 868 list($category, $categorycontextid) = explode(',', $fromform['category']); 869 } else { 870 $category = $fromform['category']; 871 } 872 if (isset($fromform['idnumber']) && ((string) $fromform['idnumber'] !== '')) { 873 if (empty($fromform['usecurrentcat']) && !empty($fromform['categorymoveto'])) { 874 $categoryinfo = $fromform['categorymoveto']; 875 } else { 876 $categoryinfo = $fromform['category']; 877 } 878 list($categoryid, $notused) = explode(',', $categoryinfo); 879 $conditions = 'questioncategoryid = ? AND idnumber = ?'; 880 $params = [$categoryid, $fromform['idnumber']]; 881 if (!empty($this->question->id)) { 882 // Get the question bank entry id to not check the idnumber for the same bank entry. 883 $sql = "SELECT DISTINCT qbe.id 884 FROM {question_versions} qv 885 JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid 886 WHERE qv.questionid = ?"; 887 $bankentry = $DB->get_record_sql($sql, ['id' => $this->question->id]); 888 $conditions .= ' AND id <> ?'; 889 $params[] = $bankentry->id; 890 } 891 892 if ($DB->record_exists_select('question_bank_entries', $conditions, $params)) { 893 $errors['idnumber'] = get_string('idnumbertaken', 'error'); 894 } 895 } 896 897 if ($this->customfieldpluginenabled) { 898 // Add the custom field validation. 899 $errors = array_merge($errors, $this->customfieldhandler->instance_form_validation($fromform, $files)); 900 } 901 return $errors; 902 } 903 904 /** 905 * Override this in the subclass to question type name. 906 * @return the question type name, should be the same as the name() method 907 * in the question type class. 908 */ 909 public abstract function qtype(); 910 911 /** 912 * Returns an array of editor options with collapsed options turned off. 913 * @deprecated since 2.6 914 * @return array 915 */ 916 protected function get_non_collabsible_editor_options() { 917 debugging('get_non_collabsible_editor_options() is deprecated, use $this->editoroptions instead.', DEBUG_DEVELOPER); 918 return $this->editoroptions; 919 } 920 921 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body