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.

Differences Between: [Versions 310 and 401] [Versions 39 and 401]

   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 numerical question type.
  19   *
  20   * @package    qtype
  21   * @subpackage numerical
  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/edit_question_form.php');
  30  require_once($CFG->dirroot . '/question/type/numerical/questiontype.php');
  31  
  32  
  33  /**
  34   * numerical editing form definition.
  35   *
  36   * @copyright  2007 Jamie Pratt me@jamiep.org
  37   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  38   */
  39  class qtype_numerical_edit_form extends question_edit_form {
  40      /** @var int we always show at least this many sets of unit fields. */
  41      const UNITS_MIN_REPEATS = 1;
  42      const UNITS_TO_ADD = 2;
  43  
  44      protected $ap = null;
  45  
  46      protected function definition_inner($mform) {
  47          $this->add_per_answer_fields($mform, get_string('answerno', 'qtype_numerical', '{no}'),
  48                  question_bank::fraction_options());
  49  
  50          $this->add_unit_options($mform);
  51          $this->add_unit_fields($mform);
  52          $this->add_interactive_settings();
  53      }
  54  
  55      protected function get_per_answer_fields($mform, $label, $gradeoptions,
  56              &$repeatedoptions, &$answersoption) {
  57          $repeated = parent::get_per_answer_fields($mform, $label, $gradeoptions,
  58                  $repeatedoptions, $answersoption);
  59  
  60          $tolerance = $mform->createElement('float', 'tolerance',
  61                  get_string('answererror', 'qtype_numerical'), array('size' => 15));
  62          $repeatedoptions['tolerance']['default'] = 0;
  63          $elements = $repeated[0]->getElements();
  64          $elements[0]->setSize(15);
  65          array_splice($elements, 1, 0, array($tolerance));
  66          $repeated[0]->setElements($elements);
  67  
  68          return $repeated;
  69      }
  70  
  71      protected function get_more_choices_string() {
  72          return get_string('addmoreanswerblanks', 'qtype_numerical');
  73      }
  74  
  75      /**
  76       * Add the unit handling options to the form.
  77       * @param object $mform the form being built.
  78       */
  79      protected function add_unit_options($mform) {
  80  
  81          $mform->addElement('header', 'unithandling',
  82                  get_string('unithandling', 'qtype_numerical'));
  83  
  84          $unitoptions = array(
  85              qtype_numerical::UNITNONE     => get_string('onlynumerical', 'qtype_numerical'),
  86              qtype_numerical::UNITOPTIONAL => get_string('manynumerical', 'qtype_numerical'),
  87              qtype_numerical::UNITGRADED   => get_string('unitgraded', 'qtype_numerical'),
  88          );
  89          $mform->addElement('select', 'unitrole',
  90                  get_string('unithandling', 'qtype_numerical'), $unitoptions);
  91          $mform->setDefault('unitrole', $this->get_default_value('unitrole', qtype_numerical::UNITNONE));
  92  
  93          $penaltygrp = array();
  94          $penaltygrp[] = $mform->createElement('float', 'unitpenalty',
  95                  get_string('unitpenalty', 'qtype_numerical'), array('size' => 6));
  96          $mform->setDefault('unitpenalty', $this->get_default_value('unitpenalty', 0.1000000));
  97  
  98          $unitgradingtypes = array(
  99              qtype_numerical::UNITGRADEDOUTOFMARK =>
 100                      get_string('decfractionofresponsegrade', 'qtype_numerical'),
 101              qtype_numerical::UNITGRADEDOUTOFMAX =>
 102                      get_string('decfractionofquestiongrade', 'qtype_numerical'),
 103          );
 104          $penaltygrp[] = $mform->createElement('select', 'unitgradingtypes', '', $unitgradingtypes);
 105          $mform->setDefault('unitgradingtypes',
 106                  $this->get_default_value('unitgradingtypes', qtype_numerical::UNITGRADEDOUTOFMARK));
 107  
 108          $mform->addGroup($penaltygrp, 'penaltygrp',
 109                  get_string('unitpenalty', 'qtype_numerical'), ' ', false);
 110          $mform->addHelpButton('penaltygrp', 'unitpenalty', 'qtype_numerical');
 111  
 112          $unitinputoptions = array(
 113              qtype_numerical::UNITINPUT => get_string('editableunittext', 'qtype_numerical'),
 114              qtype_numerical::UNITRADIO => get_string('unitchoice', 'qtype_numerical'),
 115              qtype_numerical::UNITSELECT => get_string('unitselect', 'qtype_numerical'),
 116          );
 117          $mform->addElement('select', 'multichoicedisplay',
 118                  get_string('studentunitanswer', 'qtype_numerical'), $unitinputoptions);
 119          $mform->setDefault('multichoicedisplay',
 120                  $this->get_default_value('multichoicedisplay', qtype_numerical::UNITINPUT));
 121  
 122          $unitsleftoptions = array(
 123              0 => get_string('rightexample', 'qtype_numerical'),
 124              1 => get_string('leftexample', 'qtype_numerical')
 125          );
 126          $mform->addElement('select', 'unitsleft',
 127                  get_string('unitposition', 'qtype_numerical'), $unitsleftoptions);
 128          $mform->setDefault('unitsleft', $this->get_default_value('unitsleft', 0));
 129  
 130          $mform->disabledIf('penaltygrp', 'unitrole', 'eq', qtype_numerical::UNITNONE);
 131          $mform->disabledIf('penaltygrp', 'unitrole', 'eq', qtype_numerical::UNITOPTIONAL);
 132  
 133          $mform->disabledIf('unitsleft', 'unitrole', 'eq', qtype_numerical::UNITNONE);
 134  
 135          $mform->disabledIf('multichoicedisplay', 'unitrole', 'eq', qtype_numerical::UNITNONE);
 136          $mform->disabledIf('multichoicedisplay', 'unitrole', 'eq', qtype_numerical::UNITOPTIONAL);
 137      }
 138  
 139      /**
 140       * Add the input areas for each unit.
 141       * @param object $mform the form being built.
 142       */
 143      protected function add_unit_fields($mform) {
 144          $mform->addElement('header', 'unithdr',
 145                      get_string('units', 'qtype_numerical'), '');
 146  
 147          $unitfields = array($mform->createElement('group', 'units',
 148                   get_string('unitx', 'qtype_numerical'), $this->unit_group($mform), null, false));
 149  
 150          $repeatedoptions['unit']['disabledif'] = array('unitrole', 'eq', qtype_numerical::UNITNONE);
 151          $repeatedoptions['unit']['type'] = PARAM_NOTAGS;
 152          $repeatedoptions['multiplier']['disabledif'] = array('unitrole', 'eq', qtype_numerical::UNITNONE);
 153  
 154          $mform->disabledIf('addunits', 'unitrole', 'eq', qtype_numerical::UNITNONE);
 155  
 156          if (isset($this->question->options->units)) {
 157              $repeatsatstart = max(count($this->question->options->units), self::UNITS_MIN_REPEATS);
 158          } else {
 159              $repeatsatstart = self::UNITS_MIN_REPEATS;
 160          }
 161  
 162          $this->repeat_elements($unitfields, $repeatsatstart, $repeatedoptions, 'nounits',
 163                  'addunits', self::UNITS_TO_ADD, get_string('addmoreunitblanks', 'qtype_numerical', '{no}'), true);
 164  
 165          // The following strange-looking if statement is to do with when the
 166          // form is used to move questions between categories. See MDL-15159.
 167          if ($mform->elementExists('units[0]')) {
 168              $firstunit = $mform->getElement('units[0]');
 169              $elements = $firstunit->getElements();
 170              foreach ($elements as $element) {
 171                  if ($element->getName() != 'multiplier[0]') {
 172                      continue;
 173                  }
 174                  $element->freeze();
 175                  $element->setValue('1.0');
 176                  $element->setPersistantFreeze(true);
 177              }
 178              $mform->addHelpButton('units[0]', 'numericalmultiplier', 'qtype_numerical');
 179          }
 180      }
 181  
 182      /**
 183       * Get the form fields needed to edit one unit.
 184       * @param MoodleQuickForm $mform the form being built.
 185       * @return array of form fields.
 186       */
 187      protected function unit_group($mform) {
 188          $grouparray = array();
 189          $grouparray[] = $mform->createElement('text', 'unit', get_string('unit', 'qtype_numerical'), array('size'=>10));
 190          $grouparray[] = $mform->createElement('float', 'multiplier',
 191                  get_string('multiplier', 'qtype_numerical'), array('size'=>10));
 192  
 193          return $grouparray;
 194      }
 195  
 196      protected function data_preprocessing($question) {
 197          $question = parent::data_preprocessing($question);
 198          $question = $this->data_preprocessing_answers($question);
 199          $question = $this->data_preprocessing_hints($question);
 200          $question = $this->data_preprocessing_units($question);
 201          $question = $this->data_preprocessing_unit_options($question);
 202          return $question;
 203      }
 204  
 205      protected function data_preprocessing_answers($question, $withanswerfiles = false) {
 206          $question = parent::data_preprocessing_answers($question, $withanswerfiles);
 207          if (empty($question->options->answers)) {
 208              return $question;
 209          }
 210  
 211          $key = 0;
 212          foreach ($question->options->answers as $answer) {
 213              // See comment in the parent method about this hack.
 214              unset($this->_form->_defaultValues["tolerance[{$key}]"]);
 215  
 216              $question->tolerance[$key] = $answer->tolerance;
 217  
 218              if (is_numeric($question->answer[$key])) {
 219                  $question->answer[$key] = format_float($question->answer[$key], -1);
 220              }
 221  
 222              $key++;
 223          }
 224  
 225          return $question;
 226      }
 227  
 228      /**
 229       * Perform the necessary preprocessing for the fields added by
 230       * {@link add_unit_fields()}.
 231       * @param object $question the data being passed to the form.
 232       * @return object $question the modified data.
 233       */
 234      protected function data_preprocessing_units($question) {
 235          if (empty($question->options->units)) {
 236              return $question;
 237          }
 238  
 239          foreach ($question->options->units as $key => $unit) {
 240              $question->unit[$key] = $unit->unit;
 241              $question->multiplier[$key] = $unit->multiplier;
 242          }
 243  
 244          return $question;
 245      }
 246  
 247      /**
 248       * Perform the necessary preprocessing for the fields added by
 249       * {@link add_unit_options()}.
 250       * @param object $question the data being passed to the form.
 251       * @return object $question the modified data.
 252       */
 253      protected function data_preprocessing_unit_options($question) {
 254          if (empty($question->options)) {
 255              return $question;
 256          }
 257  
 258          $question->unitpenalty = $question->options->unitpenalty;
 259          $question->unitsleft = $question->options->unitsleft;
 260  
 261          if ($question->options->unitgradingtype) {
 262              $question->unitgradingtypes = $question->options->unitgradingtype;
 263              $question->multichoicedisplay = $question->options->showunits;
 264              $question->unitrole = qtype_numerical::UNITGRADED;
 265          } else {
 266              $question->unitrole = $question->options->showunits;
 267          }
 268  
 269          return $question;
 270      }
 271  
 272      public function validation($data, $files) {
 273          $errors = parent::validation($data, $files);
 274          $errors = $this->validate_answers($data, $errors);
 275          $errors = $this->validate_numerical_options($data, $errors);
 276          return $errors;
 277      }
 278  
 279      /**
 280       * Validate the answers.
 281       * @param array $data the submitted data.
 282       * @param array $errors the errors array to add to.
 283       * @return array the updated errors array.
 284       */
 285      protected function validate_answers($data, $errors) {
 286          // Check the answers.
 287          $answercount = 0;
 288          $maxgrade = false;
 289          $answers = $data['answer'];
 290          foreach ($answers as $key => $answer) {
 291              $trimmedanswer = trim($answer);
 292              if ($trimmedanswer != '') {
 293                  $answercount++;
 294                  if (!$this->is_valid_answer($trimmedanswer, $data)) {
 295                      $errors['answeroptions[' . $key . ']'] = $this->valid_answer_message($trimmedanswer);
 296                  }
 297                  if ($data['fraction'][$key] == 1) {
 298                      $maxgrade = true;
 299                  }
 300                  if ($answer !== '*' && $data['tolerance'][$key] === false) {
 301                      $errors['answeroptions['.$key.']'] =
 302                              get_string('xmustbenumeric', 'qtype_numerical',
 303                                  get_string('acceptederror', 'qtype_numerical'));
 304                  }
 305              } else if ($data['fraction'][$key] != 0 ||
 306                      !html_is_blank($data['feedback'][$key]['text'])) {
 307                  $errors['answeroptions[' . $key . ']'] = $this->valid_answer_message($trimmedanswer);
 308                  $answercount++;
 309              }
 310          }
 311          if ($answercount == 0) {
 312              $errors['answeroptions[0]'] = get_string('notenoughanswers', 'qtype_numerical');
 313          }
 314          if ($maxgrade == false) {
 315              $errors['answeroptions[0]'] = get_string('fractionsnomax', 'question');
 316          }
 317  
 318          return $errors;
 319      }
 320  
 321      /**
 322       * Validate a particular answer.
 323       * @param string $answer an answer to validate. Known to be non-blank and already trimmed.
 324       * @param array $data the submitted data.
 325       * @return bool whether this is a valid answer.
 326       */
 327      protected function is_valid_answer($answer, $data) {
 328          return $answer == '*' || qtype_numerical::is_valid_number($answer);
 329      }
 330  
 331      /**
 332       * @return string erre describing what an answer should be.
 333       */
 334      protected function valid_answer_message($answer) {
 335          return get_string('answermustbenumberorstar', 'qtype_numerical');
 336      }
 337  
 338      /**
 339       * Validate the answers.
 340       * @param array $data the submitted data.
 341       * @param array $errors the errors array to add to.
 342       * @return array the updated errors array.
 343       */
 344      protected function validate_numerical_options($data, $errors) {
 345          if ($data['unitrole'] != qtype_numerical::UNITNONE && trim($data['unit'][0]) == '') {
 346              $errors['units[0]'] = get_string('unitonerequired', 'qtype_numerical');
 347          }
 348  
 349          if (empty($data['unit']) || $data['unitrole'] == qtype_numerical::UNITNONE) {
 350              return $errors;
 351          }
 352  
 353          // Basic unit validation.
 354          foreach ($data['unit'] as $key => $unit) {
 355              if (is_numeric($unit)) {
 356                  $errors['units[' . $key . ']'] =
 357                          get_string('xmustnotbenumeric', 'qtype_numerical',
 358                              get_string('unit', 'qtype_numerical'));
 359              }
 360  
 361              $trimmedunit = trim($unit);
 362              if (empty($trimmedunit)) {
 363                  continue;
 364              }
 365  
 366              $trimmedmultiplier = trim($data['multiplier'][$key]);
 367              if (empty($trimmedmultiplier)) {
 368                  $errors['units[' . $key . ']'] =
 369                          get_string('youmustenteramultiplierhere', 'qtype_numerical');
 370              } else if ($trimmedmultiplier === false) {
 371                  $errors['units[' . $key . ']'] =
 372                          get_string('xmustbenumeric', 'qtype_numerical',
 373                              get_string('multiplier', 'qtype_numerical'));
 374              }
 375          }
 376  
 377          // Check for repeated units.
 378          $alreadyseenunits = array();
 379          foreach ($data['unit'] as $key => $unit) {
 380              $trimmedunit = trim($unit);
 381              if ($trimmedunit == '') {
 382                  continue;
 383              }
 384  
 385              if (in_array($trimmedunit, $alreadyseenunits)) {
 386                  $errors['units[' . $key . ']'] =
 387                          get_string('errorrepeatedunit', 'qtype_numerical');
 388              } else {
 389                  $alreadyseenunits[] = $trimmedunit;
 390              }
 391          }
 392  
 393          return $errors;
 394      }
 395  
 396      public function qtype() {
 397          return 'numerical';
 398      }
 399  }