PageRenderTime 21ms CodeModel.GetById 25ms RepoModel.GetById 1ms app.codeStats 0ms

/question/type/edit_question_form.php

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