2 // This file is part of Moodle - http://moodle.org/
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.
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.
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/>.
18 * Question type class for the calculated question type.
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
27 defined('MOODLE_INTERNAL') || die();
29 require_once($CFG->dirroot . '/question/type/questionbase.php');
30 require_once($CFG->dirroot . '/question/type/numerical/question.php');
34 * The calculated question type.
36 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
37 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
39 class qtype_calculated extends question_type {
40 const MAX_DATASET_ITEMS = 100;
42 public $wizardpagesnumber = 3;
44 public function get_question_options($question) {
45 // First get the datasets and default options.
46 // The code is used for calculated, calculatedsimple and calculatedmulti qtypes.
47 global $CFG, $DB, $OUTPUT;
48 if (!$question->options = $DB->get_record('question_calculated_options',
49 array('question' => $question->id))) {
50 $question->options = new stdClass();
51 $question->options->synchronize = 0;
52 $question->options->single = 0;
53 $question->options->answernumbering = 'abc';
54 $question->options->shuffleanswers = 0;
55 $question->options->correctfeedback = '';
56 $question->options->partiallycorrectfeedback = '';
57 $question->options->incorrectfeedback = '';
58 $question->options->correctfeedbackformat = 0;
59 $question->options->partiallycorrectfeedbackformat = 0;
60 $question->options->incorrectfeedbackformat = 0;
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
69 ORDER BY a.id ASC", array($question->id))) {
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);
78 $question->hints = $DB->get_records('question_hints',
79 array('questionid' => $question->id), 'id ASC');
81 if (isset($question->export_process)&&$question->export_process) {
82 $question->options->datasets = $this->get_datasets_for_export($question);
87 public function get_datasets_for_export($question) {
89 $datasetdefs = array();
90 if (!empty($question->id)) {
92 FROM {question_datasets} d, {question_dataset_definitions} i
93 WHERE d.question = ? AND d.datasetdefinition = i.id";
94 if ($records = $DB->get_records_sql($sql, array($question->id))) {
95 foreach ($records as $r) {
97 if ($def->category == '0') {
98 $def->status = 'private';
100 $def->status = 'shared';
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) {
109 // Get the datasetitems.
110 $def->items = array();
111 if ($items = $this->get_database_dataset_items($def->id)) {
113 foreach ($items as $ii) {
115 $def->items[$n] = new stdClass();
116 $def->items[$n]->itemnumber = $ii->itemnumber;
117 $def->items[$n]->value = $ii->value;
119 $def->number_of_items = $n;
122 $datasetdefs["1-$r->category-$r->name"] = $def;
129 public function save_question_options($question) {
131 // The code is used for calculated, calculatedsimple and calculatedmulti qtypes.
132 $context = $question->context;
133 if (isset($question->answer) && !isset($question->answers)) {
134 $question->answers = $question->answer;
136 // Calculated options.
138 $options = $DB->get_record('question_calculated_options',
139 array('question' => $question->id));
142 $options = new stdClass();
143 $options->question = $question->id;
145 // As used only by calculated.
146 if (isset($question->synchronize)) {
147 $options->synchronize = $question->synchronize;
149 $options->synchronize = 0;
151 $options->single = 0;
152 $options->answernumbering = $question->answernumbering;
153 $options->shuffleanswers = $question->shuffleanswers;
155 foreach (array('correctfeedback', 'partiallycorrectfeedback',
156 'incorrectfeedback') as $feedbackname) {
157 $options->$feedbackname = '';
158 $feedbackformat = $feedbackname . 'format';
159 $options->$feedbackformat = 0;
163 $DB->update_record('question_calculated_options', $options);
165 $DB->insert_record('question_calculated_options', $options);
168 // Get old versions of the objects.
169 $oldanswers = $DB->get_records('question_answers',
170 array('question' => $question->id), 'id ASC');
172 $oldoptions = $DB->get_records('question_calculated',
173 array('question' => $question->id), 'answer ASC');
176 $virtualqtype = $this->get_virtual_qtype();
178 $result = $virtualqtype->save_units($question);
179 if (isset($result->error)) {
182 $units = $result->units;
185 // Insert all the new answers.
186 if (isset($question->answer) && !isset($question->answers)) {
187 $question->answers = $question->answer;
189 foreach ($question->answers as $key => $answerdata) {
190 if (is_array($answerdata)) {
191 $answerdata = $answerdata['text'];
193 if (trim($answerdata) == '') {
197 // Update an existing answer if possible.
198 $answer = array_shift($oldanswers);
200 $answer = new stdClass();
201 $answer->question = $question->id;
202 $answer->answer = '';
203 $answer->feedback = '';
204 $answer->id = $DB->insert_record('question_answers', $answer);
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'];
213 $DB->update_record("question_answers", $answer);
215 // Set up the options object.
216 if (!$options = array_shift($oldoptions)) {
217 $options = new stdClass();
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]);
227 if (isset($options->id)) {
228 // Reusing existing record.
229 $DB->update_record('question_calculated', $options);
232 $DB->insert_record('question_calculated', $options);
236 // Delete old answer records.
237 if (!empty($oldanswers)) {
238 foreach ($oldanswers as $oa) {
239 $DB->delete_records('question_answers', array('id' => $oa->id));
243 // Delete old answer records.
244 if (!empty($oldoptions)) {
245 foreach ($oldoptions as $oo) {
246 $DB->delete_records('question_calculated', array('id' => $oo->id));
250 $result = $virtualqtype->save_unit_options($question);
251 if (isset($result->error)) {
255 $this->save_hints($question);
257 if (isset($question->import_process)&&$question->import_process) {
258 $this->import_datasets($question);
260 // Report any problems.
261 if (!empty($result->notice)) {
267 public function import_datasets($question) {
269 $n = count($question->dataset);
270 foreach ($question->dataset as $dataset) {
271 // Name, type, option.
272 $datasetdef = new stdClass();
273 $datasetdef->name = $dataset->name;
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') {
279 $datasetdef->category = 0;
281 } else if ($dataset->status == 'shared') {
282 if ($sharedatasetdefs = $DB->get_records_select(
283 'question_dataset_definitions',
287 ORDER BY id DESC ", array($dataset->name, $question->category)
288 )) { // So there is at least one.
289 $sharedatasetdef = array_shift($sharedatasetdefs);
290 if ($sharedatasetdef->options == $datasetdef->options) {// Identical so use it.
292 $datasetdef = $sharedatasetdef;
293 } else { // Different so create a private one.
294 $datasetdef->category = 0;
297 } else { // No so create one.
298 $datasetdef->category = $question->category;
302 if ($todo == 'create') {
303 $datasetdef->id = $DB->insert_record('question_dataset_definitions', $datasetdef);
305 // Create relation to the dataset.
306 $questiondataset = new stdClass();
307 $questiondataset->question = $question->id;
308 $questiondataset->datasetdefinition = $datasetdef->id;
309 $DB->insert_record('question_datasets', $questiondataset);
310 if ($todo == 'create') {
312 foreach ($dataset->datasetitem as $dataitem) {
313 $datasetitem = new stdClass();
314 $datasetitem->definition = $datasetdef->id;
315 $datasetitem->itemnumber = $dataitem->itemnumber;
316 $datasetitem->value = $dataitem->value;
317 $DB->insert_record('question_dataset_items', $datasetitem);
323 protected function initialise_question_instance(question_definition $question, $questiondata) {
324 parent::initialise_question_instance($question, $questiondata);
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;
330 $question->answers[$a->id]->correctanswerlength = $a->correctanswerlength;
331 $question->answers[$a->id]->correctanswerformat = $a->correctanswerformat;
334 $question->synchronised = $questiondata->options->synchronize;
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);
343 $question->datasetloader = new qtype_calculated_dataset_loader($questiondata->id);
346 public function validate_form($form) {
347 switch($form->wizardpage) {
349 $calculatedmessages = array();
350 if (empty($form->name)) {
351 $calculatedmessages[] = get_string('missingname', 'qtype_calculated');
353 if (empty($form->questiontext)) {
354 $calculatedmessages[] = get_string('missingquestiontext', 'qtype_calculated');
357 foreach ($form->answers as $key => $answer) {
358 if ('' === trim($answer)) {
359 $calculatedmessages[] = get_string(
360 'missingformula', 'qtype_calculated');
362 if ($formulaerrors = qtype_calculated_find_formula_errors($answer)) {
363 $calculatedmessages[] = $formulaerrors;
365 if (! isset($form->tolerance[$key])) {
366 $form->tolerance[$key] = 0.0;
368 if (! is_numeric($form->tolerance[$key])) {
369 $calculatedmessages[] = get_string('xmustbenumeric', 'qtype_numerical',
370 get_string('tolerance', 'qtype_calculated'));
374 if (!empty($calculatedmessages)) {
375 $errorstring = "The following errors were found:<br />";
376 foreach ($calculatedmessages as $msg) {
377 $errorstring .= $msg . '<br />';
379 print_error($errorstring);
384 return parent::validate_form($form);
389 public function finished_edit_wizard($form) {
390 return isset($form->savechanges);
392 public function wizardpagesnumber() {
395 // This gets called by editquestion.php after the standard question is saved.
396 public function print_next_wizard_page($question, $form, $course) {
397 global $CFG, $SESSION, $COURSE;
399 // Catch invalid navigation & reloads.
400 if (empty($question->id) && empty($SESSION->calculated)) {
401 redirect('edit.php?courseid='.$COURSE->id, 'The page you are loading has expired.', 3);
404 // See where we're coming from.
405 switch($form->wizardpage) {
407 require("$CFG->dirroot/question/type/calculated/datasetdefinitions.php");
409 case 'datasetdefinitions':
411 require("$CFG->dirroot/question/type/calculated/datasetitems.php");
414 print_error('invalidwizardpage', 'question');
419 // This gets called by question2.php after the standard question is saved.
420 public function &next_wizard_form($submiturl, $question, $wizardnow) {
421 global $CFG, $SESSION, $COURSE;
423 // Catch invalid navigation & reloads.
424 if (empty($question->id) && empty($SESSION->calculated)) {
425 redirect('edit.php?courseid=' . $COURSE->id,
426 'The page you are loading has expired. Cannot get next wizard form.', 3);
428 if (empty($question->id)) {
429 $question = $SESSION->calculated->questionform;
432 // See where we're coming from.
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);
440 require("$CFG->dirroot/question/type/calculated/datasetitems_form.php");
441 $regenerate = optional_param('forceregeneration', false, PARAM_BOOL);
442 $mform = new question_dataset_dependent_items_form(
443 "$submiturl?wizardnow=datasetitems", $question, $regenerate);
446 print_error('invalidwizardpage', 'question');
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.
457 * @param question_edit_form $mform a child of question_edit_form
458 * @param object $question
459 * @param string $wizardnow is '' for first page.
461 public function display_question_editing_page($mform, $question, $wizardnow) {
463 switch ($wizardnow) {
465 // On the first page, the default display is fine.
466 parent::display_question_editing_page($mform, $question, $wizardnow);
469 case 'datasetdefinitions':
470 echo $OUTPUT->heading_with_help(
471 get_string('choosedatasetproperties', 'qtype_calculated'),
472 'questiondatasets', 'qtype_calculated');
476 echo $OUTPUT->heading_with_help(get_string('editdatasets', 'qtype_calculated'),
477 'questiondatasets', 'qtype_calculated');
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)
488 * when creating a new calculated question or
489 * when editing an already existing calculated question
490 * or by function save_as_new_dataset_definitions($form, $initialid)
491 * when saving as new an already existing calculated question.
493 * @param object $form
494 * @param int $questionfromid default = '0'
496 public function preparedatasets($form , $questionfromid = '0') {
497 // The dataset names present in the edit_question_form and edit_calculated_form
499 $possibledatasets = $this->find_dataset_names($form->questiontext);
500 $mandatorydatasets = array();
501 foreach ($form->answers as $answer) {
502 $mandatorydatasets += $this->find_dataset_names($answer);
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.
507 if ($questionfromid != '0') {
508 $form->id = $questionfromid;
512 // Always prepare the mandatorydatasets present in the answers.
513 // The $options are not used here.
514 foreach ($mandatorydatasets as $datasetname) {
515 if (!isset($datasets[$datasetname])) {
516 list($options, $selected) =
517 $this->dataset_options($form, $datasetname);
518 $datasets[$datasetname] = '';
519 $form->dataset[$key] = $selected;
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.
526 if ($questionfromid != '0') {
528 foreach ($possibledatasets as $datasetname) {
529 if (!isset($datasets[$datasetname])) {
530 list($options, $selected) =
531 $this->dataset_options($form, $datasetname, false);
532 $datasets[$datasetname] = '';
533 $form->dataset[$key] = $selected;
540 public function addnamecategory(&$question) {
542 $categorydatasetdefs = $DB->get_records_sql(
544 FROM {question_datasets} b, {question_dataset_definitions} a
545 WHERE a.id = b.datasetdefinition
549 ORDER BY a.name ", array($question->id));
550 $questionname = $question->name;
552 if (preg_match('~#\{([^[:space:]]*)#~', $questionname , $regs)) {
553 $questionname = str_replace($regs[0], '', $questionname);
556 if (!empty($categorydatasetdefs)) {
557 // There is at least one with the same name.
558 $questionname = '#' . $questionname;
559 foreach ($categorydatasetdefs as $def) {
560 if (strlen($def->name) + strlen($questionname) < 250) {
561 $questionname = '{' . $def->name . '}' . $questionname;
564 $questionname = '#' . $questionname;
566 $DB->set_field('question', 'name', $questionname, array('id' => $question->id));
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
583 * @param object question
584 * @param object $form
586 * @param PARAM_ALPHA $wizardnow should be added as we are coming from question2.php
588 public function save_question($question, $form) {
590 if ($this->wizardpagesnumber() == 1 || $question->qtype == 'calculatedsimple') {
591 $question = parent::save_question($question, $form);
595 $wizardnow = optional_param('wizardnow', '', PARAM_ALPHA);
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.
605 // See where we're coming from.
608 case 'question': // Coming from the first page, creating the second.
609 if (empty($form->id)) { // or a new question $form->id is empty.
610 $question = parent::save_question($question, $form);
611 // Prepare the datasets using default $questionfromid.
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);
618 } else if (!empty($form->makecopy)) {
619 $questionfromid = $form->id;
620 $question = parent::save_question($question, $form);
621 // Prepare the datasets.
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);
629 // Editing a question.
630 $question = parent::save_question($question, $form);
631 // Prepare the datasets.
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);
640 case 'datasetdefinitions':
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.
645 if (isset($form->synchronize)) {
646 $optionssynchronize = $form->synchronize;
648 $optionssynchronize = 0;
650 $DB->set_field('question_calculated_options', 'synchronize', $optionssynchronize,
651 array('question' => $question->id));
652 if (isset($form->synchronize) && $form->synchronize == 2) {
653 $this->addnamecategory($question);
656 $this->save_dataset_definitions($form);
659 $this->save_dataset_items($question, $form);
660 $this->save_question_calculated($question, $form);
663 print_error('invalidwizardpage', 'question');
669 public function delete_question($questionid, $contextid) {
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));
675 if ($datasets = $DB->get_records('question_datasets', array('question' => $questionid))) {
676 foreach ($datasets as $dataset) {
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));
687 $DB->delete_records('question_datasets', array('question' => $questionid));
689 parent::delete_question($questionid, $contextid);
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);
701 public function supports_dataset_item_generation() {
702 // Calculated support generation of randomly distributed number data.
706 public function custom_generator_tools_part($mform, $idx, $j) {
708 $minmaxgrp = array();
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);
715 $mform->setType("calcmin[$idx]", PARAM_FLOAT);
716 $mform->setType("calcmax[$idx]", PARAM_FLOAT);
718 $precisionoptions = range(0, 10);
719 $mform->addElement('select', "calclength[$idx]",
720 get_string('calclength', 'qtype_calculated'), $precisionoptions);
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);
728 public function custom_generator_set_data($datasetdefs, $formdata) {
730 foreach ($datasetdefs as $datasetdef) {
731 if (preg_match('~^(uniform|loguniform):([^:]*):([^:]*):([0-9]*)$~',
732 $datasetdef->options, $regs)) {
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];
744 public function custom_generator_tools($datasetdef) {
746 if (preg_match('~^(uniform|loguniform):([^:]*):([^:]*):([0-9]*)$~',
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'
752 : 'significantfigures'), 'qtype_calculated', $i);
754 $menu1 = html_writer::label(get_string('lengthoption', 'qtype_calculated'),
755 'menucalclength', false, array('class' => 'accesshide'));
756 $menu1 .= html_writer::select($lengthoptions, 'calclength[]', $regs[4], null);
758 $options = array('uniform' => get_string('uniformbit', 'qtype_calculated'),
759 'loguniform' => get_string('loguniformbit', 'qtype_calculated'));
760 $menu2 = html_writer::label(get_string('distributionoption', 'qtype_calculated'),
761 'menucalcdistribution', false, array('class' => 'accesshide'));
762 $menu2 .= html_writer::select($options, 'calcdistribution[]', $regs[1], null);
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]\"/> & <input name=\"calcmax[]\" "
768 . ' type="text" size="3" value="' . $regs[3] .'"/> '
777 public function update_dataset_options($datasetdefs, $form) {
779 // Do we have information about new options ?
780 if (empty($form->definition) || empty($form->calcmin)
781 ||empty($form->calcmax) || empty($form->calclength)
782 || empty($form->calcdistribution)) {
786 // Looks like we just could have some new information here.
787 $uniquedefs = array_values(array_unique($form->definition));
788 foreach ($uniquedefs as $key => $defid) {
789 if (isset($datasetdefs[$defid])
790 && is_numeric($form->calcmin[$key+1])
791 && is_numeric($form->calcmax[$key+1])
792 && is_numeric($form->calclength[$key+1])) {
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];
802 echo $OUTPUT->notification(
803 "Unexpected distribution ".$form->calcdistribution[$key+1]);
809 // Look for empty options, on which we set default values.
810 foreach ($datasetdefs as $defid => $def) {
811 if (empty($def->options)) {
812 $datasetdefs[$defid]->options = 'uniform:1.0:10.0:1';
818 public function save_question_calculated($question, $fromform) {
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]);
827 $DB->update_record('question_calculated', $options);
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.
839 public function get_database_dataset_items($definition) {
841 $databasedataitems = $DB->get_records_sql(// Use number as key!!
842 " SELECT id , itemnumber, definition, value
843 FROM {question_dataset_items}
844 WHERE definition = $definition order by id DESC ", array($definition));
845 $dataitems = Array();
846 foreach ($databasedataitems as $id => $dataitem) {
847 if (!isset($dataitems[$dataitem->itemnumber])) {
848 $dataitems[$dataitem->itemnumber] = $dataitem;
855 public function save_dataset_items($question, $fromform) {
857 $synchronize = false;
858 if (isset($fromform->nextpageparam['forceregeneration'])) {
859 $regenerate = $fromform->nextpageparam['forceregeneration'];
863 if (empty($question->options)) {
864 $this->get_question_options($question);
866 if (!empty($question->options->synchronize)) {
870 // Get the old datasets for this question.
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);
876 foreach ($datasetdefs as $defid => $datasetdef) {
877 if (isset($datasetdef->id)
878 && $datasetdef->options != $olddatasetdefs[$defid]->options) {
879 // Save the new value for options.
880 $DB->update_record('question_dataset_definitions', $datasetdef);
884 if ($maxnumber == -1 || $datasetdef->itemcount < $maxnumber) {
885 $maxnumber = $datasetdef->itemcount;
888 // Handle adding and removing of dataset items.
890 if ($maxnumber > self::MAX_DATASET_ITEMS) {
891 $maxnumber = self::MAX_DATASET_ITEMS;
894 ksort($fromform->definition);
895 foreach ($fromform->definition as $key => $defid) {
896 // If the delete button has not been pressed then skip the datasetitems
897 // in the 'add item' part of the form.
898 if ($i > count($datasetdefs)*$maxnumber) {
901 $addeditem = new stdClass();
902 $addeditem->definition = $datasetdefs[$defid]->id;
903 $addeditem->value = $fromform->number[$i];
904 $addeditem->itemnumber = ceil($i / count($datasetdefs));
906 if ($fromform->itemid[$i]) {
907 // Reuse any previously used record.
908 $addeditem->id = $fromform->itemid[$i];
909 $DB->update_record('question_dataset_items', $addeditem);
911 $DB->insert_record('question_dataset_items', $addeditem);
916 if (isset($addeditem->itemnumber) && $maxnumber < $addeditem->itemnumber
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;
922 // Save the new value for options.
923 $DB->update_record('question_dataset_definitions', $newdef);
927 // Adding supplementary items.
929 if (isset($fromform->addbutton) && $fromform->selectadd > 0 &&
930 $maxnumber < self::MAX_DATASET_ITEMS) {
931 $numbertoadd = $fromform->selectadd;
932 if (self::MAX_DATASET_ITEMS - $maxnumber < $numbertoadd) {
933 $numbertoadd = self::MAX_DATASET_ITEMS - $maxnumber;
935 // Add the other items.
936 // Generate a new dataset item (or reuse an old one).
937 foreach ($datasetdefs as $defid => $datasetdef) {
938 // In case that for category datasets some new items has been added,
939 // get actual values.
940 // Fix regenerate for this datadefs.
943 !empty ($fromform->nextpageparam["datasetregenerate[$datasetdef->name"])) {
945 } else if (!$synchronize &&
946 (($regenerate == 1 && $datasetdef->category == 0) ||$regenerate == 2)) {
949 if (isset($datasetdef->id)) {
950 $datasetdefs[$defid]->items =
951 $this->get_database_dataset_items($datasetdef->id);
953 for ($numberadded = $maxnumber+1; $numberadded <= $maxnumber + $numbertoadd; $numberadded++) {
954 if (isset($datasetdefs[$defid]->items[$numberadded])) {
955 // In case of regenerate it modifies the already existing record.
956 if ($defregenerate) {
957 $datasetitem = new stdClass();
958 $datasetitem->id = $datasetdefs[$defid]->items[$numberadded]->id;
959 $datasetitem->definition = $datasetdef->id;
960 $datasetitem->itemnumber = $numberadded;
961 $datasetitem->value =
962 $this->generate_dataset_item($datasetdef->options);
963 $DB->update_record('question_dataset_items', $datasetitem);
965 // If not regenerate do nothing as there is already a record.
967 $datasetitem = new stdClass();
968 $datasetitem->definition = $datasetdef->id;
969 $datasetitem->itemnumber = $numberadded;
970 if ($this->supports_dataset_item_generation()) {
971 $datasetitem->value =
972 $this->generate_dataset_item($datasetdef->options);
974 $datasetitem->value = '';
976 $DB->insert_record('question_dataset_items', $datasetitem);
978 }// For number added.
979 }// Datasetsdefs end.
980 $maxnumber += $numbertoadd;
981 foreach ($datasetdefs as $key => $newdef) {
982 if (isset($newdef->id) && $newdef->itemcount <= $maxnumber) {
983 $newdef->itemcount = $maxnumber;
984 // Save the new value for options.
985 $DB->update_record('question_dataset_definitions', $newdef);
990 if (isset($fromform->deletebutton)) {
991 if (isset($fromform->selectdelete)) {
992 $newmaxnumber = $maxnumber-$fromform->selectdelete;
994 $newmaxnumber = $maxnumber-1;
996 if ($newmaxnumber < 0) {
999 foreach ($datasetdefs as $datasetdef) {
1000 if ($datasetdef->itemcount == $maxnumber) {
1001 $datasetdef->itemcount= $newmaxnumber;
1002 $DB->update_record('question_dataset_definitions', $datasetdef);
1007 public function generate_dataset_item($options) {
1008 if (!preg_match('~^(uniform|loguniform):([^:]*):([^:]*):([0-9]*)$~',
1010 // Unknown options...
1013 if ($regs[1] == 'uniform') {
1014 $nbr = $regs[2] + ($regs[3]-$regs[2])*mt_rand()/mt_getrandmax();
1015 return sprintf("%.".$regs[4].'f', $nbr);
1017 } else if ($regs[1] == 'loguniform') {
1018 $log0 = log(abs($regs[2])); // It would have worked the other way to.
1019 $nbr = exp($log0 + (log(abs($regs[3])) - $log0)*mt_rand()/mt_getrandmax());
1020 return sprintf("%.".$regs[4].'f', $nbr);
1023 print_error('disterror', 'question', '', $regs[1]);
1028 public function comment_header($question) {
1032 $answers = $question->options->answers;
1034 foreach ($answers as $key => $answer) {
1035 $ans = shorten_text($answer->answer, 17, true);
1036 $strheader .= $delimiter.$ans;
1037 $delimiter = '<br/><br/><br/>';
1042 public function comment_on_datasetitems($qtypeobj, $questionid, $questiontext,
1043 $answers, $data, $number) {
1045 $comment = new stdClass();
1046 $comment->stranswers = array();
1047 $comment->outsidelimit = false;
1048 $comment->answers = array();
1049 // Find a default unit.
1050 if (!empty($questionid) && $unit = $DB->get_record('question_numerical_units',
1051 array('question' => $questionid, 'multiplier' => 1.0))) {
1052 $unit = $unit->unit;
1057 $answers = fullclone($answers);
1060 $virtualqtype = $qtypeobj->get_virtual_qtype();
1061 foreach ($answers as $key => $answer) {
1062 $formula = $this->substitute_variables($answer->answer, $data);
1063 $formattedanswer = qtype_calculated_calculate_answer(
1064 $answer->answer, $data, $answer->tolerance,
1065 $answer->tolerancetype, $answer->correctanswerlength,
1066 $answer->correctanswerformat, $unit);
1067 if ($formula === '*') {
1069 $formattedanswer->answer = $answer->answer;
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);
1076 if ($answer->min === '') {
1077 // This should mean that something is wrong.
1078 $comment->stranswers[$key] = " $formattedanswer->answer".'<br/><br/>';
1079 } else if ($formula === '*') {
1080 $comment->stranswers[$key] = $formula . ' = ' .
1081 get_string('anyvalue', 'qtype_calculated') . '<br/><br/><br/>';
1083 $formula = shorten_text($formula, 57, true);
1084 $comment->stranswers[$key] = $formula . ' = ' . $formattedanswer->answer . '<br/>';
1085 $correcttrue = new stdClass();
1086 $correcttrue->correct = $formattedanswer->answer;
1087 $correcttrue->true = '';
1088 if ($formattedanswer->answer < $answer->min ||
1089 $formattedanswer->answer > $answer->max) {
1090 $comment->outsidelimit = true;
1091 $comment->answers[$key] = $key;
1092 $comment->stranswers[$key] .=
1093 get_string('trueansweroutsidelimits', 'qtype_calculated', $correcttrue);
1095 $comment->stranswers[$key] .=
1096 get_string('trueanswerinsidelimits', 'qtype_calculated', $correcttrue);
1098 $comment->stranswers[$key] .= '<br/>';
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;
1105 return fullclone($comment);
1108 public function tolerance_types() {
1110 '1' => get_string('relative', 'qtype_numerical'),
1111 '2' => get_string('nominal', 'qtype_numerical'),
1112 '3' => get_string('geometric', 'qtype_numerical')
1116 public function dataset_options($form, $name, $mandatory = true,
1117 $renameabledatasets = false) {
1118 // Takes datasets from the parent implementation but
1119 // filters options that are currently not accepted by calculated.
1120 // It also determines a default selection.
1121 // Param $renameabledatasets not implemented anywhere.
1123 list($options, $selected) = $this->dataset_options_from_database(
1124 $form, $name, '', 'qtype_calculated');
1126 foreach ($options as $key => $whatever) {
1127 if (!preg_match('~^1-~', $key) && $key != '0') {
1128 unset($options[$key]);
1133 $selected = "1-0-$name"; // Default.
1135 $selected = '0'; // Default.
1138 return array($options, $selected);
1141 public function construct_dataset_menus($form, $mandatorydatasets,
1142 $optionaldatasets) {
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...
1150 $datasetmenus[$datasetname] = html_writer::select(
1151 $options, 'dataset[]', $selected, null);
1154 foreach ($optionaldatasets as $datasetname) {
1155 if (!isset($datasetmenus[$datasetname])) {
1156 list($options, $selected) =
1157 $this->dataset_options($form, $datasetname);
1158 $datasetmenus[$datasetname] = html_writer::select(
1159 $options, 'dataset[]', $selected, null);
1162 return $datasetmenus;
1165 public function substitute_variables($str, $dataset) {
1167 // Testing for wrong numerical values.
1168 // All calculations used this function so testing here should be OK.
1170 foreach ($dataset as $name => $value) {
1172 if (! is_numeric($val)) {
1173 $a = new stdClass();
1174 $a->name = '{'.$name.'}';
1176 echo $OUTPUT->notification(get_string('notvalidnumber', 'qtype_calculated', $a));
1179 if ($val <= 0) { // MDL-36025 Use parentheses for "-0" .
1180 $str = str_replace('{'.$name.'}', '('.$val.')', $str);
1182 $str = str_replace('{'.$name.'}', $val, $str);
1188 public function evaluate_equations($str, $dataset) {
1189 $formula = $this->substitute_variables($str, $dataset);
1190 if ($error = qtype_calculated_find_formula_errors($formula)) {
1196 public function substitute_variables_and_eval($str, $dataset) {
1197 $formula = $this->substitute_variables($str, $dataset);
1198 if ($error = qtype_calculated_find_formula_errors($formula)) {
1201 // Calculate the correct answer.
1202 if (empty($formula)) {
1204 } else if ($formula === '*') {
1208 eval('$str = '.$formula.';');
1213 public function get_dataset_definitions($questionid, $newdatasets) {
1215 // Get the existing datasets for this question.
1216 $datasetdefs = array();
1217 if (!empty($questionid)) {
1220 FROM {question_datasets} d, {question_dataset_definitions} i
1221 WHERE d.question = ? AND d.datasetdefinition = i.id
1223 if ($records = $DB->get_records_sql($sql, array($questionid))) {
1224 foreach ($records as $r) {
1225 $datasetdefs["$r->type-$r->category-$r->name"] = $r;
1230 foreach ($newdatasets as $dataset) {
1232 continue; // The no dataset case...
1235 if (!isset($datasetdefs[$dataset])) {
1236 // Make new datasetdef.
1237 list($type, $category, $name) = explode('-', $dataset, 3);
1238 $datasetdef = new stdClass();
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);
1247 return $datasetdefs;
1250 public function save_dataset_definitions($form) {
1252 // Save synchronize.
1254 if (empty($form->dataset)) {
1255 $form->dataset = array();
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])) {
1265 // This dataset is not used any more, delete it.
1266 $DB->delete_records('question_datasets',
1267 array('question' => $form->id, 'datasetdefinition' => $datasetdef->id));
1268 if ($datasetdef->category == 0) {
1269 // Question local dataset.
1270 $DB->delete_records('question_dataset_definitions',
1271 array('id' => $datasetdef->id));
1272 $DB->delete_records('question_dataset_items',
1273 array('definition' => $datasetdef->id));
1276 // This has already been saved or just got deleted.
1277 unset($datasetdefinitions[$defid]);
1281 $datasetdef->id = $DB->insert_record('question_dataset_definitions', $datasetdef);
1283 if (0 != $datasetdef->category) {
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.
1288 if ($olderdatasetdefs = $DB->get_records_select('question_dataset_definitions',
1289 'type = ? AND name = ? AND category = ? AND id < ?
1291 array($datasetdef->type, $datasetdef->name,
1292 $datasetdef->category, $datasetdef->id))) {
1294 while ($olderdatasetdef = array_shift($olderdatasetdefs)) {
1295 $DB->delete_records('question_dataset_definitions',
1296 array('id' => $datasetdef->id));
1297 $datasetdef = $olderdatasetdef;
1302 // Create relation to this dataset.
1303 $questiondataset = new stdClass();
1304 $questiondataset->question = $form->id;
1305 $questiondataset->datasetdefinition = $datasetdef->id;
1306 $DB->insert_record('question_datasets', $questiondataset);
1307 unset($datasetdefinitions[$defid]);
1310 // Remove local obsolete datasets as well as relations
1311 // to datasets in other categories.
1312 if (!empty($datasetdefinitions)) {
1313 foreach ($datasetdefinitions as $def) {
1314 $DB->delete_records('question_datasets',
1315 array('question' => $form->id, 'datasetdefinition' => $def->id));
1317 if ($def->category == 0) { // Question local dataset.
1318 $DB->delete_records('question_dataset_definitions',
1319 array('id' => $def->id));
1320 $DB->delete_records('question_dataset_items',
1321 array('definition' => $def->id));
1326 /** This function create a copy of the datasets (definition and dataitems)
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()
1331 public function save_as_new_dataset_definitions($form, $initialid) {
1333 // Get the datasets from the intial question.
1334 $datasetdefinitions = $this->get_dataset_definitions($initialid, $form->dataset);
1335 // Param $tmpdatasets contains those of the new question.
1336 $tmpdatasets = array_flip($form->dataset);
1337 $defids = array_keys($datasetdefinitions);// New datasets.
1338 foreach ($defids as $defid) {
1339 $datasetdef = &$datasetdefinitions[$defid];
1340 if (isset($datasetdef->id)) {
1341 // This dataset exist in the initial question.
1342 if (!isset($tmpdatasets[$defid])) {
1343 // Do not exist in the new question so ignore.
1344 unset($datasetdefinitions[$defid]);
1347 // Create a copy but not for category one.
1348 if (0 == $datasetdef->category) {
1349 $olddatasetid = $datasetdef->id;
1350 $olditemcount = $datasetdef->itemcount;
1351 $datasetdef->itemcount = 0;
1352 $datasetdef->id = $DB->insert_record('question_dataset_definitions',
1354 // Copy the dataitems.
1355 $olditems = $this->get_database_dataset_items($olddatasetid);
1356 if (count($olditems) > 0) {
1358 foreach ($olditems as $item) {
1359 $item->definition = $datasetdef->id;
1360 $DB->insert_record('question_dataset_items', $item);
1363 // Update item count to olditemcount if
1364 // at least this number of items has been recover from the database.
1365 if ($olditemcount <= $itemcount) {
1366 $datasetdef->itemcount = $olditemcount;
1368 $datasetdef->itemcount = $itemcount;
1370 $DB->update_record('question_dataset_definitions', $datasetdef);
1371 } // End of copy the dataitems.
1372 }// End of copy the datasetdef.
1373 // Create relation to the new question with this
1374 // copy as new datasetdef from the initial question.
1375 $questiondataset = new stdClass();
1376 $questiondataset->question = $form->id;
1377 $questiondataset->datasetdefinition = $datasetdef->id;
1378 $DB->insert_record('question_datasets', $questiondataset);
1379 unset($datasetdefinitions[$defid]);
1381 }// End of datasetdefs from the initial question.
1382 // Really new one code similar to save_dataset_definitions().
1383 $datasetdef->id = $DB->insert_record('question_dataset_definitions', $datasetdef);
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
1390 // some possible realtime concurrence.
1391 if ($olderdatasetdefs = $DB->get_records_select('question_dataset_definitions',
1392 "type = ? AND name = ? AND category = ? AND id < ?
1394 array($datasetdef->type, $datasetdef->name,
1395 $datasetdef->category, $datasetdef->id))) {
1397 while ($olderdatasetdef = array_shift($olderdatasetdefs)) {
1398 $DB->delete_records('question_dataset_definitions',
1399 array('id' => $datasetdef->id));
1400 $datasetdef = $olderdatasetdef;
1405 // Create relation to this dataset.
1406 $questiondataset = new stdClass();
1407 $questiondataset->question = $form->id;
1408 $questiondataset->datasetdefinition = $datasetdef->id;
1409 $DB->insert_record('question_datasets', $questiondataset);
1410 unset($datasetdefinitions[$defid]);
1413 // Remove local obsolete datasets as well as relations
1414 // to datasets in other categories.
1415 if (!empty($datasetdefinitions)) {
1416 foreach ($datasetdefinitions as $def) {
1417 $DB->delete_records('question_datasets',
1418 array('question' => $form->id, 'datasetdefinition' => $def->id));
1420 if ($def->category == 0) { // Question local dataset.
1421 $DB->delete_records('question_dataset_definitions',
1422 array('id' => $def->id));
1423 $DB->delete_records('question_dataset_items',
1424 array('definition' => $def->id));
1430 // Dataset functionality.
1431 public function pick_question_dataset($question, $datasetitem) {
1432 // Select a dataset in the following format:
1433 // an array indexed by the variable names (d.name) pointing to the value
1434 // to be substituted.
1436 if (!$dataitems = $DB->get_records_sql(
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);
1452 foreach ($dataitems as $id => $dataitem) {
1453 if (!isset($dataset[$dataitem->name])) {
1454 $dataset[$dataitem->name] = $dataitem->value;
1460 public function dataset_options_from_database($form, $name, $prefix = '',
1461 $langfile = 'qtype_calculated') {
1463 $type = 1; // Only type = 1 (i.e. old 'LITERAL') has ever been used.
1464 // First options - it is not a dataset...
1465 $options['0'] = get_string($prefix.'nodataset', $langfile);
1466 // New question no local.
1467 if (!isset($form->id) || $form->id == 0) {
1468 $key = "$type-0-$name";
1469 $options[$key] = get_string($prefix."newlocal$type", $langfile);
1470 $currentdatasetdef = new stdClass();
1471 $currentdatasetdef->type = '0';
1473 // Construct question local options.
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) {
1479 $currentdatasetdef = new stdClass();
1480 $currentdatasetdef->type = '0';
1482 $key = "$type-0-$name";
1483 if ($currentdatasetdef->type == $type
1484 and $currentdatasetdef->category == 0) {
1485 $options[$key] = get_string($prefix."keptlocal$type", $langfile);
1487 $options[$key] = get_string($prefix."newlocal$type", $langfile);
1490 // Construct question category options.
1491 $categorydatasetdefs = $DB->get_records_sql(
1492 "SELECT b.question, a.*
1493 FROM {question_datasets} b,
1494 {question_dataset_definitions} a
1495 WHERE a.id = b.datasetdefinition
1498 AND a.name = ?", array($form->category, $name));
1500 $key = "$type-$form->category-$name";
1501 if (!empty($categorydatasetdefs)) {
1502 // There is at least one with the same name.
1503 if (isset($form->id) && isset($categorydatasetdefs[$form->id])) {
1504 // It is already used by this question.
1505 $options[$key] = get_string($prefix."keptcategory$type", $langfile);
1507 $options[$key] = get_string($prefix."existingcategory$type", $langfile);
1510 $options[$key] = get_string($prefix."newcategory$type", $langfile);
1513 return array($options, $currentdatasetdef->type
1514 ? "$currentdatasetdef->type-$currentdatasetdef->category-$name"
1518 public function find_dataset_names($text) {
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.
1521 $datasetnames = array();
1522 while (preg_match('~\\{([[:alpha:]][^>} <{"\']*)\\}~', $text, $regs)) {
1523 $datasetnames[$regs[1]] = $regs[1];
1524 $text = str_replace($regs[0], '', $text);
1526 return $datasetnames;
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
1534 public function get_dataset_definitions_category($form) {
1536 $datasetdefs = array();
1538 if (!empty($form->category)) {
1539 $sql = "SELECT i.*, d.*
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) {
1544 if (!isset ($datasetdefs["$r->name"])) {
1545 $datasetdefs["$r->name"] = $r->itemcount;
1550 return $datasetdefs;
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
1560 public function print_dataset_definitions_category($form) {
1562 $datasetdefs = array();
1564 $namestr = get_string('name');
1565 $rangeofvaluestr = get_string('minmax', 'qtype_calculated');
1566 $questionusingstr = get_string('usedinquestion', 'qtype_calculated');
1567 $itemscountstr = get_string('itemscount', 'qtype_calculated');
1569 if (!empty($form->category)) {
1570 list($category) = explode(',', $form->category);
1571 $sql = "SELECT i.*, d.*
1572 FROM {question_datasets} d,
1573 {question_dataset_definitions} i
1574 WHERE i.id = d.datasetdefinition
1575 AND i.category = ?";
1576 if ($records = $DB->get_records_sql($sql, array($category))) {
1577 foreach ($records as $r) {
1581 if (!isset ($datasetdefs["$r->type-$r->category-$r->name"])) {
1582 $datasetdefs["$r->type-$r->category-$r->name"] = $r;
1584 if ($questionb = $DB->get_records_sql($sql1, array($r->question))) {
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();
1588 $datasetdefs["$r->type-$r->category-$r->name"]->questions[
1589 $r->question]->name = $questionb[$r->question]->name;
1594 if (!empty ($datasetdefs)) {
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>
1606 foreach ($datasetdefs as $datasetdef) {
1607 list($distribution, $min, $max, $dec) = explode(':', $datasetdef->options, 4);
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 </td>
1612 <td align=\"left\">";
1613 foreach ($datasetdef->questions as $qu) {
1614 // Limit the name length displayed.
1615 if (!empty($qu->name)) {
1616 $qu->name = (strlen($qu->name) > $lnamemax) ?
1617 substr($qu->name, 0, $lnamemax).'...' : $qu->name;
1621 $text .= " $qu->name <br/>";
1623 $text .= "</td></tr>";
1625 $text .= "</table>";
1627 $text .= get_string('nosharedwildcard', 'qtype_calculated');
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
1640 public function print_dataset_definitions_category_shared($question, $datasetdefsq) {
1642 $datasetdefs = array();
1644 $namestr = get_string('name', 'quiz');
1645 $rangeofvaluestr = get_string('minmax', 'qtype_calculated');
1646 $questionusingstr = get_string('usedinquestion', 'qtype_calculated');
1647 $itemscountstr = get_string('itemscount', 'qtype_calculated');
1649 if (!empty($question->category)) {
1650 list($category) = explode(',', $question->category);
1651 $sql = "SELECT i.*, d.*
1652 FROM {question_datasets} d, {question_dataset_definitions} i
1653 WHERE i.id = d.datasetdefinition AND i.category = ?";
1654 if ($records = $DB->get_records_sql($sql, array($category))) {
1655 foreach ($records as $r) {
1656 $key = "$r->type-$r->category-$r->name";
1660 if (!isset($datasetdefs[$key])) {
1661 $datasetdefs[$key] = $r;
1663 if ($questionb = $DB->get_records_sql($sql1, array($r->question))) {
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;
1673 if (!empty ($datasetdefs)) {
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\"> $questionusingstr </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);
1688 $count = count($datasetdef->questions);
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>";
1695 foreach ($datasetdef->questions as $qu) {
1696 // Limit the name length displayed.
1697 if (!empty($qu->name)) {
1698 $qu->name = (strlen($qu->name) > $lnamemax) ?
1699 substr($qu->name, 0, $lnamemax).'...' : $qu->name;
1707 $text .= "<td align=\"left\" style=\"white-space:nowrap;\">$qu->name</td>";
1708 // TODO should not have quiz-specific code here.
1709 $nbofquiz = $DB->count_records('quiz_question_instances', array('questionid' => $qu->id));
1710 $nbofattempts = $DB->count_records_sql("
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) {
1717 $text .= "<td align=\"center\">$nbofquiz</td>";
1718 $text .= "<td align=\"center\">$nbofattempts";
1720 $text .= "<td align=\"center\">0</td>";
1721 $text .= "<td align=\"left\"><br/>";
1724 $text .= "</td></tr>";
1727 $text .= "</table>";
1729 $text .= get_string('nosharedwildcard', 'qtype_calculated');
1734 public function find_math_equations($text) {
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.
1737 $equations = array();
1738 while (preg_match('~\{=([^[:space:]}]*)}~', $text, $regs)) {
1739 $equations[] = $regs[1];
1740 $text = str_replace($regs[0], '', $text);
1745 public function get_virtual_qtype() {
1746 return question_bank::get_qtype('numerical');
1749 public function get_possible_responses($questiondata) {
1750 $responses = array();
1752 $virtualqtype = $this->get_virtual_qtype();
1753 $unit = $virtualqtype->get_default_numerical_unit($questiondata);
1755 $tolerancetypes = $this->tolerance_types();
1758 foreach ($questiondata->options->answers as $aid => $answer) {
1759 $responseclass = $answer->answer;
1761 if ($responseclass === '*') {
1764 $a = new stdClass();
1765 $a->answer = $virtualqtype->add_unit($questiondata, $responseclass, $unit);
1766 $a->tolerance = $answer->tolerance;
1767 $a->tolerancetype = $tolerancetypes[$answer->tolerancetype];
1769 $responseclass = get_string('answerwithtolerance', 'qtype_calculated', $a);
1772 $responses[$aid] = new question_possible_response($responseclass,
1777 $responses[0] = new question_possible_response(
1778 get_string('didnotmatchanyanswer', 'question'), 0);
1781 $responses[null] = question_possible_response::no_response();
1783 return array($questiondata->id => $responses);
1786 public function move_files($questionid, $oldcontextid, $newcontextid) {
1787 $fs = get_file_storage();
1789 parent::move_files($questionid, $oldcontextid, $newcontextid);
1790 $this->move_files_in_answers($questionid, $oldcontextid, $newcontextid);
1791 $this->move_files_in_hints($questionid, $oldcontextid, $newcontextid);
1794 protected function delete_files($questionid, $contextid) {
1795 $fs = get_file_storage();
1797 parent::delete_files($questionid, $contextid);
1798 $this->delete_files_in_answers($questionid, $contextid);
1799 $this->delete_files_in_hints($questionid, $contextid);
1804 function qtype_calculated_calculate_answer($formula, $individualdata,
1805 $tolerance, $tolerancetype, $answerlength, $answerformat = '1', $unit = '') {
1806 // The return value has these properties: .
1807 // ->answer the correct answer
1808 // ->min the lower bound for an acceptable response
1809 // ->max the upper bound for an accetpable response.
1810 $calculated = new stdClass();
1811 // Exchange formula variables with the correct values...
1812 $answer = question_bank::get_qtype('calculated')->substitute_variables_and_eval(
1813 $formula, $individualdata);
1814 if (!is_numeric($answer)) {
1815 // Something went wrong, so just return NaN.
1816 $calculated->answer = NAN;
1819 if ('1' == $answerformat) { // Answer is to have $answerlength decimals.
1821 $calculated->answer = sprintf('%.' . $answerlength . 'F', $answer);
1823 } else if ($answer) { // Significant figures does only apply if the result is non-zero.
1825 // Convert to positive answer...
1833 // Determine the format 0.[1-9][0-9]* for the answer...
1835 while ($answer < 1) {
1839 while ($answer >= 1) {
1843 // ... and have the answer rounded of to the correct length.
1844 $answer = round($answer, $answerlength);
1846 // If we rounded up to 1.0, place the answer back into 0.[1-9][0-9]* format.
1852 // Have the answer written on a suitable format:
1853 // either scientific or plain numeric.
1854 if (-2 > $p10 || 4 < $p10) {
1855 // Use scientific format.
1856 $exponent = 'e'.--$p10;
1858 if (1 == $answerlength) {
1859 $calculated->answer = $sign.$answer.$exponent;
1861 // Attach additional zeros at the end of $answer.
1862 $answer .= (1 == strlen($answer) ? '.' : '')
1863 . '00000000000000000000000000000000000000000x';
1864 $calculated->answer = $sign
1865 .substr($answer, 0, $answerlength +1).$exponent;
1868 // Stick to plain numeric format.
1869 $answer *= "1e$p10";
1870 if (0.1 <= $answer / "1e$answerlength") {
1871 $calculated->answer = $sign.$answer;
1873 // Could be an idea to add some zeros here.
1874 $answer .= (preg_match('~^[0-9]*$~', $answer) ? '.' : '')
1875 . '00000000000000000000000000000000000000000x';
1876 $oklen = $answerlength + ($p10 < 1 ? 2-$p10 : 1);
1877 $calculated->answer = $sign.substr($answer, 0, $oklen);
1882 $calculated->answer = 0.0;
1885 $calculated->answer = $calculated->answer . ' ' . $unit;
1888 // Return the result.
1893 function qtype_calculated_find_formula_errors($formula) {
1894 // Validates the formula submitted from the question edit page.
1895 // Returns false if everything is alright
1896 // otherwise it constructs an error message.
1897 // Strip away dataset names.
1898 while (preg_match('~\\{[[:alpha:]][^>} <{"\']*\\}~', $formula, $regs)) {
1899 $formula = str_replace($regs[0], '1', $formula);
1902 // Strip away empty space and lowercase it.
1903 $formula = strtolower(str_replace(' ', '', $formula));
1905 $safeoperatorchar = '-+/*%>:^\~<?=&|!'; /* */
1906 $operatorornumber = "[$safeoperatorchar.0-9eE]";
1908 while (preg_match("~(^|[$safeoperatorchar,(])([a-z0-9_]*)" .
1909 "\\(($operatorornumber+(,$operatorornumber+((,$operatorornumber+)+)?)?)?\\)~",
1912 // Simple parenthesis.
1914 if ((isset($regs[4]) && $regs[4]) || strlen($regs[3]) == 0) {
1915 return get_string('illegalformulasyntax', 'qtype_calculated', $regs[0]);
1919 // Zero argument functions.
1922 return get_string('functiontakesnoargs', 'qtype_calculated', $regs[2]);
1926 // Single argument functions (the most common case).
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]);
1939 // Functions that take one or two arguments.
1940 case 'log': case 'round':
1941 if (!empty($regs[5]) || empty($regs[3])) {
1942 return get_string('functiontakesoneortwoargs', 'qtype_calculated', $regs[2]);
1946 // Functions that must have two arguments.
1947 case 'atan2': case 'fmod': case 'pow':
1948 if (!empty($regs[5]) || empty($regs[4])) {
1949 return get_string('functiontakestwoargs', 'qtype_calculated', $regs[2]);
1953 // Functions that take two or more arguments.
1954 case 'min': case 'max':
1955 if (empty($regs[4])) {
1956 return get_string('functiontakesatleasttwo', 'qtype_calculated', $regs[2]);
1961 return get_string('unsupportedformulafunction', 'qtype_calculated', $regs[2]);
1964 // Exchange the function call with '1' and then check for
1965 // another function call...
1967 // The function call is proceeded by an operator.
1968 $formula = str_replace($regs[0], $regs[1] . '1', $formula);
1970 // The function call starts the formula.
1971 $formula = preg_replace("~^$regs[2]\\([^)]*\\)~", '1', $formula);
1975 if (preg_match("~[^$safeoperatorchar.0-9eE]+~", $formula, $regs)) {
1976 return get_string('illegalformulasyntax', 'qtype_calculated', $regs[0]);
1978 // Formula just might be valid.