[ Index ]

PHP Cross Reference of moodle-2.8

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


Generated: Fri Nov 28 20:29:05 2014 Cross-referenced by PHPXref 0.7.1