Merge branch 'w25_MDL-26455_m26_installport' of git://github.com/skodak/moodle
[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     /** @var int we always show at least this many sets of unit fields. */
41     const UNITS_MIN_REPEATS = 1;
42     const UNITS_TO_ADD = 2;
44     protected $ap = null;
46     protected function definition_inner($mform) {
47         $this->add_per_answer_fields($mform, get_string('answerno', 'qtype_numerical', '{no}'),
48                 question_bank::fraction_options());
50         $this->add_unit_options($mform);
51         $this->add_unit_fields($mform);
52         $this->add_interactive_settings();
53     }
55     protected function get_per_answer_fields($mform, $label, $gradeoptions,
56             &$repeatedoptions, &$answersoption) {
57         $repeated = parent::get_per_answer_fields($mform, $label, $gradeoptions,
58                 $repeatedoptions, $answersoption);
60         $tolerance = $mform->createElement('text', 'tolerance',
61                 get_string('answererror', 'qtype_numerical'), array('size' => 15));
62         $repeatedoptions['tolerance']['type'] = PARAM_FLOAT;
63         $repeatedoptions['tolerance']['default'] = 0;
64         $elements = $repeated[0]->getElements();
65         $elements[0]->setSize(15);
66         array_splice($elements, 1, 0, array($tolerance));
67         $repeated[0]->setElements($elements);
69         return $repeated;
70     }
72     protected function get_more_choices_string() {
73         return get_string('addmoreanswerblanks', 'qtype_numerical');
74     }
76     /**
77      * Add the unit handling options to the form.
78      * @param object $mform the form being built.
79      */
80     protected function add_unit_options($mform) {
82         $mform->addElement('header', 'unithandling',
83                 get_string('unithandling', 'qtype_numerical'));
85         $unitoptions = array(
86             qtype_numerical::UNITNONE     => get_string('onlynumerical', 'qtype_numerical'),
87             qtype_numerical::UNITOPTIONAL => get_string('manynumerical', 'qtype_numerical'),
88             qtype_numerical::UNITGRADED   => get_string('unitgraded', 'qtype_numerical'),
89         );
90         $mform->addElement('select', 'unitrole',
91                 get_string('unithandling', 'qtype_numerical'), $unitoptions);
93         $penaltygrp = array();
94         $penaltygrp[] = $mform->createElement('text', 'unitpenalty',
95                 get_string('unitpenalty', 'qtype_numerical'), array('size' => 6));
96         $mform->setType('unitpenalty', PARAM_FLOAT);
97         $mform->setDefault('unitpenalty', 0.1000000);
99         $unitgradingtypes = array(
100             qtype_numerical::UNITGRADEDOUTOFMARK =>
101                     get_string('decfractionofresponsegrade', 'qtype_numerical'),
102             qtype_numerical::UNITGRADEDOUTOFMAX =>
103                     get_string('decfractionofquestiongrade', 'qtype_numerical'),
104         );
105         $penaltygrp[] = $mform->createElement('select', 'unitgradingtypes', '', $unitgradingtypes);
106         $mform->setDefault('unitgradingtypes', 1);
108         $mform->addGroup($penaltygrp, 'penaltygrp',
109                 get_string('unitpenalty', 'qtype_numerical'), ' ', false);
110         $mform->addHelpButton('penaltygrp', 'unitpenalty', 'qtype_numerical');
112         $unitinputoptions = array(
113             qtype_numerical::UNITINPUT => get_string('editableunittext', 'qtype_numerical'),
114             qtype_numerical::UNITRADIO => get_string('unitchoice', 'qtype_numerical'),
115             qtype_numerical::UNITSELECT => get_string('unitselect', 'qtype_numerical'),
116         );
117         $mform->addElement('select', 'multichoicedisplay',
118                 get_string('studentunitanswer', 'qtype_numerical'), $unitinputoptions);
120         $unitsleftoptions = array(
121             0 => get_string('rightexample', 'qtype_numerical'),
122             1 => get_string('leftexample', 'qtype_numerical')
123         );
124         $mform->addElement('select', 'unitsleft',
125                 get_string('unitposition', 'qtype_numerical'), $unitsleftoptions);
126         $mform->setDefault('unitsleft', 0);
128         $mform->disabledIf('penaltygrp', 'unitrole', 'eq', qtype_numerical::UNITNONE);
129         $mform->disabledIf('penaltygrp', 'unitrole', 'eq', qtype_numerical::UNITOPTIONAL);
131         $mform->disabledIf('unitsleft', 'unitrole', 'eq', qtype_numerical::UNITNONE);
133         $mform->disabledIf('multichoicedisplay', 'unitrole', 'eq', qtype_numerical::UNITNONE);
134         $mform->disabledIf('multichoicedisplay', 'unitrole', 'eq', qtype_numerical::UNITOPTIONAL);
135     }
137     /**
138      * Add the input areas for each unit.
139      * @param object $mform the form being built.
140      */
141     protected function add_unit_fields($mform) {
142         $mform->addElement('header', 'unithdr',
143                     get_string('units', 'qtype_numerical'), '');
145         $unitfields = array($mform->createElement('group', 'units',
146                  get_string('unitx', 'qtype_numerical'), $this->unit_group($mform), null, false));
148         $repeatedoptions['unit']['disabledif'] = array('unitrole', 'eq', qtype_numerical::UNITNONE);
149         $repeatedoptions['unit']['type'] = PARAM_NOTAGS;
150         $repeatedoptions['multiplier']['disabledif'] = array('unitrole', 'eq', qtype_numerical::UNITNONE);
151         $repeatedoptions['multiplier']['type'] = PARAM_NUMBER;
153         $mform->disabledIf('addunits', 'unitrole', 'eq', qtype_numerical::UNITNONE);
155         if (isset($this->question->options->units)) {
156             $repeatsatstart = max(count($this->question->options->units), self::UNITS_MIN_REPEATS);
157         } else {
158             $repeatsatstart = self::UNITS_MIN_REPEATS;
159         }
161         $this->repeat_elements($unitfields, $repeatsatstart, $repeatedoptions, 'nounits',
162                 'addunits', self::UNITS_TO_ADD, get_string('addmoreunitblanks', 'qtype_numerical', '{no}'), true);
164         // The following strange-looking if statement is to do with when the
165         // form is used to move questions between categories. See MDL-15159.
166         if ($mform->elementExists('units[0]')) {
167             $firstunit = $mform->getElement('units[0]');
168             $elements = $firstunit->getElements();
169             foreach ($elements as $element) {
170                 if ($element->getName() != 'multiplier[0]') {
171                     continue;
172                 }
173                 $element->freeze();
174                 $element->setValue('1.0');
175                 $element->setPersistantFreeze(true);
176             }
177             $mform->addHelpButton('units[0]', 'numericalmultiplier', 'qtype_numerical');
178         }
179     }
181     /**
182      * Get the form fields needed to edit one unit.
183      * @param MoodleQuickForm $mform the form being built.
184      * @return array of form fields.
185      */
186     protected function unit_group($mform) {
187         $grouparray = array();
188         $grouparray[] = $mform->createElement('text', 'unit', get_string('unit', 'quiz'), array('size'=>10));
189         $grouparray[] = $mform->createElement('static', '', '', ' ' .
190                 get_string('multiplier', 'quiz').' ');
191         $grouparray[] = $mform->createElement('text', 'multiplier',
192                 get_string('multiplier', 'quiz'), array('size'=>10));
194         return $grouparray;
195     }
197     protected function data_preprocessing($question) {
198         $question = parent::data_preprocessing($question);
199         $question = $this->data_preprocessing_answers($question);
200         $question = $this->data_preprocessing_hints($question);
201         $question = $this->data_preprocessing_units($question);
202         $question = $this->data_preprocessing_unit_options($question);
203         return $question;
204     }
206     protected function data_preprocessing_answers($question, $withanswerfiles = false) {
207         $question = parent::data_preprocessing_answers($question, $withanswerfiles);
208         if (empty($question->options->answers)) {
209             return $question;
210         }
212         $key = 0;
213         foreach ($question->options->answers as $answer) {
214             // See comment in the parent method about this hack.
215             unset($this->_form->_defaultValues["tolerance[$key]"]);
217             $question->tolerance[$key] = $answer->tolerance;
218             $key++;
219         }
221         return $question;
222     }
224     /**
225      * Perform the necessary preprocessing for the fields added by
226      * {@link add_unit_fields()}.
227      * @param object $question the data being passed to the form.
228      * @return object $question the modified data.
229      */
230     protected function data_preprocessing_units($question) {
231         if (empty($question->options->units)) {
232             return $question;
233         }
235         foreach ($question->options->units as $key => $unit) {
236             $question->unit[$key] = $unit->unit;
237             $question->multiplier[$key] = $unit->multiplier;
238         }
240         return $question;
241     }
243     /**
244      * Perform the necessary preprocessing for the fields added by
245      * {@link add_unit_options()}.
246      * @param object $question the data being passed to the form.
247      * @return object $question the modified data.
248      */
249     protected function data_preprocessing_unit_options($question) {
250         if (empty($question->options)) {
251             return $question;
252         }
254         $question->unitpenalty = $question->options->unitpenalty;
255         $question->unitsleft = $question->options->unitsleft;
257         if ($question->options->unitgradingtype) {
258             $question->unitgradingtypes = $question->options->unitgradingtype;
259             $question->multichoicedisplay = $question->options->showunits;
260             $question->unitrole = qtype_numerical::UNITGRADED;
261         } else {
262             $question->unitrole = $question->options->showunits;
263         }
265         return $question;
266     }
268     public function validation($data, $files) {
269         $errors = parent::validation($data, $files);
270         $errors = $this->validate_answers($data, $errors);
271         $errors = $this->validate_numerical_options($data, $errors);
272         return $errors;
273     }
275     /**
276      * Validate the answers.
277      * @param array $data the submitted data.
278      * @param array $errors the errors array to add to.
279      * @return array the updated errors array.
280      */
281     protected function validate_answers($data, $errors) {
282         // Check the answers.
283         $answercount = 0;
284         $maxgrade = false;
285         $answers = $data['answer'];
286         foreach ($answers as $key => $answer) {
287             $trimmedanswer = trim($answer);
288             if ($trimmedanswer != '') {
289                 $answercount++;
290                 if (!$this->is_valid_answer($trimmedanswer, $data)) {
291                     $errors['answeroptions[' . $key . ']'] = $this->valid_answer_message($trimmedanswer);
292                 }
293                 if ($data['fraction'][$key] == 1) {
294                     $maxgrade = true;
295                 }
296                 if ($answer !== '*' && !is_numeric($data['tolerance'][$key])) {
297                     $errors['answeroptions['.$key.']'] =
298                             get_string('xmustbenumeric', 'qtype_numerical',
299                                 get_string('acceptederror', 'qtype_numerical'));
300                 }
301             } else if ($data['fraction'][$key] != 0 ||
302                     !html_is_blank($data['feedback'][$key]['text'])) {
303                 $errors['answeroptions[' . $key . ']'] = $this->valid_answer_message($trimmedanswer);
304                 $answercount++;
305             }
306         }
307         if ($answercount == 0) {
308             $errors['answeroptions[0]'] = get_string('notenoughanswers', 'qtype_numerical');
309         }
310         if ($maxgrade == false) {
311             $errors['answeroptions[0]'] = get_string('fractionsnomax', 'question');
312         }
314         return $errors;
315     }
317     /**
318      * Validate a particular answer.
319      * @param string $answer an answer to validate. Known to be non-blank and already trimmed.
320      * @param array $data the submitted data.
321      * @return bool whether this is a valid answer.
322      */
323     protected function is_valid_answer($answer, $data) {
324         return $answer == '*' || $this->is_valid_number($answer);
325     }
327     /**
328      * Validate that a string is a nubmer formatted correctly for the current locale.
329      * @param string $x a string
330      * @return bool whether $x is a number that the numerical question type can interpret.
331      */
332     protected function is_valid_number($x) {
333         if (is_null($this->ap)) {
334             $this->ap = new qtype_numerical_answer_processor(array());
335         }
337         list($value, $unit) = $this->ap->apply_units($x);
339         return !is_null($value) && !$unit;
340     }
342     /**
343      * @return string erre describing what an answer should be.
344      */
345     protected function valid_answer_message($answer) {
346         return get_string('answermustbenumberorstar', 'qtype_numerical');
347     }
349     /**
350      * Validate the answers.
351      * @param array $data the submitted data.
352      * @param array $errors the errors array to add to.
353      * @return array the updated errors array.
354      */
355     protected function validate_numerical_options($data, $errors) {
356         if ($data['unitrole'] != qtype_numerical::UNITNONE && trim($data['unit'][0]) == '') {
357             $errors['units[0]'] = get_string('unitonerequired', 'qtype_numerical');
358         }
360         if (empty($data['unit']) || $data['unitrole'] == qtype_numerical::UNITNONE) {
361             return $errors;
362         }
364         // Basic unit validation.
365         foreach ($data['unit'] as $key => $unit) {
366             if (is_numeric($unit)) {
367                 $errors['units[' . $key . ']'] =
368                         get_string('xmustnotbenumeric', 'qtype_numerical',
369                             get_string('unit', 'qtype_numerical'));
370             }
372             $trimmedunit = trim($unit);
373             if (empty($trimmedunit)) {
374                 continue;
375             }
377             $trimmedmultiplier = trim($data['multiplier'][$key]);
378             if (empty($trimmedmultiplier)) {
379                 $errors['units[' . $key . ']'] =
380                         get_string('youmustenteramultiplierhere', 'qtype_numerical');
381             } else if (!is_numeric($trimmedmultiplier)) {
382                 $errors['units[' . $key . ']'] =
383                         get_string('xmustbenumeric', 'qtype_numerical',
384                             get_string('multiplier', 'qtype_numerical'));
385             }
386         }
388         // Check for repeated units.
389         $alreadyseenunits = array();
390         foreach ($data['unit'] as $key => $unit) {
391             $trimmedunit = trim($unit);
392             if ($trimmedunit == '') {
393                 continue;
394             }
396             if (in_array($trimmedunit, $alreadyseenunits)) {
397                 $errors['units[' . $key . ']'] =
398                         get_string('errorrepeatedunit', 'qtype_numerical');
399             } else {
400                 $alreadyseenunits[] = $trimmedunit;
401             }
402         }
404         return $errors;
405     }
407     public function qtype() {
408         return 'numerical';
409     }