Search moodle.org's
Developer Documentation


  • Bug fixes for general core bugs in 2.8.x ended 9 November 2015 (12 months).
  • Bug fixes for security issues in 2.8.x ended 9 May 2016 (18 months).
  • minimum PHP 5.4.4 (always use latest PHP 5.4.x or 5.5.x on Windows - http://windows.php.net/download/), PHP 7 is NOT supported
  • Differences Between: [Versions 28 and 29] [Versions 28 and 30] [Versions 28 and 31] [Versions 28 and 32] [Versions 28 and 33] [Versions 28 and 34] [Versions 28 and 35] [Versions 28 and 36] [Versions 28 and 37]

       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  }
    

    Search This Site: