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] [Versions 401 and 402] [Versions 401 and 403]

   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 calculated multiple-choice questions.
  19   *
  20   * @package    qtype
  21   * @subpackage calculatedmulti
  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  
  30  /**
  31   * Calculated multiple-choice question editing form.
  32   *
  33   * @copyright  2007 Jamie Pratt me@jamiep.org
  34   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  35   */
  36  class qtype_calculatedmulti_edit_form extends question_edit_form {
  37      /**
  38       * Handle to the question type for this question.
  39       *
  40       * @var question_calculatedmulti_qtype
  41       */
  42      public $qtypeobj;
  43      public $questiondisplay;
  44      public $initialname = '';
  45      public $reload = false;
  46  
  47      public function __construct($submiturl, $question, $category,
  48              $contexts, $formeditable = true) {
  49          $this->question = $question;
  50          $this->qtypeobj = question_bank::get_qtype('calculatedmulti');
  51          $this->reload = optional_param('reload', false, PARAM_BOOL);
  52          if (!$this->reload) {
  53              // Use database data as this is first pass.
  54              if (isset($this->question->id)) {
  55                  // Remove prefix #{..}# if exists.
  56                  $this->initialname = $question->name;
  57                  $question->name = question_bank::get_qtype('calculated')
  58                          ->clean_technical_prefix_from_question_name($question->name);
  59              }
  60          }
  61          parent::__construct($submiturl, $question, $category, $contexts, $formeditable);
  62      }
  63  
  64      protected function can_preview() {
  65          return false; // Generally not possible for calculated multi-choice questions on this page.
  66      }
  67  
  68      public function get_per_answer_fields($mform, $label, $gradeoptions,
  69              &$repeatedoptions, &$answersoption) {
  70          $repeated = array();
  71          $answeroptions = array();
  72          $answeroptions[] = $mform->createElement('text', 'answer',
  73                  $label, array('size' => 50));
  74          $answeroptions[] = $mform->createElement('select', 'fraction',
  75                  get_string('gradenoun'), $gradeoptions);
  76          $repeated[] = $mform->createElement('group', 'answeroptions',
  77                   $label, $answeroptions, null, false);
  78  
  79          // Added answeroptions help button in definition_inner() after called to add_per_answer_fields.
  80  
  81          $repeatedoptions['answer']['type'] = PARAM_RAW;
  82          $repeatedoptions['fraction']['default'] = 0;
  83          $answersoption = 'answers';
  84  
  85          $mform->setType('answer', PARAM_NOTAGS);
  86  
  87          $repeated[] = $mform->createElement('hidden', 'tolerance');
  88          $repeated[] = $mform->createElement('hidden', 'tolerancetype', 1);
  89          $repeatedoptions['tolerance']['type'] = PARAM_FLOAT;
  90          $repeatedoptions['tolerance']['default'] = 0.01;
  91          $repeatedoptions['tolerancetype']['type'] = PARAM_INT;
  92  
  93          // Create display group.
  94          $answerdisplay = array();
  95          $answerdisplay[] =  $mform->createElement('select', 'correctanswerlength',
  96                  get_string('answerdisplay', 'qtype_calculated'), range(0, 9));
  97          $repeatedoptions['correctanswerlength']['default'] = 2;
  98  
  99          $answerlengthformats = array(
 100              '1' => get_string('decimalformat', 'qtype_numerical'),
 101              '2' => get_string('significantfiguresformat', 'qtype_calculated')
 102          );
 103          $answerdisplay[] = $mform->createElement('select', 'correctanswerformat',
 104                  get_string('correctanswershowsformat', 'qtype_calculated'), $answerlengthformats);
 105          $repeated[] = $mform->createElement('group', 'answerdisplay',
 106                   get_string('answerdisplay', 'qtype_calculated'), $answerdisplay, null, false);
 107  
 108          // Add feedback.
 109          $repeated[] = $mform->createElement('editor', 'feedback',
 110                  get_string('feedback', 'question'), null, $this->editoroptions);
 111  
 112          return $repeated;
 113      }
 114  
 115      protected function definition_inner($mform) {
 116  
 117          $label = get_string('sharedwildcards', 'qtype_calculated');
 118          $mform->addElement('hidden', 'initialcategory', 1);
 119          $mform->addElement('hidden', 'reload', 1);
 120          $mform->setType('initialcategory', PARAM_INT);
 121          $mform->setType('reload', PARAM_BOOL);
 122  
 123          $html2 = '';
 124          $mform->insertElementBefore(
 125                  $mform->createElement('static', 'listcategory', $label, $html2), 'name');
 126          if (isset($this->question->id)) {
 127              $mform->insertElementBefore($mform->createElement('static', 'initialname',
 128                      get_string('questionstoredname', 'qtype_calculated'),
 129                      format_string($this->initialname)), 'name');
 130          };
 131          $addfieldsname = 'updatecategory';
 132          $addstring = get_string('updatecategory', 'qtype_calculated');
 133          $mform->registerNoSubmitButton($addfieldsname);
 134          $this->editasmultichoice = 1;
 135  
 136          $mform->insertElementBefore(
 137                  $mform->createElement('submit', $addfieldsname, $addstring), 'listcategory');
 138          $mform->registerNoSubmitButton('createoptionbutton');
 139          $mform->addElement('hidden', 'multichoice', $this->editasmultichoice);
 140          $mform->setType('multichoice', PARAM_INT);
 141  
 142          $menu = array(get_string('answersingleno', 'qtype_multichoice'),
 143                  get_string('answersingleyes', 'qtype_multichoice'));
 144          $mform->addElement('select', 'single',
 145                  get_string('answerhowmany', 'qtype_multichoice'), $menu);
 146          $mform->setDefault('single', 1);
 147  
 148          $mform->addElement('advcheckbox', 'shuffleanswers',
 149                  get_string('shuffleanswers', 'qtype_multichoice'), null, null, array(0, 1));
 150          $mform->addHelpButton('shuffleanswers', 'shuffleanswers', 'qtype_multichoice');
 151          $mform->setDefault('shuffleanswers', 1);
 152  
 153          $numberingoptions = question_bank::get_qtype('multichoice')->get_numbering_styles();
 154          $mform->addElement('select', 'answernumbering',
 155                  get_string('answernumbering', 'qtype_multichoice'), $numberingoptions);
 156          $mform->setDefault('answernumbering', 'abc');
 157  
 158          $this->add_per_answer_fields($mform, get_string('choiceno', 'qtype_multichoice', '{no}'),
 159                  question_bank::fraction_options_full(), max(5, QUESTION_NUMANS_START));
 160          $mform->addHelpButton('answeroptions[0]', 'answeroptions', 'qtype_calculatedmulti');
 161  
 162          $repeated = array();
 163          $nounits = optional_param('nounits', 1, PARAM_INT);
 164          $mform->addElement('hidden', 'nounits', $nounits);
 165          $mform->setType('nounits', PARAM_INT);
 166          $mform->setConstants(array('nounits'=>$nounits));
 167          for ($i = 0; $i < $nounits; $i++) {
 168              $mform->addElement('hidden', 'unit'."[{$i}]",
 169                      optional_param("unit[{$i}]", '', PARAM_NOTAGS));
 170              $mform->setType('unit'."[{$i}]", PARAM_NOTAGS);
 171              $mform->addElement('hidden', 'multiplier'."[{$i}]",
 172                      optional_param("multiplier[{$i}]", '', PARAM_FLOAT));
 173              $mform->setType("multiplier[{$i}]", PARAM_FLOAT);
 174          }
 175  
 176          $this->add_combined_feedback_fields(true);
 177          $mform->disabledIf('shownumcorrect', 'single', 'eq', 1);
 178  
 179          $this->add_interactive_settings(true, true);
 180  
 181          // Hidden elements.
 182          $mform->addElement('hidden', 'synchronize', '');
 183          $mform->setType('synchronize', PARAM_INT);
 184          if (isset($this->question->options) && isset($this->question->options->synchronize)) {
 185              $mform->setDefault('synchronize', $this->question->options->synchronize);
 186          } else {
 187              $mform->setDefault('synchronize', 0);
 188          }
 189          $mform->addElement('hidden', 'wizard', 'datasetdefinitions');
 190          $mform->setType('wizard', PARAM_ALPHA);
 191      }
 192  
 193      public function data_preprocessing($question) {
 194          $question = parent::data_preprocessing($question);
 195          $question = $this->data_preprocessing_answers($question, false);
 196          $question = $this->data_preprocessing_combined_feedback($question, true);
 197          $question = $this->data_preprocessing_hints($question, true, true);
 198  
 199          if (isset($question->options)) {
 200              $question->synchronize     = $question->options->synchronize;
 201              $question->single          = $question->options->single;
 202              $question->answernumbering = $question->options->answernumbering;
 203              $question->shuffleanswers  = $question->options->shuffleanswers;
 204          }
 205  
 206          return $question;
 207      }
 208  
 209      protected function data_preprocessing_answers($question, $withanswerfiles = false) {
 210          $question = parent::data_preprocessing_answers($question, $withanswerfiles);
 211          if (empty($question->options->answers)) {
 212              return $question;
 213          }
 214  
 215          $key = 0;
 216          foreach ($question->options->answers as $answer) {
 217              // See comment in the parent method about this hack.
 218              unset($this->_form->_defaultValues["tolerance[{$key}]"]);
 219              unset($this->_form->_defaultValues["tolerancetype[{$key}]"]);
 220              unset($this->_form->_defaultValues["correctanswerlength[{$key}]"]);
 221              unset($this->_form->_defaultValues["correctanswerformat[{$key}]"]);
 222  
 223              $question->tolerance[$key]           = $answer->tolerance;
 224              $question->tolerancetype[$key]       = $answer->tolerancetype;
 225              $question->correctanswerlength[$key] = $answer->correctanswerlength;
 226              $question->correctanswerformat[$key] = $answer->correctanswerformat;
 227              $key++;
 228          }
 229  
 230          return $question;
 231      }
 232  
 233      /**
 234       * Validate the equations in the some question content.
 235       * @param array $errors where errors are being accumulated.
 236       * @param string $field the field being validated.
 237       * @param string $text the content of that field.
 238       * @return array the updated $errors array.
 239       */
 240      protected function validate_text($errors, $field, $text) {
 241          $problems = qtype_calculated_find_formula_errors_in_text($text);
 242          if ($problems) {
 243              $errors[$field] = $problems;
 244          }
 245          return $errors;
 246      }
 247  
 248      public function validation($data, $files) {
 249          $errors = parent::validation($data, $files);
 250  
 251          // Verifying for errors in {=...} in question text.
 252          $errors = $this->validate_text($errors, 'questiontext', $data['questiontext']['text']);
 253          $errors = $this->validate_text($errors, 'generalfeedback', $data['generalfeedback']['text']);
 254          $errors = $this->validate_text($errors, 'correctfeedback', $data['correctfeedback']['text']);
 255          $errors = $this->validate_text($errors, 'partiallycorrectfeedback', $data['partiallycorrectfeedback']['text']);
 256          $errors = $this->validate_text($errors, 'incorrectfeedback', $data['incorrectfeedback']['text']);
 257  
 258          $answers = $data['answer'];
 259          $answercount = 0;
 260          $maxgrade = false;
 261          $possibledatasets = $this->qtypeobj->find_dataset_names($data['questiontext']['text']);
 262          $mandatorydatasets = array();
 263          foreach ($answers as $key => $answer) {
 264              $mandatorydatasets += $this->qtypeobj->find_dataset_names($answer);
 265          }
 266          if (count($mandatorydatasets) == 0) {
 267              foreach ($answers as $key => $answer) {
 268                  $errors['answeroptions['.$key.']'] =
 269                          get_string('atleastonewildcard', 'qtype_calculated');
 270              }
 271          }
 272  
 273          $totalfraction = 0;
 274          $maxfraction = -1;
 275          foreach ($answers as $key => $answer) {
 276              $trimmedanswer = trim($answer);
 277              $fraction = (float) $data['fraction'][$key];
 278              if (empty($trimmedanswer) && $trimmedanswer != '0' && empty($fraction)) {
 279                  continue;
 280              }
 281              if (empty($trimmedanswer)) {
 282                  $errors['answeroptions['.$key.']'] = get_string('errgradesetanswerblank', 'qtype_multichoice');
 283              }
 284              if ($trimmedanswer != '' || $answercount == 0) {
 285                  // Verifying for errors in {=...} in answer text.
 286                  $errors = $this->validate_text($errors, 'answeroptions[' . $key . ']', $answer);
 287                  $errors = $this->validate_text($errors, 'feedback[' . $key . ']',
 288                          $data['feedback'][$key]['text']);
 289              }
 290  
 291              if ($trimmedanswer != '') {
 292                  if ('2' == $data['correctanswerformat'][$key] &&
 293                          '0' == $data['correctanswerlength'][$key]) {
 294                      $errors['correctanswerlength['.$key.']'] =
 295                              get_string('zerosignificantfiguresnotallowed', 'qtype_calculated');
 296                  }
 297                  if (!is_numeric($data['tolerance'][$key])) {
 298                      $errors['tolerance['.$key.']'] =
 299                              get_string('xmustbenumeric', 'qtype_numerical',
 300                                  get_string('acceptederror', 'qtype_numerical'));
 301                  }
 302                  if ($data['fraction'][$key] > 0) {
 303                      $totalfraction += $data['fraction'][$key];
 304                  }
 305                  if ($data['fraction'][$key] > $maxfraction) {
 306                      $maxfraction = $data['fraction'][$key];
 307                  }
 308  
 309                  $answercount++;
 310              }
 311          }
 312          if ($answercount == 0) {
 313              $errors['answeroptions[0]'] = get_string('notenoughanswers', 'qtype_multichoice', 2);
 314              $errors['answeroptions[1]'] = get_string('notenoughanswers', 'qtype_multichoice', 2);
 315          } else if ($answercount == 1) {
 316              $errors['answeroptions[1]'] = get_string('notenoughanswers', 'qtype_multichoice', 2);
 317  
 318          }
 319          // Perform sanity checks on fractional grades.
 320          if ($data['single']== 1 ) {
 321              if ($maxfraction != 1) {
 322                  $errors['answeroptions[0]'] = get_string('errfractionsnomax', 'qtype_multichoice',
 323                          $maxfraction * 100);
 324              }
 325          } else {
 326              $totalfraction = round($totalfraction, 2);
 327              if ($totalfraction != 1) {
 328                  $totalfraction = $totalfraction * 100;
 329                  $errors['answeroptions[0]'] =
 330                          get_string('errfractionsaddwrong', 'qtype_multichoice', $totalfraction);
 331              }
 332          }
 333          return $errors;
 334      }
 335  
 336      public function qtype() {
 337          return 'calculatedmulti';
 338      }
 339  }