[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/question/type/calculated/ -> questiontype.php (source)

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


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1