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