Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

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  }