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