MDL-27418 fix minor regression found by Rajesh.
[moodle.git] / question / type / numerical / edit_numerical_form.php
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/>.
17 /**
18  * Defines the editing form for the numerical question type.
19  *
20  * @package    qtype
21  * @subpackage numerical
22  * @copyright  2007 Jamie Pratt me@jamiep.org
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
27 defined('MOODLE_INTERNAL') || die();
29 require_once($CFG->dirroot . '/question/type/edit_question_form.php');
30 require_once($CFG->dirroot . '/question/type/numerical/questiontype.php');
33 /**
34  * numerical editing form definition.
35  *
36  * @copyright  2007 Jamie Pratt me@jamiep.org
37  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
38  */
39 class qtype_numerical_edit_form extends question_edit_form {
40     protected $ap = null;
42     protected function definition_inner($mform) {
43         $this->add_per_answer_fields($mform, get_string('answerno', 'qtype_numerical', '{no}'),
44                 question_bank::fraction_options());
46         $this->add_unit_options($mform);
47         $this->add_unit_fields($mform);
48         $this->add_interactive_settings();
49     }
51     protected function get_per_answer_fields($mform, $label, $gradeoptions,
52             &$repeatedoptions, &$answersoption) {
53         $repeated = parent::get_per_answer_fields($mform, $label, $gradeoptions,
54                 $repeatedoptions, $answersoption);
56         $tolerance = $mform->createElement('text', 'tolerance',
57                 get_string('acceptederror', 'qtype_numerical'));
58         $repeatedoptions['tolerance']['type'] = PARAM_NUMBER;
59         $repeatedoptions['tolerance']['default'] = 0;
60         array_splice($repeated, 3, 0, array($tolerance));
61         $repeated[1]->setSize(10);
63         return $repeated;
64     }
66     /**
67      * Add the unit handling options to the form.
68      * @param object $mform the form being built.
69      */
70     protected function add_unit_options($mform) {
72         $mform->addElement('header', 'unithandling',
73                 get_string('unithandling', 'qtype_numerical'));
75         $unitoptions = array(
76             qtype_numerical::UNITNONE     => get_string('onlynumerical', 'qtype_numerical'),
77             qtype_numerical::UNITOPTIONAL => get_string('manynumerical', 'qtype_numerical'),
78             qtype_numerical::UNITGRADED   => get_string('unitgraded', 'qtype_numerical'),
79         );
80         $mform->addElement('select', 'unitrole',
81                 get_string('unithandling', 'qtype_numerical'), $unitoptions);
83         $penaltygrp = array();
84         $penaltygrp[] = $mform->createElement('text', 'unitpenalty',
85                 get_string('unitpenalty', 'qtype_numerical'), array('size' => 6));
86         $mform->setType('unitpenalty', PARAM_NUMBER);
87         $mform->setDefault('unitpenalty', 0.1000000);
89         $unitgradingtypes = array(
90             qtype_numerical::UNITGRADEDOUTOFMARK =>
91                     get_string('decfractionofresponsegrade', 'qtype_numerical'),
92             qtype_numerical::UNITGRADEDOUTOFMAX =>
93                     get_string('decfractionofquestiongrade', 'qtype_numerical'),
94         );
95         $penaltygrp[] = $mform->createElement('select', 'unitgradingtypes', '', $unitgradingtypes);
96         $mform->setDefault('unitgradingtypes', 1);
98         $mform->addGroup($penaltygrp, 'penaltygrp',
99                 get_string('unitpenalty', 'qtype_numerical'), ' ', false);
100         $mform->addHelpButton('penaltygrp', 'unitpenalty', 'qtype_numerical');
102         $unitinputoptions = array(
103             qtype_numerical::UNITINPUT => get_string('editableunittext', 'qtype_numerical'),
104             qtype_numerical::UNITRADIO => get_string('unitchoice', 'qtype_numerical'),
105             qtype_numerical::UNITSELECT => get_string('unitselect', 'qtype_numerical'),
106         );
107         $mform->addElement('select', 'multichoicedisplay',
108                 get_string('studentunitanswer', 'qtype_numerical'), $unitinputoptions);
110         $unitsleftoptions = array(
111             0 => get_string('rightexample', 'qtype_numerical'),
112             1 => get_string('leftexample', 'qtype_numerical')
113         );
114         $mform->addElement('select', 'unitsleft',
115                 get_string('unitposition', 'qtype_numerical'), $unitsleftoptions);
116         $mform->setDefault('unitsleft', 0);
118         $mform->disabledIf('penaltygrp', 'unitrole', 'eq', qtype_numerical::UNITNONE);
119         $mform->disabledIf('penaltygrp', 'unitrole', 'eq', qtype_numerical::UNITOPTIONAL);
121         $mform->disabledIf('unitsleft', 'unitrole', 'eq', qtype_numerical::UNITNONE);
123         $mform->disabledIf('multichoicedisplay', 'unitrole', 'eq', qtype_numerical::UNITNONE);
124         $mform->disabledIf('multichoicedisplay', 'unitrole', 'eq', qtype_numerical::UNITOPTIONAL);
125     }
127     /**
128      * Add the input areas for each unit.
129      * @param object $mform the form being built.
130      */
131     protected function add_unit_fields($mform) {
132         $repeated = array(
133             $mform->createElement('header', 'unithdr',
134                     get_string('unithdr', 'qtype_numerical', '{no}')),
135             $mform->createElement('text', 'unit', get_string('unit', 'quiz')),
136             $mform->createElement('text', 'multiplier', get_string('multiplier', 'quiz')),
137         );
139         $repeatedoptions['unit']['type'] = PARAM_NOTAGS;
140         $repeatedoptions['multiplier']['type'] = PARAM_NUMBER;
141         $repeatedoptions['unit']['disabledif'] =
142                 array('unitrole', 'eq', qtype_numerical::UNITNONE);
143         $repeatedoptions['multiplier']['disabledif'] =
144                 array('unitrole', 'eq', qtype_numerical::UNITNONE);
146         if (isset($this->question->options->units)) {
147             $countunits = count($this->question->options->units);
148         } else {
149             $countunits = 0;
150         }
151         if ($this->question->formoptions->repeatelements) {
152             $repeatsatstart = $countunits + 1;
153         } else {
154             $repeatsatstart = $countunits;
155         }
156         $this->repeat_elements($repeated, $repeatsatstart, $repeatedoptions, 'nounits',
157                 'addunits', 2, get_string('addmoreunitblanks', 'qtype_calculated', '{no}'));
159         if ($mform->elementExists('multiplier[0]')) {
160             $firstunit = $mform->getElement('multiplier[0]');
161             $firstunit->freeze();
162             $firstunit->setValue('1.0');
163             $firstunit->setPersistantFreeze(true);
164             $mform->addHelpButton('multiplier[0]', 'numericalmultiplier', 'qtype_numerical');
165         }
166     }
168     protected function data_preprocessing($question) {
169         $question = parent::data_preprocessing($question);
170         $question = $this->data_preprocessing_answers($question);
171         $question = $this->data_preprocessing_hints($question);
172         $question = $this->data_preprocessing_units($question);
173         $question = $this->data_preprocessing_unit_options($question);
174         return $question;
175     }
177     protected function data_preprocessing_answers($question) {
178         $question = parent::data_preprocessing_answers($question);
179         if (empty($question->options->answers)) {
180             return $question;
181         }
183         $key = 0;
184         foreach ($question->options->answers as $answer) {
185             // See comment in the parent method about this hack.
186             unset($this->_form->_defaultValues["tolerance[$key]"]);
188             $question->tolerance[$key] = $answer->tolerance;
189             $key++;
190         }
192         return $question;
193     }
195     /**
196      * Perform the necessary preprocessing for the fields added by
197      * {@link add_unit_fields()}.
198      * @param object $question the data being passed to the form.
199      * @return object $question the modified data.
200      */
201     protected function data_preprocessing_units($question) {
202         if (empty($question->options->units)) {
203             return $question;
204         }
206         foreach ($question->options->units as $key => $unit) {
207             $question->unit[$key] = $unit->unit;
208             $question->multiplier[$key] = $unit->multiplier;
209         }
211         return $question;
212     }
214     /**
215      * Perform the necessary preprocessing for the fields added by
216      * {@link add_unit_options()}.
217      * @param object $question the data being passed to the form.
218      * @return object $question the modified data.
219      */
220     protected function data_preprocessing_unit_options($question) {
221         if (empty($question->options)) {
222             return $question;
223         }
225         $question->unitpenalty = $question->options->unitpenalty;
226         $question->unitsleft = $question->options->unitsleft;
228         if ($question->options->unitgradingtype) {
229             $question->unitgradingtypes = $question->options->unitgradingtype;
230             $question->multichoicedisplay = $question->options->showunits;
231             $question->unitrole = qtype_numerical::UNITGRADED;
232         } else {
233             $question->unitrole = $question->options->showunits;
234         }
236         return $question;
237     }
239     public function validation($data, $files) {
240         $errors = parent::validation($data, $files);
241         $errors = $this->validate_answers($data, $errors);
242         $errors = $this->validate_numerical_options($data, $errors);
243         return $errors;
244     }
246     /**
247      * Validate the answers.
248      * @param array $data the submitted data.
249      * @param array $errors the errors array to add to.
250      * @return array the updated errors array.
251      */
252     protected function validate_answers($data, $errors) {
253         // Check the answers.
254         $answercount = 0;
255         $maxgrade = false;
256         $answers = $data['answer'];
257         foreach ($answers as $key => $answer) {
258             $trimmedanswer = trim($answer);
259             if ($trimmedanswer != '') {
260                 $answercount++;
261                 if (!$this->is_valid_answer($trimmedanswer, $data)) {
262                     $errors['answer[' . $key . ']'] = $this->valid_answer_message($trimmedanswer);
263                 }
264                 if ($data['fraction'][$key] == 1) {
265                     $maxgrade = true;
266                 }
267                 if (!is_numeric($data['tolerance'][$key])) {
268                     $errors['tolerance['.$key.']'] =
269                             get_string('mustbenumeric', 'qtype_calculated');
270                 }
271             } else if ($data['fraction'][$key] != 0 ||
272                     !html_is_blank($data['feedback'][$key]['text'])) {
273                 $errors['answer[' . $key . ']'] = $this->valid_answer_message($trimmedanswer);
274                 $answercount++;
275             }
276         }
277         if ($answercount == 0) {
278             $errors['answer[0]'] = get_string('notenoughanswers', 'qtype_numerical');
279         }
280         if ($maxgrade == false) {
281             $errors['fraction[0]'] = get_string('fractionsnomax', 'question');
282         }
284         return $errors;
285     }
287     /**
288      * Validate a particular answer.
289      * @param string $answer an answer to validate. Known to be non-blank and already trimmed.
290      * @param array $data the submitted data.
291      * @return bool whether this is a valid answer.
292      */
293     protected function is_valid_answer($answer, $data) {
294         return $answer == '*' || $this->is_valid_number($answer);
295     }
297     /**
298      * Validate that a string is a nubmer formatted correctly for the current locale.
299      * @param string $x a string
300      * @return bool whether $x is a number that the numerical question type can interpret.
301      */
302     protected function is_valid_number($x) {
303         if (is_null($this->ap)) {
304             $this->ap = new qtype_numerical_answer_processor(array());
305         }
307         list($value, $unit) = $this->ap->apply_units($x);
309         return !is_null($value) && !$unit;
310     }
312     /**
313      * @return string erre describing what an answer should be.
314      */
315     protected function valid_answer_message($answer) {
316         return get_string('answermustbenumberorstar', 'qtype_numerical');
317     }
319     /**
320      * Validate the answers.
321      * @param array $data the submitted data.
322      * @param array $errors the errors array to add to.
323      * @return array the updated errors array.
324      */
325     protected function validate_numerical_options($data, $errors) {
326         if ($data['unitrole'] != qtype_numerical::UNITNONE && trim($data['unit'][0]) == '') {
327             $errors['unit[0]'] = get_string('unitonerequired', 'qtype_numerical');
328         }
330         if (empty($data['unit'])) {
331             return $errors;
332         }
334         // Basic unit validation.
335         foreach ($data['unit'] as $key => $unit) {
336             if (is_numeric($unit)) {
337                 $errors['unit[' . $key . ']'] =
338                         get_string('mustnotbenumeric', 'qtype_calculated');
339             }
341             $trimmedunit = trim($unit);
342             if (empty($trimmedunit)) {
343                 continue;
344             }
346             $trimmedmultiplier = trim($data['multiplier'][$key]);
347             if (empty($trimmedmultiplier)) {
348                 $errors['multiplier[' . $key . ']'] =
349                         get_string('youmustenteramultiplierhere', 'qtype_calculated');
350             } else if (!is_numeric($trimmedmultiplier)) {
351                 $errors['multiplier[' . $key . ']'] =
352                         get_string('mustbenumeric', 'qtype_calculated');
353             }
354         }
356         // Check for repeated units.
357         $alreadyseenunits = array();
358         foreach ($data['unit'] as $key => $unit) {
359             $trimmedunit = trim($unit);
360             if ($trimmedunit == '') {
361                 continue;
362             }
364             if (in_array($trimmedunit, $alreadyseenunits)) {
365                 $errors['unit[' . $key . ']'] =
366                         get_string('errorrepeatedunit', 'qtype_numerical');
367             } else {
368                 $alreadyseenunits[] = $trimmedunit;
369             }
370         }
372         return $errors;
373     }
375     public function qtype() {
376         return 'numerical';
377     }