PageRenderTime 51ms CodeModel.GetById 15ms RepoModel.GetById 1ms app.codeStats 0ms

/question/type/edit_question_form.php

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