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