Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.
   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   * Defines the editing form for the calculated question type.
  19   *
  20   * @package    qtype
  21   * @subpackage calculated
  22   * @copyright  2007 Jamie Pratt me@jamiep.org
  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/numerical/edit_numerical_form.php');
  30  
  31  
  32  /**
  33   * Calculated question type editing form definition.
  34   *
  35   * @copyright  2007 Jamie Pratt me@jamiep.org
  36   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  37   */
  38  class qtype_calculated_edit_form extends qtype_numerical_edit_form {
  39      /**
  40       * Handle to the question type for this question.
  41       *
  42       * @var qtype_calculated
  43       */
  44      public $qtypeobj;
  45      public $questiondisplay;
  46      public $activecategory;
  47      public $categorychanged = false;
  48      public $initialname = '';
  49      public $reload = false;
  50  
  51      public function __construct($submiturl, $question, $category, $contexts,
  52              $formeditable = true) {
  53          global $CFG, $DB;
  54          $this->question = $question;
  55          $this->reload = optional_param('reload', false, PARAM_BOOL);
  56  
  57          if (!$this->reload) { // Use database data as this is first pass.
  58              if (isset($this->question->id)) {
  59                  // Remove prefix #{..}# if exists.
  60                  $this->initialname = $question->name;
  61                  $question->name = question_bank::get_qtype($this->qtype())
  62                          ->clean_technical_prefix_from_question_name($question->name);
  63              }
  64          }
  65          parent::__construct($submiturl, $question, $category, $contexts, $formeditable);
  66      }
  67  
  68      public function get_per_answer_fields($mform, $label, $gradeoptions,
  69              &$repeatedoptions, &$answersoption) {
  70          $repeated = parent::get_per_answer_fields($mform, $label, $gradeoptions,
  71                  $repeatedoptions, $answersoption);
  72  
  73          // Reorganise answer options group. 0 is the answer. 1 is tolerance. 2 is Grade.
  74          $answeroptions = $repeated[0]->getElements();
  75          // Tolerance field will be part of its own group.
  76          $tolerance = $answeroptions[1];
  77  
  78          // Update Answer options group to contain only answer and grade fields.
  79          $answeroptions[0]->setSize(55);
  80          $answeroptions = array($answeroptions[0], $answeroptions[2]);
  81          $repeated[0]->setElements($answeroptions);
  82  
  83          // Update answer field and group label.
  84          $repeated[0]->setLabel(get_string('answerformula', 'qtype_calculated', '{no}') . ' =');
  85          $answeroptions[0]->setLabel(get_string('answerformula', 'qtype_calculated', '{no}') . ' =');
  86  
  87          // Get feedback field to re append later.
  88          $feedback = array_pop($repeated);
  89  
  90          // Create tolerance group.
  91          $answertolerance = array();
  92          $tolerance->setLabel(get_string('tolerance', 'qtype_calculated') . '=');
  93          $answertolerance[] = $tolerance;
  94          $answertolerance[] = $mform->createElement('select', 'tolerancetype',
  95                  get_string('tolerancetype', 'qtype_calculated'), $this->qtypeobj->tolerance_types());
  96          $repeated[] = $mform->createElement('group', 'answertolerance',
  97                   get_string('tolerance', 'qtype_calculated'), $answertolerance, null, false);
  98          $repeatedoptions['tolerance']['default'] = 0.01;
  99  
 100          // Create display group.
 101          $answerdisplay = array();
 102          $answerdisplay[] = $mform->createElement('select', 'correctanswerlength',
 103                  get_string('answerdisplay', 'qtype_calculated'), range(0, 9));
 104          $repeatedoptions['correctanswerlength']['default'] = 2;
 105  
 106          $answerlengthformats = array(
 107              '1' => get_string('decimalformat', 'qtype_numerical'),
 108              '2' => get_string('significantfiguresformat', 'qtype_calculated')
 109          );
 110          $answerdisplay[] = $mform->createElement('select', 'correctanswerformat',
 111                  get_string('correctanswershowsformat', 'qtype_calculated'), $answerlengthformats);
 112          $repeated[] = $mform->createElement('group', 'answerdisplay',
 113                   get_string('answerdisplay', 'qtype_calculated'), $answerdisplay, null, false);
 114  
 115          // Add feedback.
 116          $repeated[] = $feedback;
 117  
 118          return $repeated;
 119      }
 120  
 121      /**
 122       * Add question-type specific form fields.
 123       *
 124       * @param MoodleQuickForm $mform the form being built.
 125       */
 126      protected function definition_inner($mform) {
 127          $this->qtypeobj = question_bank::get_qtype($this->qtype());
 128          $label = get_string('sharedwildcards', 'qtype_calculated');
 129          $mform->addElement('hidden', 'initialcategory', 1);
 130          $mform->addElement('hidden', 'reload', 1);
 131          $mform->setType('initialcategory', PARAM_INT);
 132          $mform->setType('reload', PARAM_BOOL);
 133          $html2 = $this->qtypeobj->print_dataset_definitions_category($this->question);
 134          $mform->insertElementBefore(
 135                  $mform->createElement('static', 'listcategory', $label, $html2), 'name');
 136          if (isset($this->question->id)) {
 137              $mform->insertElementBefore($mform->createElement('static', 'initialname',
 138                      get_string('questionstoredname', 'qtype_calculated'),
 139                      format_string($this->initialname)), 'name');
 140          };
 141          $addfieldsname = 'updatecategory';
 142          $addstring = get_string('updatecategory', 'qtype_calculated');
 143          $mform->registerNoSubmitButton($addfieldsname);
 144  
 145          $mform->insertElementBefore(
 146                  $mform->createElement('submit', $addfieldsname, $addstring), 'listcategory');
 147          $mform->registerNoSubmitButton('createoptionbutton');
 148  
 149          // Editing as regular question.
 150          $mform->setType('single', PARAM_INT);
 151  
 152          $mform->addElement('hidden', 'shuffleanswers', '1');
 153          $mform->setType('shuffleanswers', PARAM_INT);
 154          $mform->addElement('hidden', 'answernumbering', 'abc');
 155          $mform->setType('answernumbering', PARAM_SAFEDIR);
 156  
 157          $this->add_per_answer_fields($mform, get_string('answerhdr', 'qtype_calculated', '{no}'),
 158                  question_bank::fraction_options(), 1, 1);
 159  
 160          $repeated = array();
 161  
 162          $this->add_unit_options($mform, $this);
 163          $this->add_unit_fields($mform, $this);
 164          $this->add_interactive_settings();
 165  
 166          // Hidden elements.
 167          $mform->addElement('hidden', 'synchronize', '');
 168          $mform->setType('synchronize', PARAM_INT);
 169          $mform->addElement('hidden', 'wizard', 'datasetdefinitions');
 170          $mform->setType('wizard', PARAM_ALPHA);
 171      }
 172  
 173      protected function can_preview() {
 174          return false; // Generally not possible for calculated questions on this page.
 175      }
 176  
 177      public function data_preprocessing($question) {
 178          $question = parent::data_preprocessing($question);
 179          $question = $this->data_preprocessing_answers($question);
 180          $question = $this->data_preprocessing_hints($question);
 181          $question = $this->data_preprocessing_units($question);
 182          $question = $this->data_preprocessing_unit_options($question);
 183  
 184          if (isset($question->options->synchronize)) {
 185              $question->synchronize = $question->options->synchronize;
 186          }
 187  
 188          return $question;
 189      }
 190  
 191      protected function data_preprocessing_answers($question, $withanswerfiles = false) {
 192          $question = parent::data_preprocessing_answers($question, $withanswerfiles);
 193          if (empty($question->options->answers)) {
 194              return $question;
 195          }
 196  
 197          $key = 0;
 198          foreach ($question->options->answers as $answer) {
 199              // See comment in the parent method about this hack.
 200              unset($this->_form->_defaultValues["tolerancetype[{$key}]"]);
 201              unset($this->_form->_defaultValues["correctanswerlength[{$key}]"]);
 202              unset($this->_form->_defaultValues["correctanswerformat[{$key}]"]);
 203  
 204              $question->tolerancetype[$key]       = $answer->tolerancetype;
 205              $question->correctanswerlength[$key] = $answer->correctanswerlength;
 206              $question->correctanswerformat[$key] = $answer->correctanswerformat;
 207              $key++;
 208          }
 209  
 210          return $question;
 211      }
 212  
 213      public function qtype() {
 214          return 'calculated';
 215      }
 216  
 217      /**
 218       * Validate the equations in the some question content.
 219       * @param array $errors where errors are being accumulated.
 220       * @param string $field the field being validated.
 221       * @param string $text the content of that field.
 222       * @return array the updated $errors array.
 223       */
 224      protected function validate_text($errors, $field, $text) {
 225          $problems = qtype_calculated_find_formula_errors_in_text($text);
 226          if ($problems) {
 227              $errors[$field] = $problems;
 228          }
 229          return $errors;
 230      }
 231  
 232      public function validation($data, $files) {
 233          $errors = parent::validation($data, $files);
 234  
 235          // Verifying for errors in {=...} in question text.
 236          $errors = $this->validate_text($errors, 'questiontext', $data['questiontext']['text']);
 237          $errors = $this->validate_text($errors, 'generalfeedback', $data['generalfeedback']['text']);
 238  
 239          // Check that the answers use datasets.
 240          $answers = $data['answer'];
 241          $mandatorydatasets = array();
 242          foreach ($answers as $key => $answer) {
 243              $problems = qtype_calculated_find_formula_errors($answer);
 244              if ($problems) {
 245                  $errors['answeroptions['.$key.']'] = $problems;
 246              }
 247              $mandatorydatasets += $this->qtypeobj->find_dataset_names($answer);
 248              $errors = $this->validate_text($errors, 'feedback[' . $key . ']',
 249                      $data['feedback'][$key]['text']);
 250          }
 251          if (empty($mandatorydatasets)) {
 252              foreach ($answers as $key => $answer) {
 253                  $errors['answeroptions['.$key.']'] =
 254                          get_string('atleastonewildcard', 'qtype_calculated');
 255              }
 256          }
 257  
 258          // Validate the answer format.
 259          foreach ($answers as $key => $answer) {
 260              $trimmedanswer = trim($answer);
 261              if (trim($answer)) {
 262                  if ($data['correctanswerformat'][$key] == 2 &&
 263                          $data['correctanswerlength'][$key] == '0') {
 264                      $errors['answerdisplay['.$key.']'] =
 265                              get_string('zerosignificantfiguresnotallowed', 'qtype_calculated');
 266                  }
 267              }
 268          }
 269  
 270          return $errors;
 271      }
 272  
 273      protected function is_valid_answer($answer, $data) {
 274          return !qtype_calculated_find_formula_errors($answer);
 275      }
 276  
 277      protected function valid_answer_message($answer) {
 278          if (!$answer) {
 279              return get_string('mustenteraformulaorstar', 'qtype_numerical');
 280          } else {
 281              return qtype_calculated_find_formula_errors($answer);
 282          }
 283      }
 284  }