MDL-46148 qtype_calculated: validate formulas everywhere.
[moodle.git] / question / type / calculatedmulti / questiontype.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  * Question type class for the calculated multiple-choice question type.
19  *
20  * @package    qtype
21  * @subpackage calculatedmulti
22  * @copyright  2009 Pierre Pichet
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/multichoice/questiontype.php');
30 require_once($CFG->dirroot . '/question/type/calculated/questiontype.php');
33 /**
34  * The calculated multiple-choice question type.
35  *
36  * @copyright  2009 Pierre Pichet
37  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
38  */
39 class qtype_calculatedmulti extends qtype_calculated {
41     public function save_question_options($question) {
42         global $CFG, $DB;
43         $context = $question->context;
45         // Calculated options.
46         $update = true;
47         $options = $DB->get_record('question_calculated_options',
48                 array('question' => $question->id));
49         if (!$options) {
50             $options = new stdClass();
51             $options->question = $question->id;
52             $options->correctfeedback = '';
53             $options->partiallycorrectfeedback = '';
54             $options->incorrectfeedback = '';
55             $options->id = $DB->insert_record('question_calculated_options', $options);
56         }
57         $options->synchronize = $question->synchronize;
58         $options->single = $question->single;
59         $options->answernumbering = $question->answernumbering;
60         $options->shuffleanswers = $question->shuffleanswers;
61         $options = $this->save_combined_feedback_helper($options, $question, $context, true);
62         $DB->update_record('question_calculated_options', $options);
64         // Get old versions of the objects.
65         if (!$oldanswers = $DB->get_records('question_answers',
66                 array('question' => $question->id), 'id ASC')) {
67             $oldanswers = array();
68         }
69         if (!$oldoptions = $DB->get_records('question_calculated',
70                 array('question' => $question->id), 'answer ASC')) {
71             $oldoptions = array();
72         }
74         // Insert all the new answers.
75         if (isset($question->answer) && !isset($question->answers)) {
76             $question->answers = $question->answer;
77         }
78         foreach ($question->answers as $key => $answerdata) {
79             if (is_array($answerdata)) {
80                 $answerdata = $answerdata['text'];
81             }
82             if (trim($answerdata) == '') {
83                 continue;
84             }
86             // Update an existing answer if possible.
87             $answer = array_shift($oldanswers);
88             if (!$answer) {
89                 $answer = new stdClass();
90                 $answer->question = $question->id;
91                 $answer->answer   = '';
92                 $answer->feedback = '';
93                 $answer->id       = $DB->insert_record('question_answers', $answer);
94             }
96             if (is_array($answerdata)) {
97                 // Doing an import.
98                 $answer->answer = $this->import_or_save_files($answerdata,
99                         $context, 'question', 'answer', $answer->id);
100                 $answer->answerformat = $answerdata['format'];
101             } else {
102                 // Saving the form.
103                 $answer->answer = $answerdata;
104                 $answer->answerformat = FORMAT_HTML;
105             }
106             $answer->fraction = $question->fraction[$key];
107             $answer->feedback = $this->import_or_save_files($question->feedback[$key],
108                     $context, 'question', 'answerfeedback', $answer->id);
109             $answer->feedbackformat = $question->feedback[$key]['format'];
111             $DB->update_record("question_answers", $answer);
113             // Set up the options object.
114             if (!$options = array_shift($oldoptions)) {
115                 $options = new stdClass();
116             }
117             $options->question            = $question->id;
118             $options->answer              = $answer->id;
119             $options->tolerance           = trim($question->tolerance[$key]);
120             $options->tolerancetype       = trim($question->tolerancetype[$key]);
121             $options->correctanswerlength = trim($question->correctanswerlength[$key]);
122             $options->correctanswerformat = trim($question->correctanswerformat[$key]);
124             // Save options.
125             if (isset($options->id)) {
126                 // Reusing existing record.
127                 $DB->update_record('question_calculated', $options);
128             } else {
129                 // New options.
130                 $DB->insert_record('question_calculated', $options);
131             }
132         }
134         // Delete old answer records.
135         if (!empty($oldanswers)) {
136             foreach ($oldanswers as $oa) {
137                 $DB->delete_records('question_answers', array('id' => $oa->id));
138             }
139         }
140         if (!empty($oldoptions)) {
141             foreach ($oldoptions as $oo) {
142                 $DB->delete_records('question_calculated', array('id' => $oo->id));
143             }
144         }
146         $this->save_hints($question, true);
148         if (isset($question->import_process) && $question->import_process) {
149             $this->import_datasets($question);
150         }
151         // Report any problems.
152         if (!empty($result->notice)) {
153             return $result;
154         }
156         return true;
157     }
159     protected function validate_answer($answer) {
160         $error = qtype_calculated_find_formula_errors_in_text($answer);
161         if ($error) {
162             throw new coding_exception($error);
163         }
164     }
166     protected function make_question_instance($questiondata) {
167         question_bank::load_question_definition_classes($this->name());
168         if ($questiondata->options->single) {
169             $class = 'qtype_calculatedmulti_single_question';
170         } else {
171             $class = 'qtype_calculatedmulti_multi_question';
172         }
173         return new $class();
174     }
176     protected function initialise_question_instance(question_definition $question, $questiondata) {
177         question_type::initialise_question_instance($question, $questiondata);
179         $question->shuffleanswers = $questiondata->options->shuffleanswers;
180         $question->answernumbering = $questiondata->options->answernumbering;
181         if (!empty($questiondata->options->layout)) {
182             $question->layout = $questiondata->options->layout;
183         } else {
184             $question->layout = qtype_multichoice_single_question::LAYOUT_VERTICAL;
185         }
187         $question->synchronised = $questiondata->options->synchronize;
189         $this->initialise_combined_feedback($question, $questiondata, true);
190         $this->initialise_question_answers($question, $questiondata);
192         foreach ($questiondata->options->answers as $a) {
193             $question->answers[$a->id]->correctanswerlength = $a->correctanswerlength;
194             $question->answers[$a->id]->correctanswerformat = $a->correctanswerformat;
195         }
197         $question->datasetloader = new qtype_calculated_dataset_loader($questiondata->id);
198     }
200     public function comment_header($question) {
201         $strheader = '';
202         $delimiter = '';
204         $answers = $question->options->answers;
206         foreach ($answers as $key => $answer) {
207             $ans = shorten_text($answer->answer, 17, true);
208             $strheader .= $delimiter.$ans;
209             $delimiter = '<br/><br/>';
210         }
211         return $strheader;
212     }
214     public function comment_on_datasetitems($qtypeobj, $questionid, $questiontext,
215             $answers, $data, $number) {
216         global $DB;
217         $comment = new stdClass();
218         $comment->stranswers = array();
219         $comment->outsidelimit = false;
220         $comment->answers = array();
222         $answers = fullclone($answers);
223         $errors = '';
224         $delimiter = ': ';
225         foreach ($answers as $key => $answer) {
226             $anssubstituted = $this->substitute_variables($answer->answer, $data);
227             // Evaluate the equations i.e {=5+4).
228             $anstext = '';
229             $anstextremaining = $anssubstituted;
230             while (preg_match('~\{=([^[:space:]}]*)}~', $anstextremaining, $regs1)) {
231                 $anstextsplits = explode($regs1[0], $anstextremaining, 2);
232                 $anstext =$anstext.$anstextsplits[0];
233                 $anstextremaining = $anstextsplits[1];
234                 if (empty($regs1[1])) {
235                     $str = '';
236                 } else {
237                     if ($formulaerrors = qtype_calculated_find_formula_errors($regs1[1])) {
238                         $str=$formulaerrors;
239                     } else {
240                         eval('$str = '.$regs1[1].';');
241                     }
242                 }
243                 $anstext = $anstext.$str;
244             }
245             $anstext .= $anstextremaining;
246             $comment->stranswers[$key] = $anssubstituted.'<br/>'.$anstext;
247         }
248         return fullclone($comment);
249     }
251     public function get_virtual_qtype() {
252         return question_bank::get_qtype('multichoice');
253     }
255     public function get_possible_responses($questiondata) {
256         if ($questiondata->options->single) {
257             $responses = array();
259             foreach ($questiondata->options->answers as $aid => $answer) {
260                 $responses[$aid] = new question_possible_response($answer->answer,
261                         $answer->fraction);
262             }
264             $responses[null] = question_possible_response::no_response();
265             return array($questiondata->id => $responses);
266         } else {
267             $parts = array();
269             foreach ($questiondata->options->answers as $aid => $answer) {
270                 $parts[$aid] = array($aid =>
271                         new question_possible_response($answer->answer, $answer->fraction));
272             }
274             return $parts;
275         }
276     }
278     public function move_files($questionid, $oldcontextid, $newcontextid) {
279         $fs = get_file_storage();
281         parent::move_files($questionid, $oldcontextid, $newcontextid);
282         $this->move_files_in_answers($questionid, $oldcontextid, $newcontextid, true);
283         $this->move_files_in_hints($questionid, $oldcontextid, $newcontextid);
285         $fs->move_area_files_to_new_context($oldcontextid,
286                 $newcontextid, 'qtype_calculatedmulti', 'correctfeedback', $questionid);
287         $fs->move_area_files_to_new_context($oldcontextid,
288                 $newcontextid, 'qtype_calculatedmulti', 'partiallycorrectfeedback', $questionid);
289         $fs->move_area_files_to_new_context($oldcontextid,
290                 $newcontextid, 'qtype_calculatedmulti', 'incorrectfeedback', $questionid);
291     }
293     protected function delete_files($questionid, $contextid) {
294         $fs = get_file_storage();
296         parent::delete_files($questionid, $contextid);
297         $this->delete_files_in_answers($questionid, $contextid, true);
298         $this->delete_files_in_hints($questionid, $contextid);
300         $fs->delete_area_files($contextid, 'qtype_calculatedmulti',
301                 'correctfeedback', $questionid);
302         $fs->delete_area_files($contextid, 'qtype_calculatedmulti',
303                 'partiallycorrectfeedback', $questionid);
304         $fs->delete_area_files($contextid, 'qtype_calculatedmulti',
305                 'incorrectfeedback', $questionid);
306     }