MDL-46148 qtype_calculated: validate formulas everywhere.
[moodle.git] / question / type / calculated / questiontype.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
d3603157
TH
17/**
18 * Question type class for the calculated question type.
19 *
20 * @package qtype
21 * @subpackage calculated
22 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24 */
fe6ce234 25
516cf3eb 26
a17b297d
TH
27defined('MOODLE_INTERNAL') || die();
28
4267fc52 29require_once($CFG->dirroot . '/question/type/questionbase.php');
18f9b2d2
TH
30require_once($CFG->dirroot . '/question/type/numerical/question.php');
31
a17b297d 32
d3603157
TH
33/**
34 * The calculated question type.
35 *
36 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
37 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
38 */
f184c65c 39class qtype_calculated extends question_type {
155bc754
TH
40 /** Regular expression that finds the formulas in content. */
41 const FORMULAS_IN_TEXT_REGEX = '~\{=([^{}]*(?:\{[^{}]+}[^{}]*)*)\}~';
42
f184c65c 43 const MAX_DATASET_ITEMS = 100;
516cf3eb 44
f184c65c 45 public $wizardpagesnumber = 3;
516cf3eb 46
f184c65c 47 public function get_question_options($question) {
3d9645ae 48 // First get the datasets and default options.
49 // The code is used for calculated, calculatedsimple and calculatedmulti qtypes.
f184c65c
TH
50 global $CFG, $DB, $OUTPUT;
51 if (!$question->options = $DB->get_record('question_calculated_options',
52 array('question' => $question->id))) {
0cba6a8d 53 $question->options = new stdClass();
ab8b5142 54 $question->options->synchronize = 0;
f184c65c 55 $question->options->single = 0;
ab8b5142 56 $question->options->answernumbering = 'abc';
f184c65c 57 $question->options->shuffleanswers = 0;
ab8b5142
PP
58 $question->options->correctfeedback = '';
59 $question->options->partiallycorrectfeedback = '';
60 $question->options->incorrectfeedback = '';
f9ce6989
PP
61 $question->options->correctfeedbackformat = 0;
62 $question->options->partiallycorrectfeedbackformat = 0;
63 $question->options->incorrectfeedbackformat = 0;
28a27ef1 64 }
65
f184c65c
TH
66 if (!$question->options->answers = $DB->get_records_sql("
67 SELECT a.*, c.tolerance, c.tolerancetype, c.correctanswerlength, c.correctanswerformat
68 FROM {question_answers} a,
69 {question_calculated} c
70 WHERE a.question = ?
71 AND a.id = c.answer
72 ORDER BY a.id ASC", array($question->id))) {
fe6ce234 73 return false;
f184c65c 74 }
516cf3eb 75
18f9b2d2
TH
76 if ($this->get_virtual_qtype()->name() == 'numerical') {
77 $this->get_virtual_qtype()->get_numerical_units($question);
78 $this->get_virtual_qtype()->get_numerical_options($question);
a6d46515 79 }
80
b130270d
TH
81 $question->hints = $DB->get_records('question_hints',
82 array('questionid' => $question->id), 'id ASC');
83
f184c65c 84 if (isset($question->export_process)&&$question->export_process) {
c9e4ba36 85 $question->options->datasets = $this->get_datasets_for_export($question);
f34488b2 86 }
516cf3eb 87 return true;
88 }
f34488b2 89
f184c65c 90 public function get_datasets_for_export($question) {
fe6ce234 91 global $DB, $CFG;
3f76dd52 92 $datasetdefs = array();
c9e4ba36 93 if (!empty($question->id)) {
3f76dd52 94 $sql = "SELECT i.*
fe6ce234
DC
95 FROM {question_datasets} d, {question_dataset_definitions} i
96 WHERE d.question = ? AND d.datasetdefinition = i.id";
f34488b2 97 if ($records = $DB->get_records_sql($sql, array($question->id))) {
3f76dd52 98 foreach ($records as $r) {
f184c65c
TH
99 $def = $r;
100 if ($def->category == '0') {
101 $def->status = 'private';
f34488b2 102 } else {
f184c65c 103 $def->status = 'shared';
f34488b2 104 }
f184c65c
TH
105 $def->type = 'calculated';
106 list($distribution, $min, $max, $dec) = explode(':', $def->options, 4);
107 $def->distribution = $distribution;
108 $def->minimum = $min;
109 $def->maximum = $max;
110 $def->decimals = $dec;
111 if ($def->itemcount > 0) {
3d9645ae 112 // Get the datasetitems.
3f76dd52 113 $def->items = array();
f184c65c 114 if ($items = $this->get_database_dataset_items($def->id)) {
3f76dd52 115 $n = 0;
f184c65c 116 foreach ($items as $ii) {
3f76dd52 117 $n++;
0ff4bd08 118 $def->items[$n] = new stdClass();
f184c65c
TH
119 $def->items[$n]->itemnumber = $ii->itemnumber;
120 $def->items[$n]->value = $ii->value;
fe6ce234 121 }
f184c65c 122 $def->number_of_items = $n;
3f76dd52 123 }
124 }
f34488b2 125 $datasetdefs["1-$r->category-$r->name"] = $def;
3f76dd52 126 }
127 }
128 }
f184c65c 129 return $datasetdefs;
f34488b2 130 }
131
f184c65c
TH
132 public function save_question_options($question) {
133 global $CFG, $DB;
3d9645ae 134 // The code is used for calculated, calculatedsimple and calculatedmulti qtypes.
fe6ce234 135 $context = $question->context;
f7fa6874 136 if (isset($question->answer) && !isset($question->answers)) {
2aef1fe5 137 $question->answers = $question->answer;
138 }
3d9645ae 139 // Calculated options.
f184c65c
TH
140 $update = true;
141 $options = $DB->get_record('question_calculated_options',
142 array('question' => $question->id));
1ee53ca9 143 if (!$options) {
144 $update = false;
0ff4bd08 145 $options = new stdClass();
1ee53ca9 146 $options->question = $question->id;
147 }
3d9645ae 148 // As used only by calculated.
f184c65c 149 if (isset($question->synchronize)) {
fe6ce234 150 $options->synchronize = $question->synchronize;
f184c65c
TH
151 } else {
152 $options->synchronize = 0;
8b59e62e 153 }
f184c65c 154 $options->single = 0;
ab8b5142 155 $options->answernumbering = $question->answernumbering;
28a27ef1 156 $options->shuffleanswers = $question->shuffleanswers;
fe6ce234 157
f184c65c
TH
158 foreach (array('correctfeedback', 'partiallycorrectfeedback',
159 'incorrectfeedback') as $feedbackname) {
f9ce6989 160 $options->$feedbackname = '';
fe6ce234 161 $feedbackformat = $feedbackname . 'format';
f184c65c 162 $options->$feedbackformat = 0;
fe6ce234
DC
163 }
164
1ee53ca9 165 if ($update) {
f184c65c 166 $DB->update_record('question_calculated_options', $options);
1ee53ca9 167 } else {
f184c65c 168 $DB->insert_record('question_calculated_options', $options);
1ee53ca9 169 }
2aef1fe5 170
3d9645ae 171 // Get old versions of the objects.
f184c65c
TH
172 $oldanswers = $DB->get_records('question_answers',
173 array('question' => $question->id), 'id ASC');
516cf3eb 174
f184c65c
TH
175 $oldoptions = $DB->get_records('question_calculated',
176 array('question' => $question->id), 'answer ASC');
2aef1fe5 177
178 // Save the units.
ab8b5142 179 $virtualqtype = $this->get_virtual_qtype();
18f9b2d2
TH
180
181 $result = $virtualqtype->save_units($question);
a6d46515 182 if (isset($result->error)) {
183 return $result;
184 } else {
18f9b2d2 185 $units = $result->units;
a6d46515 186 }
18f9b2d2 187
3d9645ae 188 // Insert all the new answers.
0a6555f8 189 if (isset($question->answer) && !isset($question->answers)) {
f184c65c 190 $question->answers = $question->answer;
0a6555f8 191 }
e6d76583
TH
192 foreach ($question->answers as $key => $answerdata) {
193 if (is_array($answerdata)) {
194 $answerdata = $answerdata['text'];
cde2709a 195 }
e6d76583
TH
196 if (trim($answerdata) == '') {
197 continue;
198 }
199
200 // Update an existing answer if possible.
201 $answer = array_shift($oldanswers);
202 if (!$answer) {
0ff4bd08 203 $answer = new stdClass();
a6d46515 204 $answer->question = $question->id;
e6d76583
TH
205 $answer->answer = '';
206 $answer->feedback = '';
207 $answer->id = $DB->insert_record('question_answers', $answer);
208 }
f34488b2 209
e6d76583
TH
210 $answer->answer = trim($answerdata);
211 $answer->fraction = $question->fraction[$key];
212 $answer->feedback = $this->import_or_save_files($question->feedback[$key],
213 $context, 'question', 'answerfeedback', $answer->id);
214 $answer->feedbackformat = $question->feedback[$key]['format'];
a6d46515 215
e6d76583
TH
216 $DB->update_record("question_answers", $answer);
217
3d9645ae 218 // Set up the options object.
e6d76583
TH
219 if (!$options = array_shift($oldoptions)) {
220 $options = new stdClass();
221 }
222 $options->question = $question->id;
223 $options->answer = $answer->id;
224 $options->tolerance = trim($question->tolerance[$key]);
225 $options->tolerancetype = trim($question->tolerancetype[$key]);
226 $options->correctanswerlength = trim($question->correctanswerlength[$key]);
227 $options->correctanswerformat = trim($question->correctanswerformat[$key]);
228
3d9645ae 229 // Save options.
e6d76583 230 if (isset($options->id)) {
3d9645ae 231 // Reusing existing record.
e6d76583
TH
232 $DB->update_record('question_calculated', $options);
233 } else {
3d9645ae 234 // New options.
e6d76583 235 $DB->insert_record('question_calculated', $options);
516cf3eb 236 }
237 }
e6d76583 238
3d9645ae 239 // Delete old answer records.
a6d46515 240 if (!empty($oldanswers)) {
f184c65c 241 foreach ($oldanswers as $oa) {
f34488b2 242 $DB->delete_records('question_answers', array('id' => $oa->id));
516cf3eb 243 }
a6d46515 244 }
245
3d9645ae 246 // Delete old answer records.
a6d46515 247 if (!empty($oldoptions)) {
f184c65c 248 foreach ($oldoptions as $oo) {
f34488b2 249 $DB->delete_records('question_calculated', array('id' => $oo->id));
516cf3eb 250 }
251 }
fe6ce234 252
18f9b2d2 253 $result = $virtualqtype->save_unit_options($question);
824f0182 254 if (isset($result->error)) {
255 return $result;
256 }
516cf3eb 257
b130270d
TH
258 $this->save_hints($question);
259
f184c65c 260 if (isset($question->import_process)&&$question->import_process) {
3f76dd52 261 $this->import_datasets($question);
fe6ce234 262 }
a6d46515 263 // Report any problems.
264 if (!empty($result->notice)) {
265 return $result;
266 }
516cf3eb 267 return true;
268 }
269
f184c65c 270 public function import_datasets($question) {
f34488b2 271 global $DB;
3f76dd52 272 $n = count($question->dataset);
273 foreach ($question->dataset as $dataset) {
3d9645ae 274 // Name, type, option.
3f76dd52 275 $datasetdef = new stdClass();
276 $datasetdef->name = $dataset->name;
f184c65c
TH
277 $datasetdef->type = 1;
278 $datasetdef->options = $dataset->distribution . ':' . $dataset->min . ':' .
279 $dataset->max . ':' . $dataset->length;
280 $datasetdef->itemcount = $dataset->itemcount;
281 if ($dataset->status == 'private') {
3f76dd52 282 $datasetdef->category = 0;
f184c65c
TH
283 $todo = 'create';
284 } else if ($dataset->status == 'shared') {
f34488b2 285 if ($sharedatasetdefs = $DB->get_records_select(
fe6ce234
DC
286 'question_dataset_definitions',
287 "type = '1'
288 AND name = ?
289 AND category = ?
290 ORDER BY id DESC ", array($dataset->name, $question->category)
3d9645ae 291 )) { // So there is at least one.
3f76dd52 292 $sharedatasetdef = array_shift($sharedatasetdefs);
3d9645ae 293 if ($sharedatasetdef->options == $datasetdef->options) {// Identical so use it.
f184c65c
TH
294 $todo = 'useit';
295 $datasetdef = $sharedatasetdef;
3d9645ae 296 } else { // Different so create a private one.
3f76dd52 297 $datasetdef->category = 0;
f184c65c 298 $todo = 'create';
f34488b2 299 }
3d9645ae 300 } else { // No so create one.
f184c65c
TH
301 $datasetdef->category = $question->category;
302 $todo = 'create';
fe6ce234 303 }
f34488b2 304 }
f184c65c
TH
305 if ($todo == 'create') {
306 $datasetdef->id = $DB->insert_record('question_dataset_definitions', $datasetdef);
fe6ce234 307 }
3d9645ae 308 // Create relation to the dataset.
0ff4bd08 309 $questiondataset = new stdClass();
fe6ce234
DC
310 $questiondataset->question = $question->id;
311 $questiondataset->datasetdefinition = $datasetdef->id;
bb4b6010 312 $DB->insert_record('question_datasets', $questiondataset);
f184c65c 313 if ($todo == 'create') {
3d9645ae 314 // Add the items.
f184c65c 315 foreach ($dataset->datasetitem as $dataitem) {
0ff4bd08 316 $datasetitem = new stdClass();
f184c65c
TH
317 $datasetitem->definition = $datasetdef->id;
318 $datasetitem->itemnumber = $dataitem->itemnumber;
319 $datasetitem->value = $dataitem->value;
bb4b6010 320 $DB->insert_record('question_dataset_items', $datasetitem);
f34488b2 321 }
322 }
3f76dd52 323 }
324 }
f34488b2 325
18f9b2d2
TH
326 protected function initialise_question_instance(question_definition $question, $questiondata) {
327 parent::initialise_question_instance($question, $questiondata);
328
329 question_bank::get_qtype('numerical')->initialise_numerical_answers(
330 $question, $questiondata);
331 foreach ($questiondata->options->answers as $a) {
332 $question->answers[$a->id]->tolerancetype = $a->tolerancetype;
cdece95e
TH
333 $question->answers[$a->id]->correctanswerlength = $a->correctanswerlength;
334 $question->answers[$a->id]->correctanswerformat = $a->correctanswerformat;
f184c65c 335 }
fbe2cfea 336
e35ba43c
TH
337 $question->synchronised = $questiondata->options->synchronize;
338
18f9b2d2
TH
339 $question->unitdisplay = $questiondata->options->showunits;
340 $question->unitgradingtype = $questiondata->options->unitgradingtype;
341 $question->unitpenalty = $questiondata->options->unitpenalty;
342 $question->ap = question_bank::get_qtype(
343 'numerical')->make_answer_processor(
344 $questiondata->options->units, $questiondata->options->unitsleft);
345
346 $question->datasetloader = new qtype_calculated_dataset_loader($questiondata->id);
fbe2cfea 347 }
348
f184c65c 349 public function validate_form($form) {
516cf3eb 350 switch($form->wizardpage) {
f184c65c
TH
351 case 'question':
352 $calculatedmessages = array();
353 if (empty($form->name)) {
354 $calculatedmessages[] = get_string('missingname', 'qtype_calculated');
355 }
356 if (empty($form->questiontext)) {
357 $calculatedmessages[] = get_string('missingquestiontext', 'qtype_calculated');
516cf3eb 358 }
3d9645ae 359 // Verify formulas.
f184c65c
TH
360 foreach ($form->answers as $key => $answer) {
361 if ('' === trim($answer)) {
362 $calculatedmessages[] = get_string(
363 'missingformula', 'qtype_calculated');
364 }
365 if ($formulaerrors = qtype_calculated_find_formula_errors($answer)) {
1d9ed698 366 $calculatedmessages[] = $formulaerrors;
516cf3eb 367 }
f184c65c
TH
368 if (! isset($form->tolerance[$key])) {
369 $form->tolerance[$key] = 0.0;
370 }
371 if (! is_numeric($form->tolerance[$key])) {
55748620
TH
372 $calculatedmessages[] = get_string('xmustbenumeric', 'qtype_numerical',
373 get_string('tolerance', 'qtype_calculated'));
f184c65c 374 }
fe6ce234 375 }
516cf3eb 376
f184c65c
TH
377 if (!empty($calculatedmessages)) {
378 $errorstring = "The following errors were found:<br />";
379 foreach ($calculatedmessages as $msg) {
380 $errorstring .= $msg . '<br />';
381 }
382 print_error($errorstring);
1d9ed698 383 }
384
f184c65c
TH
385 break;
386 default:
387 return parent::validate_form($form);
388 break;
516cf3eb 389 }
390 return true;
391 }
72553162 392 public function finished_edit_wizard($form) {
5e8a85aa 393 return isset($form->savechanges);
fbe2cfea 394 }
f184c65c 395 public function wizardpagesnumber() {
5e8a85aa 396 return 3;
bd806d55 397 }
3d9645ae 398 // This gets called by editquestion.php after the standard question is saved.
72553162
TH
399 public function print_next_wizard_page($question, $form, $course) {
400 global $CFG, $SESSION, $COURSE;
fbe2cfea 401
3d9645ae 402 // Catch invalid navigation & reloads.
fbe2cfea 403 if (empty($question->id) && empty($SESSION->calculated)) {
404 redirect('edit.php?courseid='.$COURSE->id, 'The page you are loading has expired.', 3);
405 }
406
3d9645ae 407 // See where we're coming from.
fbe2cfea 408 switch($form->wizardpage) {
f184c65c
TH
409 case 'question':
410 require("$CFG->dirroot/question/type/calculated/datasetdefinitions.php");
411 break;
412 case 'datasetdefinitions':
413 case 'datasetitems':
414 require("$CFG->dirroot/question/type/calculated/datasetitems.php");
415 break;
416 default:
417 print_error('invalidwizardpage', 'question');
418 break;
fbe2cfea 419 }
420 }
421
3d9645ae 422 // This gets called by question2.php after the standard question is saved.
f184c65c 423 public function &next_wizard_form($submiturl, $question, $wizardnow) {
fbe2cfea 424 global $CFG, $SESSION, $COURSE;
425
3d9645ae 426 // Catch invalid navigation & reloads.
fbe2cfea 427 if (empty($question->id) && empty($SESSION->calculated)) {
f184c65c
TH
428 redirect('edit.php?courseid=' . $COURSE->id,
429 'The page you are loading has expired. Cannot get next wizard form.', 3);
fbe2cfea 430 }
f184c65c
TH
431 if (empty($question->id)) {
432 $question = $SESSION->calculated->questionform;
fbe2cfea 433 }
434
3d9645ae 435 // See where we're coming from.
fbe2cfea 436 switch($wizardnow) {
f184c65c
TH
437 case 'datasetdefinitions':
438 require("$CFG->dirroot/question/type/calculated/datasetdefinitions_form.php");
439 $mform = new question_dataset_dependent_definitions_form(
440 "$submiturl?wizardnow=datasetdefinitions", $question);
441 break;
442 case 'datasetitems':
443 require("$CFG->dirroot/question/type/calculated/datasetitems_form.php");
7d087744 444 $regenerate = optional_param('forceregeneration', false, PARAM_BOOL);
f184c65c
TH
445 $mform = new question_dataset_dependent_items_form(
446 "$submiturl?wizardnow=datasetitems", $question, $regenerate);
447 break;
448 default:
449 print_error('invalidwizardpage', 'question');
450 break;
fbe2cfea 451 }
452
453 return $mform;
454 }
516cf3eb 455
fbe2cfea 456 /**
457 * This method should be overriden if you want to include a special heading or some other
458 * html on a question editing page besides the question editing form.
459 *
460 * @param question_edit_form $mform a child of question_edit_form
461 * @param object $question
462 * @param string $wizardnow is '' for first page.
463 */
72553162 464 public function display_question_editing_page($mform, $question, $wizardnow) {
f184c65c
TH
465 global $OUTPUT;
466 switch ($wizardnow) {
467 case '':
72553162 468 // On the first page, the default display is fine.
f184c65c
TH
469 parent::display_question_editing_page($mform, $question, $wizardnow);
470 return;
72553162 471
f184c65c
TH
472 case 'datasetdefinitions':
473 echo $OUTPUT->heading_with_help(
474 get_string('choosedatasetproperties', 'qtype_calculated'),
475 'questiondatasets', 'qtype_calculated');
476 break;
72553162 477
f184c65c
TH
478 case 'datasetitems':
479 echo $OUTPUT->heading_with_help(get_string('editdatasets', 'qtype_calculated'),
480 'questiondatasets', 'qtype_calculated');
481 break;
fbe2cfea 482 }
483
fbe2cfea 484 $mform->display();
fbe2cfea 485 }
486
88ec9f30
TH
487 /**
488 * Verify that the equations in part of the question are OK.
489 * We throw an exception here because this should have already been validated
490 * by the form. This is just a last line of defence to prevent a question
491 * being stored in the database if it has bad formulas. This saves us from,
492 * for example, malicious imports.
493 * @param string $text containing equations.
494 */
495 protected function validate_text($text) {
496 $error = qtype_calculated_find_formula_errors_in_text($text);
497 if ($error) {
498 throw new coding_exception($error);
499 }
500 }
501
502 /**
503 * Verify that an answer is OK.
504 * We throw an exception here because this should have already been validated
505 * by the form. This is just a last line of defence to prevent a question
506 * being stored in the database if it has bad formulas. This saves us from,
507 * for example, malicious imports.
508 * @param string $text containing equations.
509 */
510 protected function validate_answer($answer) {
511 $error = qtype_calculated_find_formula_errors($answer);
512 if ($error) {
513 throw new coding_exception($error);
514 }
515 }
516
fe6ce234 517 /**
fbe2cfea 518 * This method prepare the $datasets in a format similar to dadatesetdefinitions_form.php
519 * so that they can be saved
520 * using the function save_dataset_definitions($form)
3d9645ae 521 * when creating a new calculated question or
522 * when editing an already existing calculated question
fbe2cfea 523 * or by function save_as_new_dataset_definitions($form, $initialid)
3d9645ae 524 * when saving as new an already existing calculated question.
fbe2cfea 525 *
526 * @param object $form
527 * @param int $questionfromid default = '0'
528 */
f184c65c 529 public function preparedatasets($form , $questionfromid = '0') {
88ec9f30 530
3d9645ae 531 // The dataset names present in the edit_question_form and edit_calculated_form
532 // are retrieved.
fbe2cfea 533 $possibledatasets = $this->find_dataset_names($form->questiontext);
534 $mandatorydatasets = array();
88ec9f30 535 foreach ($form->answers as $key => $answer) {
c28357f4 536 $mandatorydatasets += $this->find_dataset_names($answer);
fe6ce234 537 }
3d9645ae 538 // If there are identical datasetdefs already saved in the original question
539 // either when editing a question or saving as new,
540 // they are retrieved using $questionfromid.
f184c65c
TH
541 if ($questionfromid != '0') {
542 $form->id = $questionfromid;
fbe2cfea 543 }
544 $datasets = array();
f184c65c 545 $key = 0;
3d9645ae 546 // Always prepare the mandatorydatasets present in the answers.
547 // The $options are not used here.
fbe2cfea 548 foreach ($mandatorydatasets as $datasetname) {
549 if (!isset($datasets[$datasetname])) {
550 list($options, $selected) =
fe6ce234 551 $this->dataset_options($form, $datasetname);
f184c65c
TH
552 $datasets[$datasetname] = '';
553 $form->dataset[$key] = $selected;
fbe2cfea 554 $key++;
555 }
556 }
3d9645ae 557 // Do not prepare possibledatasets when creating a question.
558 // They will defined and stored with datasetdefinitions_form.php.
559 // The $options are not used here.
f184c65c 560 if ($questionfromid != '0') {
fbe2cfea 561
fe6ce234
DC
562 foreach ($possibledatasets as $datasetname) {
563 if (!isset($datasets[$datasetname])) {
564 list($options, $selected) =
f184c65c
TH
565 $this->dataset_options($form, $datasetname, false);
566 $datasets[$datasetname] = '';
567 $form->dataset[$key] = $selected;
fe6ce234
DC
568 $key++;
569 }
fbe2cfea 570 }
571 }
f184c65c 572 return $datasets;
fe6ce234 573 }
f184c65c 574 public function addnamecategory(&$question) {
d90b016b 575 global $DB;
fe6ce234
DC
576 $categorydatasetdefs = $DB->get_records_sql(
577 "SELECT a.*
578 FROM {question_datasets} b, {question_dataset_definitions} a
f184c65c
TH
579 WHERE a.id = b.datasetdefinition
580 AND a.type = '1'
581 AND a.category != 0
582 AND b.question = ?
fe6ce234 583 ORDER BY a.name ", array($question->id));
f184c65c 584 $questionname = $question->name;
d90b016b 585 $regs= array();
f184c65c 586 if (preg_match('~#\{([^[:space:]]*)#~', $questionname , $regs)) {
fe6ce234 587 $questionname = str_replace($regs[0], '', $questionname);
8b59e62e 588 };
d90b016b 589
f184c65c 590 if (!empty($categorydatasetdefs)) {
3d9645ae 591 // There is at least one with the same name.
f184c65c
TH
592 $questionname = '#' . $questionname;
593 foreach ($categorydatasetdefs as $def) {
594 if (strlen($def->name) + strlen($questionname) < 250) {
595 $questionname = '{' . $def->name . '}' . $questionname;
d90b016b
PP
596 }
597 }
f184c65c 598 $questionname = '#' . $questionname;
d90b016b 599 }
f184c65c 600 $DB->set_field('question', 'name', $questionname, array('id' => $question->id));
d90b016b 601 }
fbe2cfea 602
603 /**
fe6ce234
DC
604 * this version save the available data at the different steps of the question editing process
605 * without using global $SESSION as storage between steps
606 * at the first step $wizardnow = 'question'
607 * when creating a new question
608 * when modifying a question
609 * when copying as a new question
610 * the general parameters and answers are saved using parent::save_question
611 * then the datasets are prepared and saved
612 * at the second step $wizardnow = 'datasetdefinitions'
613 * the datadefs final type are defined as private, category or not a datadef
614 * at the third step $wizardnow = 'datasetitems'
615 * the datadefs parameters and the data items are created or defined
616 *
617 * @param object question
618 * @param object $form
619 * @param int $course
620 * @param PARAM_ALPHA $wizardnow should be added as we are coming from question2.php
621 */
f184c65c 622 public function save_question($question, $form) {
1ee53ca9 623 global $DB;
88ec9f30
TH
624
625 if (isset($form->correctfeedback)) {
626 $this->validate_text($form->correctfeedback['text']);
627 $this->validate_text($form->partiallycorrectfeedback['text']);
628 $this->validate_text($form->incorrectfeedback['text']);
629 }
630
e605209a 631 if ($this->wizardpagesnumber() == 1 || $question->qtype == 'calculatedsimple') {
88ec9f30 632 $question = parent::save_question($question, $form);
f184c65c 633 return $question;
bd806d55
PP
634 }
635
fbe2cfea 636 $wizardnow = optional_param('wizardnow', '', PARAM_ALPHA);
3d9645ae 637 $id = optional_param('id', 0, PARAM_INT); // Question id.
638 // In case 'question':
639 // For a new question $form->id is empty
640 // when saving as new question.
641 // The $question->id = 0, $form is $data from question2.php
642 // and $data->makecopy is defined as $data->id is the initial question id.
643 // Edit case. If it is a new question we don't necessarily need to
644 // return a valid question object.
645
646 // See where we're coming from.
fbe2cfea 647 switch($wizardnow) {
f184c65c 648 case '' :
3d9645ae 649 case 'question': // Coming from the first page, creating the second.
650 if (empty($form->id)) { // or a new question $form->id is empty.
88ec9f30
TH
651 // Make it impossible to save bad formulas anywhere.
652 $this->validate_text($form->questiontext['text']);
653 $this->validate_text($form->generalfeedback['text']);
654 foreach ($form->answer as $key => $answer) {
655 $this->validate_answer($answer);
656 $this->validate_text($form->feedback[$key]['text']);
657 }
658
f184c65c 659 $question = parent::save_question($question, $form);
3d9645ae 660 // Prepare the datasets using default $questionfromid.
f184c65c
TH
661 $this->preparedatasets($form);
662 $form->id = $question->id;
663 $this->save_dataset_definitions($form);
664 if (isset($form->synchronize) && $form->synchronize == 2) {
665 $this->addnamecategory($question);
666 }
667 } else if (!empty($form->makecopy)) {
668 $questionfromid = $form->id;
669 $question = parent::save_question($question, $form);
3d9645ae 670 // Prepare the datasets.
f184c65c
TH
671 $this->preparedatasets($form, $questionfromid);
672 $form->id = $question->id;
673 $this->save_as_new_dataset_definitions($form, $questionfromid);
674 if (isset($form->synchronize) && $form->synchronize == 2) {
675 $this->addnamecategory($question);
676 }
677 } else {
3d9645ae 678 // Editing a question.
f184c65c 679 $question = parent::save_question($question, $form);
3d9645ae 680 // Prepare the datasets.
f184c65c
TH
681 $this->preparedatasets($form, $question->id);
682 $form->id = $question->id;
683 $this->save_dataset_definitions($form);
684 if (isset($form->synchronize) && $form->synchronize == 2) {
685 $this->addnamecategory($question);
686 }
fbe2cfea 687 }
f184c65c
TH
688 break;
689 case 'datasetdefinitions':
3d9645ae 690 // Calculated options.
691 // It cannot go here without having done the first page,
692 // so the question_calculated_options should exist.
693 // We only need to update the synchronize field.
f184c65c
TH
694 if (isset($form->synchronize)) {
695 $optionssynchronize = $form->synchronize;
696 } else {
697 $optionssynchronize = 0;
28a27ef1 698 }
f184c65c
TH
699 $DB->set_field('question_calculated_options', 'synchronize', $optionssynchronize,
700 array('question' => $question->id));
701 if (isset($form->synchronize) && $form->synchronize == 2) {
fe6ce234 702 $this->addnamecategory($question);
28a27ef1 703 }
f184c65c
TH
704
705 $this->save_dataset_definitions($form);
706 break;
707 case 'datasetitems':
708 $this->save_dataset_items($question, $form);
709 $this->save_question_calculated($question, $form);
710 break;
711 default:
712 print_error('invalidwizardpage', 'question');
713 break;
fbe2cfea 714 }
715 return $question;
716 }
9203b705 717
f184c65c 718 public function delete_question($questionid, $contextid) {
f34488b2 719 global $DB;
ab8b5142 720
f184c65c
TH
721 $DB->delete_records('question_calculated', array('question' => $questionid));
722 $DB->delete_records('question_calculated_options', array('question' => $questionid));
723 $DB->delete_records('question_numerical_units', array('question' => $questionid));
f34488b2 724 if ($datasets = $DB->get_records('question_datasets', array('question' => $questionid))) {
90c3f310 725 foreach ($datasets as $dataset) {
f184c65c
TH
726 if (!$DB->get_records_select('question_datasets',
727 "question != ? AND datasetdefinition = ? ",
728 array($questionid, $dataset->datasetdefinition))) {
729 $DB->delete_records('question_dataset_definitions',
730 array('id' => $dataset->datasetdefinition));
731 $DB->delete_records('question_dataset_items',
732 array('definition' => $dataset->datasetdefinition));
733 }
90c3f310 734 }
735 }
f184c65c 736 $DB->delete_records('question_datasets', array('question' => $questionid));
9203b705
TH
737
738 parent::delete_question($questionid, $contextid);
516cf3eb 739 }
9203b705 740
24400682
TH
741 public function get_random_guess_score($questiondata) {
742 foreach ($questiondata->options->answers as $aid => $answer) {
743 if ('*' == trim($answer->answer)) {
744 return max($answer->fraction - $questiondata->options->unitpenalty, 0);
745 }
746 }
747 return 0;
748 }
749
f184c65c 750 public function supports_dataset_item_generation() {
3d9645ae 751 // Calculated support generation of randomly distributed number data.
516cf3eb 752 return true;
753 }
18f9b2d2
TH
754
755 public function custom_generator_tools_part($mform, $idx, $j) {
60b5ecd3 756
757 $minmaxgrp = array();
f184c65c
TH
758 $minmaxgrp[] = $mform->createElement('text', "calcmin[$idx]",
759 get_string('calcmin', 'qtype_calculated'));
760 $minmaxgrp[] = $mform->createElement('text', "calcmax[$idx]",
761 get_string('calcmax', 'qtype_calculated'));
762 $mform->addGroup($minmaxgrp, 'minmaxgrp',
763 get_string('minmax', 'qtype_calculated'), ' - ', false);
61cca0b7
RT
764 $mform->setType("calcmin[$idx]", PARAM_FLOAT);
765 $mform->setType("calcmax[$idx]", PARAM_FLOAT);
60b5ecd3 766
767 $precisionoptions = range(0, 10);
f184c65c
TH
768 $mform->addElement('select', "calclength[$idx]",
769 get_string('calclength', 'qtype_calculated'), $precisionoptions);
60b5ecd3 770
f184c65c
TH
771 $distriboptions = array('uniform' => get_string('uniform', 'qtype_calculated'),
772 'loguniform' => get_string('loguniform', 'qtype_calculated'));
773 $mform->addElement('select', "calcdistribution[$idx]",
774 get_string('calcdistribution', 'qtype_calculated'), $distriboptions);
60b5ecd3 775 }
776
f184c65c 777 public function custom_generator_set_data($datasetdefs, $formdata) {
60b5ecd3 778 $idx = 1;
f184c65c
TH
779 foreach ($datasetdefs as $datasetdef) {
780 if (preg_match('~^(uniform|loguniform):([^:]*):([^:]*):([0-9]*)$~',
781 $datasetdef->options, $regs)) {
60b5ecd3 782 $defid = "$datasetdef->type-$datasetdef->category-$datasetdef->name";
783 $formdata["calcdistribution[$idx]"] = $regs[1];
784 $formdata["calcmin[$idx]"] = $regs[2];
785 $formdata["calcmax[$idx]"] = $regs[3];
786 $formdata["calclength[$idx]"] = $regs[4];
787 }
788 $idx++;
789 }
790 return $formdata;
791 }
516cf3eb 792
f184c65c 793 public function custom_generator_tools($datasetdef) {
44c64ad8 794 global $OUTPUT;
6dbcacee 795 if (preg_match('~^(uniform|loguniform):([^:]*):([^:]*):([0-9]*)$~',
f184c65c
TH
796 $datasetdef->options, $regs)) {
797 $defid = "$datasetdef->type-$datasetdef->category-$datasetdef->name";
798 for ($i = 0; $i<10; ++$i) {
799 $lengthoptions[$i] = get_string(($regs[1] == 'uniform'
800 ? 'decimals'
801 : 'significantfigures'), 'qtype_calculated', $i);
802 }
3d9645ae 803 $menu1 = html_writer::label(get_string('lengthoption', 'qtype_calculated'),
804 'menucalclength', false, array('class' => 'accesshide'));
83690170 805 $menu1 .= html_writer::select($lengthoptions, 'calclength[]', $regs[4], null);
f184c65c
TH
806
807 $options = array('uniform' => get_string('uniformbit', 'qtype_calculated'),
3d9645ae 808 'loguniform' => get_string('loguniformbit', 'qtype_calculated'));
809 $menu2 = html_writer::label(get_string('distributionoption', 'qtype_calculated'),
810 'menucalcdistribution', false, array('class' => 'accesshide'));
83690170 811 $menu2 .= html_writer::select($options, 'calcdistribution[]', $regs[1], null);
f184c65c
TH
812 return '<input type="submit" onclick="'
813 . "getElementById('addform').regenerateddefid.value='$defid'; return true;"
814 .'" value="'. get_string('generatevalue', 'qtype_calculated') . '"/><br/>'
815 . '<input type="text" size="3" name="calcmin[]" '
816 . " value=\"$regs[2]\"/> &amp; <input name=\"calcmax[]\" "
817 . ' type="text" size="3" value="' . $regs[3] .'"/> '
818 . $menu1 . '<br/>'
819 . $menu2;
820 } else {
821 return '';
822 }
516cf3eb 823 }
824
60b5ecd3 825
f184c65c 826 public function update_dataset_options($datasetdefs, $form) {
042a8403 827 global $OUTPUT;
3d9645ae 828 // Do we have information about new options ?
516cf3eb 829 if (empty($form->definition) || empty($form->calcmin)
3d9645ae 830 ||empty($form->calcmax) || empty($form->calclength)
f184c65c 831 || empty($form->calcdistribution)) {
3d9645ae 832 // I guess not.
516cf3eb 833
f184c65c 834 } else {
3d9645ae 835 // Looks like we just could have some new information here.
f184c65c
TH
836 $uniquedefs = array_values(array_unique($form->definition));
837 foreach ($uniquedefs as $key => $defid) {
838 if (isset($datasetdefs[$defid])
60b5ecd3 839 && is_numeric($form->calcmin[$key+1])
840 && is_numeric($form->calcmax[$key+1])
841 && is_numeric($form->calclength[$key+1])) {
f184c65c
TH
842 switch ($form->calcdistribution[$key+1]) {
843 case 'uniform': case 'loguniform':
844 $datasetdefs[$defid]->options =
845 $form->calcdistribution[$key+1] . ':'
846 . $form->calcmin[$key+1] . ':'
847 . $form->calcmax[$key+1] . ':'
848 . $form->calclength[$key+1];
849 break;
850 default:
851 echo $OUTPUT->notification(
852 "Unexpected distribution ".$form->calcdistribution[$key+1]);
853 }
516cf3eb 854 }
855 }
f184c65c 856 }
516cf3eb 857
3d9645ae 858 // Look for empty options, on which we set default values.
516cf3eb 859 foreach ($datasetdefs as $defid => $def) {
860 if (empty($def->options)) {
861 $datasetdefs[$defid]->options = 'uniform:1.0:10.0:1';
862 }
863 }
864 return $datasetdefs;
865 }
866
f184c65c 867 public function save_question_calculated($question, $fromform) {
450f1127 868 global $DB;
869
870 foreach ($question->options->answers as $key => $answer) {
871 if ($options = $DB->get_record('question_calculated', array('answer' => $key))) {
872 $options->tolerance = trim($fromform->tolerance[$key]);
873 $options->tolerancetype = trim($fromform->tolerancetype[$key]);
874 $options->correctanswerlength = trim($fromform->correctanswerlength[$key]);
875 $options->correctanswerformat = trim($fromform->correctanswerformat[$key]);
0bcf8b6f 876 $DB->update_record('question_calculated', $options);
450f1127 877 }
878 }
879 }
880
fe6ce234
DC
881 /**
882 * This function get the dataset items using id as unique parameter and return an
883 * array with itemnumber as index sorted ascendant
884 * If the multiple records with the same itemnumber exist, only the newest one
885 * i.e with the greatest id is used, the others are ignored but not deleted.
886 * MDL-19210
887 */
f184c65c 888 public function get_database_dataset_items($definition) {
fe6ce234 889 global $CFG, $DB;
f184c65c 890 $databasedataitems = $DB->get_records_sql(// Use number as key!!
fe6ce234
DC
891 " SELECT id , itemnumber, definition, value
892 FROM {question_dataset_items}
893 WHERE definition = $definition order by id DESC ", array($definition));
894 $dataitems = Array();
f184c65c
TH
895 foreach ($databasedataitems as $id => $dataitem) {
896 if (!isset($dataitems[$dataitem->itemnumber])) {
897 $dataitems[$dataitem->itemnumber] = $dataitem;
fe6ce234
DC
898 }
899 }
900 ksort($dataitems);
f184c65c 901 return $dataitems;
a2155a7b 902 }
aeb15530 903
f184c65c 904 public function save_dataset_items($question, $fromform) {
f34488b2 905 global $CFG, $DB;
f184c65c
TH
906 $synchronize = false;
907 if (isset($fromform->nextpageparam['forceregeneration'])) {
908 $regenerate = $fromform->nextpageparam['forceregeneration'];
909 } else {
910 $regenerate = 0;
d650e1a3 911 }
60b5ecd3 912 if (empty($question->options)) {
913 $this->get_question_options($question);
914 }
f184c65c
TH
915 if (!empty($question->options->synchronize)) {
916 $synchronize = true;
d90b016b 917 }
8b59e62e 918
3d9645ae 919 // Get the old datasets for this question.
60b5ecd3 920 $datasetdefs = $this->get_dataset_definitions($question->id, array());
921 // Handle generator options...
922 $olddatasetdefs = fullclone($datasetdefs);
923 $datasetdefs = $this->update_dataset_options($datasetdefs, $fromform);
924 $maxnumber = -1;
925 foreach ($datasetdefs as $defid => $datasetdef) {
926 if (isset($datasetdef->id)
f184c65c 927 && $datasetdef->options != $olddatasetdefs[$defid]->options) {
3d9645ae 928 // Save the new value for options.
f184c65c 929 $DB->update_record('question_dataset_definitions', $datasetdef);
60b5ecd3 930
f184c65c 931 }
3d9645ae 932 // Get maxnumber.
60b5ecd3 933 if ($maxnumber == -1 || $datasetdef->itemcount < $maxnumber) {
934 $maxnumber = $datasetdef->itemcount;
935 }
936 }
3d9645ae 937 // Handle adding and removing of dataset items.
60b5ecd3 938 $i = 1;
f184c65c
TH
939 if ($maxnumber > self::MAX_DATASET_ITEMS) {
940 $maxnumber = self::MAX_DATASET_ITEMS;
d90b016b 941 }
8b59e62e 942
a8d2a373 943 ksort($fromform->definition);
60b5ecd3 944 foreach ($fromform->definition as $key => $defid) {
3d9645ae 945 // If the delete button has not been pressed then skip the datasetitems
946 // in the 'add item' part of the form.
f184c65c 947 if ($i > count($datasetdefs)*$maxnumber) {
60b5ecd3 948 break;
949 }
950 $addeditem = new stdClass();
951 $addeditem->definition = $datasetdefs[$defid]->id;
952 $addeditem->value = $fromform->number[$i];
953 $addeditem->itemnumber = ceil($i / count($datasetdefs));
954
955 if ($fromform->itemid[$i]) {
3d9645ae 956 // Reuse any previously used record.
60b5ecd3 957 $addeditem->id = $fromform->itemid[$i];
bb4b6010 958 $DB->update_record('question_dataset_items', $addeditem);
60b5ecd3 959 } else {
bb4b6010 960 $DB->insert_record('question_dataset_items', $addeditem);
60b5ecd3 961 }
962
963 $i++;
964 }
8b59e62e 965 if (isset($addeditem->itemnumber) && $maxnumber < $addeditem->itemnumber
f184c65c
TH
966 && $addeditem->itemnumber < self::MAX_DATASET_ITEMS) {
967 $maxnumber = $addeditem->itemnumber;
968 foreach ($datasetdefs as $key => $newdef) {
969 if (isset($newdef->id) && $newdef->itemcount <= $maxnumber) {
970 $newdef->itemcount = $maxnumber;
3d9645ae 971 // Save the new value for options.
f184c65c 972 $DB->update_record('question_dataset_definitions', $newdef);
60b5ecd3 973 }
974 }
f184c65c 975 }
3d9645ae 976 // Adding supplementary items.
f184c65c
TH
977 $numbertoadd = 0;
978 if (isset($fromform->addbutton) && $fromform->selectadd > 0 &&
979 $maxnumber < self::MAX_DATASET_ITEMS) {
980 $numbertoadd = $fromform->selectadd;
e35ba43c
TH
981 if (self::MAX_DATASET_ITEMS - $maxnumber < $numbertoadd) {
982 $numbertoadd = self::MAX_DATASET_ITEMS - $maxnumber;
c31f631b 983 }
3d9645ae 984 // Add the other items.
985 // Generate a new dataset item (or reuse an old one).
451373ed 986 foreach ($datasetdefs as $defid => $datasetdef) {
3d9645ae 987 // In case that for category datasets some new items has been added,
988 // get actual values.
989 // Fix regenerate for this datadefs.
f184c65c
TH
990 $defregenerate = 0;
991 if ($synchronize &&
992 !empty ($fromform->nextpageparam["datasetregenerate[$datasetdef->name"])) {
993 $defregenerate = 1;
994 } else if (!$synchronize &&
995 (($regenerate == 1 && $datasetdef->category == 0) ||$regenerate == 2)) {
996 $defregenerate = 1;
fe6ce234 997 }
451373ed 998 if (isset($datasetdef->id)) {
f184c65c
TH
999 $datasetdefs[$defid]->items =
1000 $this->get_database_dataset_items($datasetdef->id);
451373ed 1001 }
3d9645ae 1002 for ($numberadded = $maxnumber+1; $numberadded <= $maxnumber + $numbertoadd; $numberadded++) {
f184c65c 1003 if (isset($datasetdefs[$defid]->items[$numberadded])) {
3d9645ae 1004 // In case of regenerate it modifies the already existing record.
f184c65c 1005 if ($defregenerate) {
0ff4bd08 1006 $datasetitem = new stdClass();
fe6ce234 1007 $datasetitem->id = $datasetdefs[$defid]->items[$numberadded]->id;
f184c65c 1008 $datasetitem->definition = $datasetdef->id;
fe6ce234 1009 $datasetitem->itemnumber = $numberadded;
f184c65c
TH
1010 $datasetitem->value =
1011 $this->generate_dataset_item($datasetdef->options);
fe6ce234
DC
1012 $DB->update_record('question_dataset_items', $datasetitem);
1013 }
3d9645ae 1014 // If not regenerate do nothing as there is already a record.
451373ed 1015 } else {
0ff4bd08 1016 $datasetitem = new stdClass();
f184c65c 1017 $datasetitem->definition = $datasetdef->id;
451373ed 1018 $datasetitem->itemnumber = $numberadded;
1019 if ($this->supports_dataset_item_generation()) {
f184c65c
TH
1020 $datasetitem->value =
1021 $this->generate_dataset_item($datasetdef->options);
451373ed 1022 } else {
1023 $datasetitem->value = '';
1024 }
bb4b6010 1025 $DB->insert_record('question_dataset_items', $datasetitem);
c31f631b 1026 }
3d9645ae 1027 }// For number added.
1028 }// Datasetsdefs end.
f184c65c 1029 $maxnumber += $numbertoadd;
c31f631b 1030 foreach ($datasetdefs as $key => $newdef) {
451373ed 1031 if (isset($newdef->id) && $newdef->itemcount <= $maxnumber) {
1032 $newdef->itemcount = $maxnumber;
3d9645ae 1033 // Save the new value for options.
f34488b2 1034 $DB->update_record('question_dataset_definitions', $newdef);
451373ed 1035 }
1036 }
f34488b2 1037 }
451373ed 1038
f184c65c
TH
1039 if (isset($fromform->deletebutton)) {
1040 if (isset($fromform->selectdelete)) {
1041 $newmaxnumber = $maxnumber-$fromform->selectdelete;
1042 } else {
1043 $newmaxnumber = $maxnumber-1;
1044 }
1045 if ($newmaxnumber < 0) {
1046 $newmaxnumber = 0;
1047 }
60b5ecd3 1048 foreach ($datasetdefs as $datasetdef) {
1049 if ($datasetdef->itemcount == $maxnumber) {
f184c65c 1050 $datasetdef->itemcount= $newmaxnumber;
bb4b6010 1051 $DB->update_record('question_dataset_definitions', $datasetdef);
60b5ecd3 1052 }
1053 }
fe6ce234 1054 }
60b5ecd3 1055 }
f184c65c 1056 public function generate_dataset_item($options) {
6dbcacee 1057 if (!preg_match('~^(uniform|loguniform):([^:]*):([^:]*):([0-9]*)$~',
f184c65c
TH
1058 $options, $regs)) {
1059 // Unknown options...
1060 return false;
1061 }
516cf3eb 1062 if ($regs[1] == 'uniform') {
1063 $nbr = $regs[2] + ($regs[3]-$regs[2])*mt_rand()/mt_getrandmax();
f184c65c 1064 return sprintf("%.".$regs[4].'f', $nbr);
516cf3eb 1065
1066 } else if ($regs[1] == 'loguniform') {
3d9645ae 1067 $log0 = log(abs($regs[2])); // It would have worked the other way to.
f34488b2 1068 $nbr = exp($log0 + (log(abs($regs[3])) - $log0)*mt_rand()/mt_getrandmax());
f184c65c 1069 return sprintf("%.".$regs[4].'f', $nbr);
f34488b2 1070
516cf3eb 1071 } else {
0b4f4187 1072 print_error('disterror', 'question', '', $regs[1]);
516cf3eb 1073 }
1074 return '';
1075 }
1076
f184c65c 1077 public function comment_header($question) {
28a27ef1 1078 $strheader = '';
516cf3eb 1079 $delimiter = '';
60b5ecd3 1080
1081 $answers = $question->options->answers;
1082
450f1127 1083 foreach ($answers as $key => $answer) {
9ddb8a56 1084 $ans = shorten_text($answer->answer, 17, true);
1085 $strheader .= $delimiter.$ans;
fe6ce234 1086 $delimiter = '<br/><br/><br/>';
516cf3eb 1087 }
1088 return $strheader;
1089 }
1090
f184c65c
TH
1091 public function comment_on_datasetitems($qtypeobj, $questionid, $questiontext,
1092 $answers, $data, $number) {
1093 global $DB;
0ff4bd08 1094 $comment = new stdClass();
450f1127 1095 $comment->stranswers = array();
f184c65c 1096 $comment->outsidelimit = false;
450f1127 1097 $comment->answers = array();
3d9645ae 1098 // Find a default unit.
f184c65c
TH
1099 if (!empty($questionid) && $unit = $DB->get_record('question_numerical_units',
1100 array('question' => $questionid, 'multiplier' => 1.0))) {
516cf3eb 1101 $unit = $unit->unit;
1102 } else {
1103 $unit = '';
1104 }
1105
450f1127 1106 $answers = fullclone($answers);
516cf3eb 1107 $errors = '';
1108 $delimiter = ': ';
f184c65c 1109 $virtualqtype = $qtypeobj->get_virtual_qtype();
450f1127 1110 foreach ($answers as $key => $answer) {
f184c65c 1111 $formula = $this->substitute_variables($answer->answer, $data);
1d9ed698 1112 $formattedanswer = qtype_calculated_calculate_answer(
fe6ce234
DC
1113 $answer->answer, $data, $answer->tolerance,
1114 $answer->tolerancetype, $answer->correctanswerlength,
1115 $answer->correctanswerformat, $unit);
f184c65c 1116 if ($formula === '*') {
fe6ce234 1117 $answer->min = ' ';
f184c65c
TH
1118 $formattedanswer->answer = $answer->answer;
1119 } else {
18f9b2d2
TH
1120 eval('$ansvalue = '.$formula.';');
1121 $ans = new qtype_numerical_answer(0, $ansvalue, 0, '', 0, $answer->tolerance);
1122 $ans->tolerancetype = $answer->tolerancetype;
1123 list($answer->min, $answer->max) = $ans->get_tolerance_interval($answer);
fe6ce234 1124 }
1d9ed698 1125 if ($answer->min === '') {
3d9645ae 1126 // This should mean that something is wrong.
450f1127 1127 $comment->stranswers[$key] = " $formattedanswer->answer".'<br/><br/>';
f184c65c
TH
1128 } else if ($formula === '*') {
1129 $comment->stranswers[$key] = $formula . ' = ' .
1130 get_string('anyvalue', 'qtype_calculated') . '<br/><br/><br/>';
1131 } else {
9ddb8a56 1132 $formula = shorten_text($formula, 57, true);
f184c65c 1133 $comment->stranswers[$key] = $formula . ' = ' . $formattedanswer->answer . '<br/>';
0cba6a8d 1134 $correcttrue = new stdClass();
f184c65c 1135 $correcttrue->correct = $formattedanswer->answer;
9ddb8a56 1136 $correcttrue->true = '';
f184c65c
TH
1137 if ($formattedanswer->answer < $answer->min ||
1138 $formattedanswer->answer > $answer->max) {
1139 $comment->outsidelimit = true;
450f1127 1140 $comment->answers[$key] = $key;
f184c65c
TH
1141 $comment->stranswers[$key] .=
1142 get_string('trueansweroutsidelimits', 'qtype_calculated', $correcttrue);
1143 } else {
1144 $comment->stranswers[$key] .=
1145 get_string('trueanswerinsidelimits', 'qtype_calculated', $correcttrue);
0d03d6be 1146 }
f184c65c 1147 $comment->stranswers[$key] .= '<br/>';
18f9b2d2
TH
1148 $comment->stranswers[$key] .= get_string('min', 'qtype_calculated') .
1149 $delimiter . $answer->min . ' --- ';
1150 $comment->stranswers[$key] .= get_string('max', 'qtype_calculated') .
1151 $delimiter . $answer->max;
516cf3eb 1152 }
1153 }
450f1127 1154 return fullclone($comment);
516cf3eb 1155 }
1156
f184c65c 1157 public function tolerance_types() {
06525476
TH
1158 return array(
1159 '1' => get_string('relative', 'qtype_numerical'),
f184c65c 1160 '2' => get_string('nominal', 'qtype_numerical'),
06525476
TH
1161 '3' => get_string('geometric', 'qtype_numerical')
1162 );
516cf3eb 1163 }
1164
f184c65c
TH
1165 public function dataset_options($form, $name, $mandatory = true,
1166 $renameabledatasets = false) {
fe6ce234 1167 // Takes datasets from the parent implementation but
3d9645ae 1168 // filters options that are currently not accepted by calculated.
1169 // It also determines a default selection.
1170 // Param $renameabledatasets not implemented anywhere.
1171
f184c65c
TH
1172 list($options, $selected) = $this->dataset_options_from_database(
1173 $form, $name, '', 'qtype_calculated');
fd0973cc 1174
516cf3eb 1175 foreach ($options as $key => $whatever) {
6dbcacee 1176 if (!preg_match('~^1-~', $key) && $key != '0') {
516cf3eb 1177 unset($options[$key]);
1178 }
1179 }
1180 if (!$selected) {
f184c65c 1181 if ($mandatory) {
3d9645ae 1182 $selected = "1-0-$name"; // Default.
f184c65c 1183 } else {
3d9645ae 1184 $selected = '0'; // Default.
f34488b2 1185 }
516cf3eb 1186 }
1187 return array($options, $selected);
1188 }
1189
f184c65c
TH
1190 public function construct_dataset_menus($form, $mandatorydatasets,
1191 $optionaldatasets) {
1192 global $OUTPUT;
1193 $datasetmenus = array();
1194 foreach ($mandatorydatasets as $datasetname) {
1195 if (!isset($datasetmenus[$datasetname])) {
1196 list($options, $selected) =
1197 $this->dataset_options($form, $datasetname);
1198 unset($options['0']); // Mandatory...
3211569a 1199 $datasetmenus[$datasetname] = html_writer::select(
f184c65c 1200 $options, 'dataset[]', $selected, null);
516cf3eb 1201 }
f184c65c
TH
1202 }
1203 foreach ($optionaldatasets as $datasetname) {
1204 if (!isset($datasetmenus[$datasetname])) {
1205 list($options, $selected) =
1206 $this->dataset_options($form, $datasetname);
3211569a 1207 $datasetmenus[$datasetname] = html_writer::select(
f184c65c 1208 $options, 'dataset[]', $selected, null);
516cf3eb 1209 }
1210 }
f184c65c
TH
1211 return $datasetmenus;
1212 }
516cf3eb 1213
f184c65c
TH
1214 public function substitute_variables($str, $dataset) {
1215 global $OUTPUT;
3d9645ae 1216 // Testing for wrong numerical values.
1217 // All calculations used this function so testing here should be OK.
fbe2cfea 1218
1219 foreach ($dataset as $name => $value) {
f184c65c
TH
1220 $val = $value;
1221 if (! is_numeric($val)) {
0ff4bd08 1222 $a = new stdClass();
f184c65c
TH
1223 $a->name = '{'.$name.'}';
1224 $a->value = $value;
1225 echo $OUTPUT->notification(get_string('notvalidnumber', 'qtype_calculated', $a));
1226 $val = 1.0;
aeb15530 1227 }
3d9645ae 1228 if ($val <= 0) { // MDL-36025 Use parentheses for "-0" .
1ee53ca9 1229 $str = str_replace('{'.$name.'}', '('.$val.')', $str);
fbe2cfea 1230 } else {
1ee53ca9 1231 $str = str_replace('{'.$name.'}', $val, $str);
fbe2cfea 1232 }
1233 }
1234 return $str;
1235 }
24400682 1236
f184c65c
TH
1237 public function evaluate_equations($str, $dataset) {
1238 $formula = $this->substitute_variables($str, $dataset);
fe6ce234 1239 if ($error = qtype_calculated_find_formula_errors($formula)) {
28a27ef1 1240 return $error;
1241 }
1242 return $str;
1243 }
aeb15530 1244
f184c65c
TH
1245 public function substitute_variables_and_eval($str, $dataset) {
1246 $formula = $this->substitute_variables($str, $dataset);
fe6ce234 1247 if ($error = qtype_calculated_find_formula_errors($formula)) {
516cf3eb 1248 return $error;
1249 }
3d9645ae 1250 // Calculate the correct answer.
516cf3eb 1251 if (empty($formula)) {
1252 $str = '';
f184c65c 1253 } else if ($formula === '*') {
078cc3f0 1254 $str = '*';
516cf3eb 1255 } else {
a538cf83 1256 $str = null;
f34488b2 1257 eval('$str = '.$formula.';');
516cf3eb 1258 }
1259 return $str;
1260 }
f34488b2 1261
f184c65c 1262 public function get_dataset_definitions($questionid, $newdatasets) {
fbe2cfea 1263 global $DB;
3d9645ae 1264 // Get the existing datasets for this question.
fbe2cfea 1265 $datasetdefs = array();
1266 if (!empty($questionid)) {
1267 global $CFG;
1268 $sql = "SELECT i.*
fe6ce234 1269 FROM {question_datasets} d, {question_dataset_definitions} i
e6d76583
TH
1270 WHERE d.question = ? AND d.datasetdefinition = i.id
1271 ORDER BY i.id";
fbe2cfea 1272 if ($records = $DB->get_records_sql($sql, array($questionid))) {
1273 foreach ($records as $r) {
1274 $datasetdefs["$r->type-$r->category-$r->name"] = $r;
1275 }
1276 }
1277 }
1278
1279 foreach ($newdatasets as $dataset) {
1280 if (!$dataset) {
1281 continue; // The no dataset case...
1282 }
1283
1284 if (!isset($datasetdefs[$dataset])) {
3d9645ae 1285 // Make new datasetdef.
fbe2cfea 1286 list($type, $category, $name) = explode('-', $dataset, 3);
0ff4bd08 1287 $datasetdef = new stdClass();
fbe2cfea 1288 $datasetdef->type = $type;
1289 $datasetdef->name = $name;
1290 $datasetdef->category = $category;
1291 $datasetdef->itemcount = 0;
1292 $datasetdef->options = 'uniform:1.0:10.0:1';
1293 $datasetdefs[$dataset] = clone($datasetdef);
1294 }
1295 }
1296 return $datasetdefs;
1297 }
1298
f184c65c 1299 public function save_dataset_definitions($form) {
fbe2cfea 1300 global $DB;
3d9645ae 1301 // Save synchronize.
aeb15530 1302
fe6ce234
DC
1303 if (empty($form->dataset)) {
1304 $form->dataset = array();
1305 }
3d9645ae 1306 // Save datasets.
fbe2cfea 1307 $datasetdefinitions = $this->get_dataset_definitions($form->id, $form->dataset);
1308 $tmpdatasets = array_flip($form->dataset);
1309 $defids = array_keys($datasetdefinitions);
1310 foreach ($defids as $defid) {
1311 $datasetdef = &$datasetdefinitions[$defid];
1312 if (isset($datasetdef->id)) {
1313 if (!isset($tmpdatasets[$defid])) {
3d9645ae 1314 // This dataset is not used any more, delete it.
f184c65c
TH
1315 $DB->delete_records('question_datasets',
1316 array('question' => $form->id, 'datasetdefinition' => $datasetdef->id));
1317 if ($datasetdef->category == 0) {
3d9645ae 1318 // Question local dataset.
f184c65c
TH
1319 $DB->delete_records('question_dataset_definitions',
1320 array('id' => $datasetdef->id));
1321 $DB->delete_records('question_dataset_items',
1322 array('definition' => $datasetdef->id));
fbe2cfea 1323 }
1324 }
3d9645ae 1325 // This has already been saved or just got deleted.
fbe2cfea 1326 unset($datasetdefinitions[$defid]);
1327 continue;
1328 }
1329
bb4b6010 1330 $datasetdef->id = $DB->insert_record('question_dataset_definitions', $datasetdef);
fbe2cfea 1331
1332 if (0 != $datasetdef->category) {
3d9645ae 1333 // We need to look for already existing datasets in the category.
1334 // First creating the datasetdefinition above
1335 // then we can manage to automatically take care of some possible realtime concurrence.
1336
fe6ce234 1337 if ($olderdatasetdefs = $DB->get_records_select('question_dataset_definitions',
f184c65c
TH
1338 'type = ? AND name = ? AND category = ? AND id < ?
1339 ORDER BY id DESC',
1340 array($datasetdef->type, $datasetdef->name,
1341 $datasetdef->category, $datasetdef->id))) {
fbe2cfea 1342
1343 while ($olderdatasetdef = array_shift($olderdatasetdefs)) {
f184c65c
TH
1344 $DB->delete_records('question_dataset_definitions',
1345 array('id' => $datasetdef->id));
fbe2cfea 1346 $datasetdef = $olderdatasetdef;
1347 }
1348 }
1349 }
1350
3d9645ae 1351 // Create relation to this dataset.
0ff4bd08 1352 $questiondataset = new stdClass();
fbe2cfea 1353 $questiondataset->question = $form->id;
1354 $questiondataset->datasetdefinition = $datasetdef->id;
bb4b6010 1355 $DB->insert_record('question_datasets', $questiondataset);
fbe2cfea 1356 unset($datasetdefinitions[$defid]);
1357 }
1358
1359 // Remove local obsolete datasets as well as relations
3d9645ae 1360 // to datasets in other categories.
fbe2cfea 1361 if (!empty($datasetdefinitions)) {
1362 foreach ($datasetdefinitions as $def) {
f184c65c
TH
1363 $DB->delete_records('question_datasets',
1364 array('question' => $form->id, 'datasetdefinition' => $def->id));
fbe2cfea 1365
3d9645ae 1366 if ($def->category == 0) { // Question local dataset.
f184c65c
TH
1367 $DB->delete_records('question_dataset_definitions',
1368 array('id' => $def->id));
1369 $DB->delete_records('question_dataset_items',
1370 array('definition' => $def->id));
fbe2cfea 1371 }
1372 }
1373 }
1374 }
f184c65c 1375 /** This function create a copy of the datasets (definition and dataitems)
fe6ce234
DC
1376 * from the preceding question if they remain in the new question
1377 * otherwise its create the datasets that have been added as in the
1378 * save_dataset_definitions()
1379 */
f184c65c 1380 public function save_as_new_dataset_definitions($form, $initialid) {
fe6ce234 1381 global $CFG, $DB;
3d9645ae 1382 // Get the datasets from the intial question.
fbe2cfea 1383 $datasetdefinitions = $this->get_dataset_definitions($initialid, $form->dataset);
3d9645ae 1384 // Param $tmpdatasets contains those of the new question.
fbe2cfea 1385 $tmpdatasets = array_flip($form->dataset);
3d9645ae 1386 $defids = array_keys($datasetdefinitions);// New datasets.
fbe2cfea 1387 foreach ($defids as $defid) {
1388 $datasetdef = &$datasetdefinitions[$defid];
1389 if (isset($datasetdef->id)) {
3d9645ae 1390 // This dataset exist in the initial question.
fbe2cfea 1391 if (!isset($tmpdatasets[$defid])) {
3d9645ae 1392 // Do not exist in the new question so ignore.
fbe2cfea 1393 unset($datasetdefinitions[$defid]);
1394 continue;
1395 }
3d9645ae 1396 // Create a copy but not for category one.
fbe2cfea 1397 if (0 == $datasetdef->category) {
f184c65c
TH
1398 $olddatasetid = $datasetdef->id;
1399 $olditemcount = $datasetdef->itemcount;
1400 $datasetdef->itemcount = 0;
1401 $datasetdef->id = $DB->insert_record('question_dataset_definitions',
1402 $datasetdef);
3d9645ae 1403 // Copy the dataitems.
fe6ce234 1404 $olditems = $this->get_database_dataset_items($olddatasetid);
f184c65c 1405 if (count($olditems) > 0) {
fbe2cfea 1406 $itemcount = 0;
f184c65c 1407 foreach ($olditems as $item) {
fbe2cfea 1408 $item->definition = $datasetdef->id;
bb4b6010 1409 $DB->insert_record('question_dataset_items', $item);
1410 $itemcount++;
fbe2cfea 1411 }
3d9645ae 1412 // Update item count to olditemcount if
1413 // at least this number of items has been recover from the database.
f184c65c 1414 if ($olditemcount <= $itemcount) {
ed15021f
PP
1415 $datasetdef->itemcount = $olditemcount;
1416 } else {
f184c65c 1417 $datasetdef->itemcount = $itemcount;
ed15021f 1418 }
fbe2cfea 1419 $DB->update_record('question_dataset_definitions', $datasetdef);
3d9645ae 1420 } // End of copy the dataitems.
1421 }// End of copy the datasetdef.
fbe2cfea 1422 // Create relation to the new question with this
3d9645ae 1423 // copy as new datasetdef from the initial question.
0ff4bd08 1424 $questiondataset = new stdClass();
fbe2cfea 1425 $questiondataset->question = $form->id;
1426 $questiondataset->datasetdefinition = $datasetdef->id;
bb4b6010 1427 $DB->insert_record('question_datasets', $questiondataset);
fbe2cfea 1428 unset($datasetdefinitions[$defid]);
1429 continue;
3d9645ae 1430 }// End of datasetdefs from the initial question.
1431 // Really new one code similar to save_dataset_definitions().
bb4b6010 1432 $datasetdef->id = $DB->insert_record('question_dataset_definitions', $datasetdef);
fbe2cfea 1433
1434 if (0 != $datasetdef->category) {
1435 // We need to look for already existing
1436 // datasets in the category.
1437 // By first creating the datasetdefinition above we
1438 // can manage to automatically take care of
3d9645ae 1439 // some possible realtime concurrence.
f184c65c
TH
1440 if ($olderdatasetdefs = $DB->get_records_select('question_dataset_definitions',
1441 "type = ? AND name = ? AND category = ? AND id < ?
1442 ORDER BY id DESC",
1443 array($datasetdef->type, $datasetdef->name,
1444 $datasetdef->category, $datasetdef->id))) {
fe6ce234 1445
f184c65c
TH
1446 while ($olderdatasetdef = array_shift($olderdatasetdefs)) {
1447 $DB->delete_records('question_dataset_definitions',
1448 array('id' => $datasetdef->id));
1449 $datasetdef = $olderdatasetdef;
fbe2cfea 1450 }
f184c65c 1451 }
fbe2cfea 1452 }
1453
3d9645ae 1454 // Create relation to this dataset.
0ff4bd08 1455 $questiondataset = new stdClass();
fbe2cfea 1456 $questiondataset->question = $form->id;
1457 $questiondataset->datasetdefinition = $datasetdef->id;
bb4b6010 1458 $DB->insert_record('question_datasets', $questiondataset);
fbe2cfea 1459 unset($datasetdefinitions[$defid]);
1460 }
1461
1462 // Remove local obsolete datasets as well as relations
3d9645ae 1463 // to datasets in other categories.
fbe2cfea 1464 if (!empty($datasetdefinitions)) {
1465 foreach ($datasetdefinitions as $def) {
f184c65c
TH
1466 $DB->delete_records('question_datasets',
1467 array('question' => $form->id, 'datasetdefinition' => $def->id));
fbe2cfea 1468
3d9645ae 1469 if ($def->category == 0) { // Question local dataset.
f184c65c
TH
1470 $DB->delete_records('question_dataset_definitions',
1471 array('id' => $def->id));
1472 $DB->delete_records('question_dataset_items',
1473 array('definition' => $def->id));
fbe2cfea 1474 }
1475 }
1476 }
1477 }
1478
3d9645ae 1479 // Dataset functionality.
f184c65c 1480 public function pick_question_dataset($question, $datasetitem) {
fbe2cfea 1481 // Select a dataset in the following format:
3d9645ae 1482 // an array indexed by the variable names (d.name) pointing to the value
1483 // to be substituted.
fbe2cfea 1484 global $CFG, $DB;
a2155a7b 1485 if (!$dataitems = $DB->get_records_sql(
f184c65c
TH
1486 "SELECT i.id, d.name, i.value
1487 FROM {question_dataset_definitions} d,
1488 {question_dataset_items} i,
1489 {question_datasets} q
1490 WHERE q.question = ?
1491 AND q.datasetdefinition = d.id
1492 AND d.id = i.definition
1493 AND i.itemnumber = ?
1494 ORDER BY i.id DESC ", array($question->id, $datasetitem))) {
1495 $a = new stdClass();
1496 $a->id = $question->id;
1497 $a->item = $datasetitem;
1498 print_error('cannotgetdsfordependent', 'question', '', $a);
fbe2cfea 1499 }
a2155a7b 1500 $dataset = Array();
f184c65c
TH
1501 foreach ($dataitems as $id => $dataitem) {
1502 if (!isset($dataset[$dataitem->name])) {
1503 $dataset[$dataitem->name] = $dataitem->value;
fe6ce234
DC
1504 }
1505 }
fbe2cfea 1506 return $dataset;
1507 }
aeb15530 1508
f184c65c
TH
1509 public function dataset_options_from_database($form, $name, $prefix = '',
1510 $langfile = 'qtype_calculated') {
d90b016b 1511 global $CFG, $DB;
3d9645ae 1512 $type = 1; // Only type = 1 (i.e. old 'LITERAL') has ever been used.
fbe2cfea 1513 // First options - it is not a dataset...
1514 $options['0'] = get_string($prefix.'nodataset', $langfile);
3d9645ae 1515 // New question no local.
f184c65c 1516 if (!isset($form->id) || $form->id == 0) {
d90b016b
PP
1517 $key = "$type-0-$name";
1518 $options[$key] = get_string($prefix."newlocal$type", $langfile);
0ff4bd08 1519 $currentdatasetdef = new stdClass();
d90b016b 1520 $currentdatasetdef->type = '0';
f184c65c 1521 } else {
3d9645ae 1522 // Construct question local options.
fe6ce234
DC
1523 $sql = "SELECT a.*
1524 FROM {question_dataset_definitions} a, {question_datasets} b
1525 WHERE a.id = b.datasetdefinition AND a.type = '1' AND b.question = ? AND a.name = ?";
1526 $currentdatasetdef = $DB->get_record_sql($sql, array($form->id, $name));
1527 if (!$currentdatasetdef) {
0cba6a8d 1528 $currentdatasetdef = new stdClass();
fe6ce234
DC
1529 $currentdatasetdef->type = '0';
1530 }
1531 $key = "$type-0-$name";
1532 if ($currentdatasetdef->type == $type
f184c65c
TH
1533 and $currentdatasetdef->category == 0) {
1534 $options[$key] = get_string($prefix."keptlocal$type", $langfile);
1535 } else {
1536 $options[$key] = get_string($prefix."newlocal$type", $langfile);
1537 }
d90b016b 1538 }
3d9645ae 1539 // Construct question category options.
fbe2cfea 1540 $categorydatasetdefs = $DB->get_records_sql(
fe6ce234
DC
1541 "SELECT b.question, a.*
1542 FROM {question_datasets} b,
1543 {question_dataset_definitions} a
1544 WHERE a.id = b.datasetdefinition
1545 AND a.type = '1'
1546 AND a.category = ?
1547 AND a.name = ?", array($form->category, $name));
f184c65c 1548 $type = 1;
cae0b24a 1549 $key = "$type-$form->category-$name";
f184c65c 1550 if (!empty($categorydatasetdefs)) {
3d9645ae 1551 // There is at least one with the same name.
f184c65c 1552 if (isset($form->id) && isset($categorydatasetdefs[$form->id])) {
3d9645ae 1553 // It is already used by this question.
fe6ce234
DC
1554 $options[$key] = get_string($prefix."keptcategory$type", $langfile);
1555 } else {
1556 $options[$key] = get_string($prefix."existingcategory$type", $langfile);
1557 }
cae0b24a 1558 } else {
1559 $options[$key] = get_string($prefix."newcategory$type", $langfile);
fbe2cfea 1560 }
fbe2cfea 1561 // All done!
1562 return array($options, $currentdatasetdef->type
fe6ce234
DC
1563 ? "$currentdatasetdef->type-$currentdatasetdef->category-$name"
1564 : '');
fbe2cfea 1565 }
1566
f184c65c 1567 public function find_dataset_names($text) {
3d9645ae 1568 // Returns the possible dataset names found in the text as an array.
1569 // The array has the dataset name for both key and value.
fbe2cfea 1570 $datasetnames = array();
6dbcacee 1571 while (preg_match('~\\{([[:alpha:]][^>} <{"\']*)\\}~', $text, $regs)) {
fbe2cfea 1572 $datasetnames[$regs[1]] = $regs[1];
1573 $text = str_replace($regs[0], '', $text);
1574 }
1575 return $datasetnames;
1576 }
1577
fd0973cc 1578 /**
fe6ce234
DC
1579 * This function retrieve the item count of the available category shareable
1580 * wild cards that is added as a comment displayed when a wild card with
1581 * the same name is displayed in datasetdefinitions_form.php
1582 */
f184c65c 1583 public function get_dataset_definitions_category($form) {
f34488b2 1584 global $CFG, $DB;
fd0973cc 1585 $datasetdefs = array();
1586 $lnamemax = 30;
f34488b2 1587 if (!empty($form->category)) {
f184c65c 1588 $sql = "SELECT i.*, d.*
fe6ce234
DC
1589 FROM {question_datasets} d, {question_dataset_definitions} i
1590 WHERE i.id = d.datasetdefinition AND i.category = ?";
1591 if ($records = $DB->get_records_sql($sql, array($form->category))) {
1592 foreach ($records as $r) {
f184c65c
TH
1593 if (!isset ($datasetdefs["$r->name"])) {
1594 $datasetdefs["$r->name"] = $r->itemcount;
1595 }
fd0973cc 1596 }
fe6ce234 1597 }
f34488b2 1598 }
f184c65c 1599 return $datasetdefs;
f34488b2 1600 }
fd0973cc 1601
1602 /**
fe6ce234
DC
1603 * This function build a table showing the available category shareable
1604 * wild cards, their name, their definition (Min, Max, Decimal) , the item count
1605 * and the name of the question where they are used.
1606 * This table is intended to be add before the question text to help the user use
1607 * these wild cards
1608 */
f184c65c 1609 public function print_dataset_definitions_category($form) {
f34488b2 1610 global $CFG, $DB;
fd0973cc 1611 $datasetdefs = array();
1612 $lnamemax = 22;
f184c65c 1613 $namestr = get_string('name');
f184c65c
TH
1614 $rangeofvaluestr = get_string('minmax', 'qtype_calculated');
1615 $questionusingstr = get_string('usedinquestion', 'qtype_calculated');
1616 $itemscountstr = get_string('itemscount', 'qtype_calculated');
1617 $text = '';
f34488b2 1618 if (!empty($form->category)) {
18f9b2d2 1619 list($category) = explode(',', $form->category);
f184c65c 1620 $sql = "SELECT i.*, d.*
fe6ce234
DC
1621 FROM {question_datasets} d,
1622 {question_dataset_definitions} i
1623 WHERE i.id = d.datasetdefinition
1624 AND i.category = ?";
f34488b2 1625 if ($records = $DB->get_records_sql($sql, array($category))) {
fd0973cc 1626 foreach ($records as $r) {
1627 $sql1 = "SELECT q.*
fe6ce234
DC
1628 FROM {question} q
1629 WHERE q.id = ?";
f184c65c 1630 if (!isset ($datasetdefs["$r->type-$r->category-$r->name"])) {
c52bcbf6 1631 $datasetdefs["$r->type-$r->category-$r->name"] = $r;
fd0973cc 1632 }
f34488b2 1633 if ($questionb = $DB->get_records_sql($sql1, array($r->question))) {
c52bcbf6
TH
1634 if (!isset ($datasetdefs["$r->type-$r->category-$r->name"]->questions[$r->question])) {
1635 $datasetdefs["$r->type-$r->category-$r->name"]->questions[$r->question] = new stdClass();
1636 }
f184c65c
TH
1637 $datasetdefs["$r->type-$r->category-$r->name"]->questions[
1638 $r->question]->name = $questionb[$r->question]->name;
fd0973cc 1639 }
1640 }
1641 }
1642 }
f184c65c 1643 if (!empty ($datasetdefs)) {
f34488b2 1644
f184c65c
TH
1645 $text = "<table width=\"100%\" border=\"1\"><tr>
1646 <th style=\"white-space:nowrap;\" class=\"header\"
1647 scope=\"col\">$namestr</th>
1648 <th style=\"white-space:nowrap;\" class=\"header\"
1649 scope=\"col\">$rangeofvaluestr</th>
1650 <th style=\"white-space:nowrap;\" class=\"header\"
1651 scope=\"col\">$itemscountstr</th>
1652 <th style=\"white-space:nowrap;\" class=\"header\"
1653 scope=\"col\">$questionusingstr</th>
1654 </tr>";
1655 foreach ($datasetdefs as $datasetdef) {
1656 list($distribution, $min, $max, $dec) = explode(':', $datasetdef->options, 4);
1657 $text .= "<tr>
1658 <td valign=\"top\" align=\"center\">$datasetdef->name</td>
1659 <td align=\"center\" valign=\"top\">$min <strong>-</strong> $max</td>
1660 <td align=\"right\" valign=\"top\">$datasetdef->itemcount&nbsp;&nbsp;</td>
1661 <td align=\"left\">";
fd0973cc 1662 foreach ($datasetdef->questions as $qu) {
3d9645ae 1663 // Limit the name length displayed.
fd0973cc 1664 if (!empty($qu->name)) {
1665 $qu->name = (strlen($qu->name) > $lnamemax) ?
fe6ce234 1666 substr($qu->name, 0, $lnamemax).'...' : $qu->name;
fd0973cc 1667 } else {
1668 $qu->name = '';
1669 }
f184c65c 1670 $text .= " &nbsp;&nbsp; $qu->name <br/>";
f34488b2 1671 }
f184c65c 1672 $text .= "</td></tr>";
fd0973cc 1673 }
f184c65c
TH
1674 $text .= "</table>";
1675 } else {
1676 $text .= get_string('nosharedwildcard', 'qtype_calculated');
fd0973cc 1677 }
f184c65c 1678 return $text;
f34488b2 1679 }
8b59e62e 1680
ab8b5142 1681 /**
fe6ce234
DC
1682 * This function build a table showing the available category shareable
1683 * wild cards, their name, their definition (Min, Max, Decimal) , the item count
1684 * and the name of the question where they are used.
1685 * This table is intended to be add before the question text to help the user use
1686 * these wild cards
1687 */
ab8b5142 1688
f184c65c 1689 public function print_dataset_definitions_category_shared($question, $datasetdefsq) {
ab8b5142
PP
1690 global $CFG, $DB;
1691 $datasetdefs = array();
1692 $lnamemax = 22;
f184c65c 1693 $namestr = get_string('name', 'quiz');
f184c65c
TH
1694 $rangeofvaluestr = get_string('minmax', 'qtype_calculated');
1695 $questionusingstr = get_string('usedinquestion', 'qtype_calculated');
1696 $itemscountstr = get_string('itemscount', 'qtype_calculated');
1697 $text = '';
ab8b5142
PP
1698 if (!empty($question->category)) {
1699 list($category) = explode(',', $question->category);
f184c65c 1700 $sql = "SELECT i.*, d.*
fe6ce234
DC
1701 FROM {question_datasets} d, {question_dataset_definitions} i
1702 WHERE i.id = d.datasetdefinition AND i.category = ?";
ab8b5142
PP
1703 if ($records = $DB->get_records_sql($sql, array($category))) {
1704 foreach ($records as $r) {
2b3f70db 1705 $key = "$r->type-$r->category-$r->name";
ab8b5142 1706 $sql1 = "SELECT q.*
fe6ce234
DC
1707 FROM {question} q
1708 WHERE q.id = ?";
2b3f70db
TH
1709 if (!isset($datasetdefs[$key])) {
1710 $datasetdefs[$key] = $r;
ab8b5142
PP
1711 }
1712 if ($questionb = $DB->get_records_sql($sql1, array($r->question))) {
2b3f70db
TH
1713 $datasetdefs[$key]->questions[$r->question] = new stdClass();
1714 $datasetdefs[$key]->questions[$r->question]->name =
1715 $questionb[$r->question]->name;
1716 $datasetdefs[$key]->questions[$r->question]->id =
1717 $questionb[$r->question]->id;
ab8b5142
PP
1718 }
1719 }
1720 }
1721 }
f184c65c 1722 if (!empty ($datasetdefs)) {
ab8b5142 1723
f184c65c
TH
1724 $text = "<table width=\"100%\" border=\"1\"><tr>
1725 <th style=\"white-space:nowrap;\" class=\"header\"
1726 scope=\"col\">$namestr</th>";
1727 $text .= "<th style=\"white-space:nowrap;\" class=\"header\"
1728 scope=\"col\">$itemscountstr</th>";
1729 $text .= "<th style=\"white-space:nowrap;\" class=\"header\"
1730 scope=\"col\">&nbsp;&nbsp;$questionusingstr &nbsp;&nbsp;</th>";
1731 $text .= "<th style=\"white-space:nowrap;\" class=\"header\"
1732 scope=\"col\">Quiz</th>";
1733 $text .= "<th style=\"white-space:nowrap;\" class=\"header\"
1734 scope=\"col\">Attempts</th></tr>";
1735 foreach ($datasetdefs as $datasetdef) {
1736 list($distribution, $min, $max, $dec) = explode(':', $datasetdef->options, 4);
8b59e62e 1737 $count = count($datasetdef->questions);
f184c65c
TH
1738 $text .= "<tr>
1739 <td style=\"white-space:nowrap;\" valign=\"top\"
1740 align=\"center\" rowspan=\"$count\"> $datasetdef->name </td>
1741 <td align=\"right\" valign=\"top\"
1742 rowspan=\"$count\">$datasetdef->itemcount</td>";
1743 $line = 0;
ab8b5142 1744 foreach ($datasetdef->questions as $qu) {
3d9645ae 1745 // Limit the name length displayed.
ab8b5142
PP
1746 if (!empty($qu->name)) {
1747 $qu->name = (strlen($qu->name) > $lnamemax) ?
fe6ce234 1748 substr($qu->name, 0, $lnamemax).'...' : $qu->name;
ab8b5142
PP
1749 } else {
1750 $qu->name = '';
1751 }
f184c65c
TH
1752 if ($line) {
1753 $text .= "<tr>";
ab8b5142 1754 }
8b59e62e 1755 $line++;
f184c65c 1756 $text .= "<td align=\"left\" style=\"white-space:nowrap;\">$qu->name</td>";
e5c5f52e 1757 // TODO MDL-43779 should not have quiz-specific code here.
ccba5b88 1758 $nbofquiz = $DB->count_records('quiz_slots', array('questionid' => $qu->id));
dc4a3ea1
TH
1759 $nbofattempts = $DB->count_records_sql("
1760 SELECT count(1)
ccba5b88
TH
1761 FROM {quiz_slots} slot
1762 JOIN {quiz_attempts} quiza ON quiza.quiz = slot.quizid
1763 WHERE slot.questionid = ?
dc4a3ea1
TH
1764 AND quiza.preview = 0", array($qu->id));
1765 if ($nbofquiz > 0) {
f184c65c 1766 $text .= "<td align=\"center\">$nbofquiz</td>";
f184c65c
TH
1767 $text .= "<td align=\"center\">$nbofattempts";
1768 } else {
dc4a3ea1 1769 $text .= "<td align=\"center\">0</td>";
f184c65c 1770 $text .= "<td align=\"left\"><br/>";
fe6ce234 1771 }
8b59e62e 1772
f184c65c 1773 $text .= "</td></tr>";
ab8b5142
PP
1774 }
1775 }
f184c65c
TH
1776 $text .= "</table>";
1777 } else {
1778 $text .= get_string('nosharedwildcard', 'qtype_calculated');
ab8b5142 1779 }
f184c65c 1780 return $text;
ab8b5142
PP
1781 }
1782
f184c65c 1783 public function find_math_equations($text) {
3d9645ae 1784 // Returns the possible dataset names found in the text as an array.
1785 // The array has the dataset name for both key and value.
ab8b5142 1786 $equations = array();
ab8b5142
PP
1787 while (preg_match('~\{=([^[:space:]}]*)}~', $text, $regs)) {
1788 $equations[] = $regs[1];
1789 $text = str_replace($regs[0], '', $text);
1790 }
1791 return $equations;
1792 }
1793
f184c65c 1794 public function get_virtual_qtype() {
18f9b2d2 1795 return question_bank::get_qtype('numerical');
fbe2cfea 1796 }
fd0973cc 1797
d1770e42
TH
1798 public function get_possible_responses($questiondata) {
1799 $responses = array();
1800
1801 $virtualqtype = $this->get_virtual_qtype();
1802 $unit = $virtualqtype->get_default_numerical_unit($questiondata);
1803
24400682
TH
1804 $tolerancetypes = $this->tolerance_types();
1805
1806 $starfound = false;
d1770e42
TH
1807 foreach ($questiondata->options->answers as $aid => $answer) {
1808 $responseclass = $answer->answer;
1809
24400682
TH
1810 if ($responseclass === '*') {
1811 $starfound = true;
1812 } else {
1813 $a = new stdClass();
1814 $a->answer = $virtualqtype->add_unit($questiondata, $responseclass, $unit);
1815 $a->tolerance = $answer->tolerance;
1816 $a->tolerancetype = $tolerancetypes[$answer->tolerancetype];
d1770e42 1817
24400682 1818 $responseclass = get_string('answerwithtolerance', 'qtype_calculated', $a);
d1770e42
TH
1819 }
1820
1821 $responses[$aid] = new question_possible_response($responseclass,
1822 $answer->fraction);
1823 }
24400682
TH
1824
1825 if (!$starfound) {
1826 $responses[0] = new question_possible_response(
1827 get_string('didnotmatchanyanswer', 'question'), 0);
1828 }
1829
d1770e42
TH
1830 $responses[null] = question_possible_response::no_response();
1831
1832 return array($questiondata->id => $responses);
1833 }
1834
1835 public function move_files($questionid, $oldcontextid, $newcontextid) {
fe6ce234 1836 $fs = get_file_storage();
5d548d3e
TH
1837
1838 parent::move_files($questionid, $oldcontextid, $newcontextid);
1839 $this->move_files_in_answers($questionid, $oldcontextid, $newcontextid);
d44480f6 1840 $this->move_files_in_hints($questionid, $oldcontextid, $newcontextid);
9203b705
TH
1841 }
1842
1843 protected function delete_files($questionid, $contextid) {
1844 $fs = get_file_storage();
1845
1846 parent::delete_files($questionid, $contextid);
1847 $this->delete_files_in_answers($questionid, $contextid);
d44480f6 1848 $this->delete_files_in_hints($questionid, $contextid);
fe6ce234 1849 }
516cf3eb 1850}
516cf3eb 1851
d90b016b 1852
7518b645 1853function qtype_calculated_calculate_answer($formula, $individualdata,
f184c65c 1854 $tolerance, $tolerancetype, $answerlength, $answerformat = '1', $unit = '') {
3d9645ae 1855 // The return value has these properties: .
f184c65c
TH
1856 // ->answer the correct answer
1857 // ->min the lower bound for an acceptable response
3d9645ae 1858 // ->max the upper bound for an accetpable response.
0cba6a8d 1859 $calculated = new stdClass();
f184c65c 1860 // Exchange formula variables with the correct values...
18f9b2d2
TH
1861 $answer = question_bank::get_qtype('calculated')->substitute_variables_and_eval(
1862 $formula, $individualdata);
a538cf83
MP
1863 if (!is_numeric($answer)) {
1864 // Something went wrong, so just return NaN.
1865 $calculated->answer = NAN;
1866 return $calculated;
1867 }
9ddb8a56 1868 if ('1' == $answerformat) { // Answer is to have $answerlength decimals.
1869 // Decimal places.
1870 $calculated->answer = sprintf('%.' . $answerlength . 'F', $answer);
516cf3eb 1871
3d9645ae 1872 } else if ($answer) { // Significant figures does only apply if the result is non-zero.
516cf3eb 1873
1874 // Convert to positive answer...
1875 if ($answer < 0) {
1876 $answer = -$answer;
1877 $sign = '-';
1878 } else {
1879 $sign = '';
1880 }
1881
1882 // Determine the format 0.[1-9][0-9]* for the answer...
1883 $p10 = 0;
1884 while ($answer < 1) {
1885 --$p10;
1886 $answer *= 10;
1887 }
1888 while ($answer >= 1) {
1889 ++$p10;
1890 $answer /= 10;
1891 }
3d9645ae 1892 // ... and have the answer rounded of to the correct length.
516cf3eb 1893 $answer = round($answer, $answerlength);
1894
3d9645ae 1895 // If we rounded up to 1.0, place the answer back into 0.[1-9][0-9]* format.
aecb3393
KT
1896 if ($answer >= 1) {
1897 ++$p10;
1898 $answer /= 10;
1899 }
1900
3d9645ae 1901 // Have the answer written on a suitable format:
1902 // either scientific or plain numeric.
516cf3eb 1903 if (-2 > $p10 || 4 < $p10) {
3d9645ae 1904 // Use scientific format.
f184c65c 1905 $exponent = 'e'.--$p10;
516cf3eb 1906 $answer *= 10;
1907 if (1 == $answerlength) {
9ddb8a56 1908 $calculated->answer = $sign.$answer.$exponent;
516cf3eb 1909 } else {
3d9645ae 1910 // Attach additional zeros at the end of $answer.
f184c65c 1911 $answer .= (1 == strlen($answer) ? '.' : '')
fe6ce234 1912 . '00000000000000000000000000000000000000000x';
516cf3eb 1913 $calculated->answer = $sign
9ddb8a56 1914 .substr($answer, 0, $answerlength +1).$exponent;
516cf3eb 1915 }
1916 } else {
3d9645ae 1917 // Stick to plain numeric format.
516cf3eb 1918 $answer *= "1e$p10";
1919 if (0.1 <= $answer / "1e$answerlength") {
9ddb8a56 1920 $calculated->answer = $sign.$answer;
516cf3eb 1921 } else {
3d9645ae 1922 // Could be an idea to add some zeros here.
6dbcacee 1923 $answer .= (preg_match('~^[0-9]*$~', $answer) ? '.' : '')
fe6ce234 1924 . '00000000000000000000000000000000000000000x';
516cf3eb 1925 $oklen = $answerlength + ($p10 < 1 ? 2-$p10 : 1);
9ddb8a56 1926 $calculated->answer = $sign.substr($answer, 0, $oklen);
516cf3eb 1927 }
1928 }
1929
1930 } else {
1931 $calculated->answer = 0.0;
1932 }
9ddb8a56 1933 if ($unit != '') {
1934 $calculated->answer = $calculated->answer . ' ' . $unit;
1935 }
516cf3eb 1936
3d9645ae 1937 // Return the result.
516cf3eb 1938 return $calculated;
1939}
1940
1941
44f726a7
TH
1942/**
1943 * Validate a forumula.
1944 * @param string $formula the formula to validate.
1945 * @return string|boolean false if there are no problems. Otherwise a string error message.
1946 */
7518b645 1947function qtype_calculated_find_formula_errors($formula) {
f184c65c 1948 // Validates the formula submitted from the question edit page.
3d9645ae 1949 // Returns false if everything is alright
1950 // otherwise it constructs an error message.
1951 // Strip away dataset names.
6dbcacee 1952 while (preg_match('~\\{[[:alpha:]][^>} <{"\']*\\}~', $formula, $regs)) {
516cf3eb 1953 $formula = str_replace($regs[0], '1', $formula);
1954 }
1955
3d9645ae 1956 // Strip away empty space and lowercase it.
516cf3eb 1957 $formula = strtolower(str_replace(' ', '', $formula));
1958
86e85775 1959 $safeoperatorchar = '-+/*%>:^\~<?=&|!'; /* */
516cf3eb 1960 $operatorornumber = "[$safeoperatorchar.0-9eE]";
1961
f184c65c
TH
1962 while (preg_match("~(^|[$safeoperatorchar,(])([a-z0-9_]*)" .
1963 "\\(($operatorornumber+(,$operatorornumber+((,$operatorornumber+)+)?)?)?\\)~",
fe6ce234 1964 $formula, $regs)) {
516cf3eb 1965 switch ($regs[2]) {
3d9645ae 1966 // Simple parenthesis.
f184c65c
TH
1967 case '':
1968 if ((isset($regs[4]) && $regs[4]) || strlen($regs[3]) == 0) {
1969 return get_string('illegalformulasyntax', 'qtype_calculated', $regs[0]);
1970 }
1971 break;
516cf3eb 1972
3d9645ae 1973 // Zero argument functions.
f184c65c 1974 case 'pi':
44f726a7 1975 if (array_key_exists(3, $regs)) {
f184c65c
TH
1976 return get_string('functiontakesnoargs', 'qtype_calculated', $regs[2]);
1977 }
1978 break;
516cf3eb 1979
3d9645ae 1980 // Single argument functions (the most common case).
f184c65c
TH
1981 case 'abs': case 'acos': case 'acosh': case 'asin': case 'asinh':
1982 case 'atan': case 'atanh': case 'bindec': case 'ceil': case 'cos':
1983 case 'cosh': case 'decbin': case 'decoct': case 'deg2rad':
1984 case 'exp': case 'expm1': case 'floor': case 'is_finite':
1985 case 'is_infinite': case 'is_nan': case 'log10': case 'log1p':
1986 case 'octdec': case 'rad2deg': case 'sin': case 'sinh': case 'sqrt':
1987 case 'tan': case 'tanh':
1988 if (!empty($regs[4]) || empty($regs[3])) {
1989 return get_string('functiontakesonearg', 'qtype_calculated', $regs[2]);
1990 }
1991 break;
516cf3eb 1992
3d9645ae 1993 // Functions that take one or two arguments.
f184c65c
TH
1994 case 'log': case 'round':
1995 if (!empty($regs[5]) || empty($regs[3])) {
1996 return get_string('functiontakesoneortwoargs', 'qtype_calculated', $regs[2]);
1997 }
1998 break;
516cf3eb 1999
3d9645ae 2000 // Functions that must have two arguments.
f184c65c
TH
2001 case 'atan2': case 'fmod': case 'pow':
2002 if (!empty($regs[5]) || empty($regs[4])) {
2003 return get_string('functiontakestwoargs', 'qtype_calculated', $regs[2]);
2004 }
2005 break;
516cf3eb 2006
3d9645ae 2007 // Functions that take two or more arguments.
f184c65c
TH
2008 case 'min': case 'max':
2009 if (empty($regs[4])) {
2010 return get_string('functiontakesatleasttwo', 'qtype_calculated', $regs[2]);
2011 }
2012 break;
516cf3eb 2013
f184c65c
TH
2014 default:
2015 return get_string('unsupportedformulafunction', 'qtype_calculated', $regs[2]);
516cf3eb 2016 }
2017
3d9645ae 2018 // Exchange the function call with '1' and then check for
516cf3eb 2019 // another function call...
2020 if ($regs[1]) {
3d9645ae 2021 // The function call is proceeded by an operator.
516cf3eb 2022 $formula = str_replace($regs[0], $regs[1] . '1', $formula);
2023 } else {
3d9645ae 2024 // The function call starts the formula.
6dbcacee 2025 $formula = preg_replace("~^$regs[2]\\([^)]*\\)~", '1', $formula);
516cf3eb 2026 }
2027 }
2028
6dbcacee 2029 if (preg_match("~[^$safeoperatorchar.0-9eE]+~", $formula, $regs)) {
5e8a85aa 2030 return get_string('illegalformulasyntax', 'qtype_calculated', $regs[0]);
516cf3eb 2031 } else {
3d9645ae 2032 // Formula just might be valid.
516cf3eb 2033 return false;
2034 }
2035}
155bc754
TH
2036
2037/**
2038 * Validate all the forumulas in a bit of text.
2039 * @param string $text the text in which to validate the formulas.
2040 * @return string|boolean false if there are no problems. Otherwise a string error message.
2041 */
2042function qtype_calculated_find_formula_errors_in_text($text) {
2043 preg_match_all(qtype_calculated::FORMULAS_IN_TEXT_REGEX, $text, $matches);
2044
2045 $errors = array();
2046 foreach ($matches[1] as $match) {
2047 $error = qtype_calculated_find_formula_errors($match);
2048 if ($error) {
2049 $errors[] = $error;
2050 }
2051 }
2052
2053 if ($errors) {
2054 return implode(' ', $errors);
2055 }
2056
2057 return false;
2058}