Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

Differences Between: [Versions 310 and 311] [Versions 311 and 400] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]

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