MDL-46148 qtype_calculated: function to validate equations in text.
[moodle.git] / question / type / calculated / edit_calculated_form.php
CommitLineData
aeb15530 1<?php
fe6ce234
DC
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
271ffe3f 17/**
18 * Defines the editing form for the calculated question type.
19 *
b04a4319
TH
20 * @package qtype
21 * @subpackage calculated
22 * @copyright 2007 Jamie Pratt me@jamiep.org
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
271ffe3f 24 */
25
b04a4319 26
a17b297d
TH
27defined('MOODLE_INTERNAL') || die();
28
18f9b2d2
TH
29require_once($CFG->dirroot . '/question/type/numerical/edit_numerical_form.php');
30
a17b297d 31
271ffe3f 32/**
d3603157 33 * Calculated question type editing form definition.
b04a4319
TH
34 *
35 * @copyright 2007 Jamie Pratt me@jamiep.org
36 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
271ffe3f 37 */
18f9b2d2 38class qtype_calculated_edit_form extends qtype_numerical_edit_form {
9aa022fe 39 /**
40 * Handle to the question type for this question.
41 *
f184c65c 42 * @var qtype_calculated
9aa022fe 43 */
d90b016b 44 public $qtypeobj;
fe6ce234
DC
45 public $questiondisplay;
46 public $activecategory;
47 public $categorychanged = false;
d90b016b 48 public $initialname = '';
fe6ce234
DC
49 public $reload = false;
50
f184c65c
TH
51 public function __construct($submiturl, $question, $category, $contexts,
52 $formeditable = true) {
53 global $CFG, $DB;
d90b016b 54 $this->question = $question;
7d087744 55 $this->reload = optional_param('reload', false, PARAM_BOOL);
2aef1fe5 56
c7218aef 57 if (!$this->reload) { // Use database data as this is first pass.
f184c65c 58 if (isset($this->question->id)) {
c7218aef 59 // Remove prefix #{..}# if exists.
f184c65c 60 $this->initialname = $question->name;
d90b016b 61 $regs= array();
f184c65c 62 if (preg_match('~#\{([^[:space:]]*)#~', $question->name , $regs)) {
d90b016b 63 $question->name = str_replace($regs[0], '', $question->name);
fe6ce234 64 };
d90b016b 65 }
fe6ce234 66 }
f184c65c 67 parent::__construct($submiturl, $question, $category, $contexts, $formeditable);
d90b016b 68 }
fe6ce234 69
f184c65c
TH
70 public function get_per_answer_fields($mform, $label, $gradeoptions,
71 &$repeatedoptions, &$answersoption) {
fb17f129
TH
72 $repeated = parent::get_per_answer_fields($mform, $label, $gradeoptions,
73 $repeatedoptions, $answersoption);
74
c7218aef
CC
75 // Reorganise answer options group. 0 is the answer. 1 is tolerance. 2 is Grade.
76 $answeroptions = $repeated[0]->getElements();
77 // Tolerance field will be part of its own group.
78 $tolerance = $answeroptions[1];
79
80 // Update Answer options group to contain only answer and grade fields.
f16d5f97 81 $answeroptions[0]->setSize(55);
c7218aef
CC
82 $answeroptions = array($answeroptions[0], $answeroptions[2]);
83 $repeated[0]->setElements($answeroptions);
84
85 // Update answer field and group label.
86 $repeated[0]->setLabel(get_string('answerformula', 'qtype_calculated', '{no}') . ' =');
87 $answeroptions[0]->setLabel(get_string('answerformula', 'qtype_calculated', '{no}') . ' =');
88
89 // Get feedback field to re append later.
90 $feedback = array_pop($repeated);
91
92 // Create tolerance group.
93 $answertolerance = array();
94 $tolerance->setLabel(get_string('tolerance', 'qtype_calculated') . '=');
95 $answertolerance[] = $tolerance;
96 $answertolerance[] = $mform->createElement('select', 'tolerancetype',
97 get_string('tolerancetype', 'qtype_calculated'), $this->qtypeobj->tolerance_types());
98 $repeated[] = $mform->createElement('group', 'answertolerance',
99 get_string('tolerance', 'qtype_calculated'), $answertolerance, null, false);
fb17f129 100 $repeatedoptions['tolerance']['default'] = 0.01;
2aef1fe5 101
c7218aef
CC
102 // Create display group.
103 $answerdisplay = array();
104 $answerdisplay[] = $mform->createElement('select', 'correctanswerlength',
105 get_string('answerdisplay', 'qtype_calculated'), range(0, 9));
2aef1fe5 106 $repeatedoptions['correctanswerlength']['default'] = 2;
107
f184c65c
TH
108 $answerlengthformats = array(
109 '1' => get_string('decimalformat', 'qtype_numerical'),
18f9b2d2 110 '2' => get_string('significantfiguresformat', 'qtype_calculated')
f184c65c 111 );
c7218aef 112 $answerdisplay[] = $mform->createElement('select', 'correctanswerformat',
f184c65c 113 get_string('correctanswershowsformat', 'qtype_calculated'), $answerlengthformats);
c7218aef
CC
114 $repeated[] = $mform->createElement('group', 'answerdisplay',
115 get_string('answerdisplay', 'qtype_calculated'), $answerdisplay, null, false);
fb17f129 116
c7218aef
CC
117 // Add feedback.
118 $repeated[] = $feedback;
fb17f129 119
2aef1fe5 120 return $repeated;
121 }
122
271ffe3f 123 /**
124 * Add question-type specific form fields.
125 *
9aa022fe 126 * @param MoodleQuickForm $mform the form being built.
271ffe3f 127 */
18f9b2d2
TH
128 protected function definition_inner($mform) {
129 $this->qtypeobj = question_bank::get_qtype($this->qtype());
cf146692 130 $label = get_string('sharedwildcards', 'qtype_calculated');
a6d46515 131 $mform->addElement('hidden', 'initialcategory', 1);
d90b016b 132 $mform->addElement('hidden', 'reload', 1);
d18e0fe6 133 $mform->setType('initialcategory', PARAM_INT);
1e988d26 134 $mform->setType('reload', PARAM_BOOL);
a6d46515 135 $html2 = $this->qtypeobj->print_dataset_definitions_category($this->question);
f184c65c
TH
136 $mform->insertElementBefore(
137 $mform->createElement('static', 'listcategory', $label, $html2), 'name');
138 if (isset($this->question->id)) {
139 $mform->insertElementBefore($mform->createElement('static', 'initialname',
140 get_string('questionstoredname', 'qtype_calculated'),
141 $this->initialname), 'name');
d90b016b 142 };
f184c65c
TH
143 $addfieldsname = 'updatecategory';
144 $addstring = get_string('updatecategory', 'qtype_calculated');
fe6ce234 145 $mform->registerNoSubmitButton($addfieldsname);
271e6dec 146
f184c65c
TH
147 $mform->insertElementBefore(
148 $mform->createElement('submit', $addfieldsname, $addstring), 'listcategory');
28a27ef1 149 $mform->registerNoSubmitButton('createoptionbutton');
aeb15530 150
3d9645ae 151 // Editing as regular question.
fe6ce234 152 $mform->setType('single', PARAM_INT);
aeb15530 153
f184c65c 154 $mform->addElement('hidden', 'shuffleanswers', '1');
fe6ce234 155 $mform->setType('shuffleanswers', PARAM_INT);
f184c65c 156 $mform->addElement('hidden', 'answernumbering', 'abc');
fe6ce234 157 $mform->setType('answernumbering', PARAM_SAFEDIR);
271ffe3f 158
f184c65c 159 $this->add_per_answer_fields($mform, get_string('answerhdr', 'qtype_calculated', '{no}'),
92111e8d 160 question_bank::fraction_options(), 1, 1);
92186abc 161
271e6dec 162 $repeated = array();
271ffe3f 163
18f9b2d2
TH
164 $this->add_unit_options($mform, $this);
165 $this->add_unit_fields($mform, $this);
b130270d 166 $this->add_interactive_settings();
fe6ce234 167
c7218aef 168 // Hidden elements.
e593233a 169 $mform->addElement('hidden', 'synchronize', '');
d18e0fe6 170 $mform->setType('synchronize', PARAM_INT);
60b5ecd3 171 $mform->addElement('hidden', 'wizard', 'datasetdefinitions');
172 $mform->setType('wizard', PARAM_ALPHA);
271ffe3f 173 }
174
8011be18
TH
175 protected function can_preview() {
176 return false; // Generally not possible for calculated questions on this page.
177 }
178
f184c65c 179 public function data_preprocessing($question) {
5cf69d7f
TH
180 $question = parent::data_preprocessing($question);
181 $question = $this->data_preprocessing_answers($question);
b130270d 182 $question = $this->data_preprocessing_hints($question);
18f9b2d2
TH
183 $question = $this->data_preprocessing_units($question);
184 $question = $this->data_preprocessing_unit_options($question);
185
5cf69d7f
TH
186 if (isset($question->options->synchronize)) {
187 $question->synchronize = $question->options->synchronize;
188 }
189
190 return $question;
191 }
192
072db71c
PS
193 protected function data_preprocessing_answers($question, $withanswerfiles = false) {
194 $question = parent::data_preprocessing_answers($question, $withanswerfiles);
5cf69d7f
TH
195 if (empty($question->options->answers)) {
196 return $question;
197 }
198
199 $key = 0;
200 foreach ($question->options->answers as $answer) {
201 // See comment in the parent method about this hack.
202 unset($this->_form->_defaultValues["tolerancetype[$key]"]);
203 unset($this->_form->_defaultValues["correctanswerlength[$key]"]);
204 unset($this->_form->_defaultValues["correctanswerformat[$key]"]);
205
206 $question->tolerancetype[$key] = $answer->tolerancetype;
207 $question->correctanswerlength[$key] = $answer->correctanswerlength;
208 $question->correctanswerformat[$key] = $answer->correctanswerformat;
209 $key++;
210 }
211
fe6ce234 212 return $question;
271ffe3f 213 }
214
f184c65c 215 public function qtype() {
271ffe3f 216 return 'calculated';
217 }
218
f184c65c 219 public function validation($data, $files) {
28a27ef1 220
c7218aef 221 // Verifying for errors in {=...} in question text.
bfdc0bce 222 $qtext = "";
fe6ce234
DC
223 $qtextremaining = $data['questiontext']['text'];
224 $possibledatasets = $this->qtypeobj->find_dataset_names($data['questiontext']['text']);
225 foreach ($possibledatasets as $name => $value) {
bfdc0bce 226 $qtextremaining = str_replace('{'.$name.'}', '1', $qtextremaining);
227 }
fe6ce234 228 while (preg_match('~\{=([^[:space:]}]*)}~', $qtextremaining, $regs1)) {
bfdc0bce 229 $qtextsplits = explode($regs1[0], $qtextremaining, 2);
f184c65c 230 $qtext = $qtext.$qtextsplits[0];
bfdc0bce 231 $qtextremaining = $qtextsplits[1];
f184c65c
TH
232 if (!empty($regs1[1]) && $formulaerrors =
233 qtype_calculated_find_formula_errors($regs1[1])) {
234 if (!isset($errors['questiontext'])) {
235 $errors['questiontext'] = $formulaerrors.':'.$regs1[1];
236 } else {
bfdc0bce 237 $errors['questiontext'] .= '<br/>'.$formulaerrors.':'.$regs1[1];
271e6dec 238 }
bfdc0bce 239 }
271e6dec 240 }
18f9b2d2
TH
241
242 $errors = parent::validation($data, $files);
243
244 // Check that the answers use datasets.
9af77e9d 245 $answers = $data['answer'];
f6232d58 246 $mandatorydatasets = array();
f184c65c 247 foreach ($answers as $key => $answer) {
a6d46515 248 $mandatorydatasets += $this->qtypeobj->find_dataset_names($answer);
271e6dec 249 }
18f9b2d2 250 if (empty($mandatorydatasets)) {
f184c65c 251 foreach ($answers as $key => $answer) {
11a66e2c 252 $errors['answeroptions['.$key.']'] =
efe3e87b 253 get_string('atleastonewildcard', 'qtype_calculated');
271e6dec 254 }
255 }
18f9b2d2
TH
256
257 // Validate the answer format.
f184c65c 258 foreach ($answers as $key => $answer) {
fe6ce234 259 $trimmedanswer = trim($answer);
18f9b2d2
TH
260 if (trim($answer)) {
261 if ($data['correctanswerformat'][$key] == 2 &&
262 $data['correctanswerlength'][$key] == '0') {
11a66e2c 263 $errors['answerdisplay['.$key.']'] =
f184c65c
TH
264 get_string('zerosignificantfiguresnotallowed', 'qtype_calculated');
265 }
fe6ce234 266 }
fe6ce234 267 }
aeb15530 268
271ffe3f 269 return $errors;
270 }
18f9b2d2 271
e0736817 272 protected function is_valid_answer($answer, $data) {
18f9b2d2
TH
273 return !qtype_calculated_find_formula_errors($answer);
274 }
275
e0736817 276 protected function valid_answer_message($answer) {
18f9b2d2
TH
277 if (!$answer) {
278 return get_string('mustenteraformulaorstar', 'qtype_numerical');
279 } else {
280 return qtype_calculated_find_formula_errors($answer);
281 }
282 }
271ffe3f 283}