Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403] [Versions 402 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 quiz module ettings form.
  19   *
  20   * @package    mod_quiz
  21   * @copyright  2006 Jamie Pratt
  22   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  
  26  defined('MOODLE_INTERNAL') || die();
  27  
  28  require_once($CFG->dirroot . '/course/moodleform_mod.php');
  29  require_once($CFG->dirroot . '/mod/quiz/locallib.php');
  30  
  31  use mod_quiz\access_manager;
  32  use mod_quiz\question\display_options;
  33  
  34  /**
  35   * Settings form for the quiz module.
  36   *
  37   * @copyright  2006 Jamie Pratt
  38   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  39   */
  40  class mod_quiz_mod_form extends moodleform_mod {
  41      /** @var array options to be used with date_time_selector fields in the quiz. */
  42      public static $datefieldoptions = ['optional' => true];
  43  
  44      /** @var array caches the quiz overall feedback, for convenience. */
  45      protected $_feedbacks;
  46  
  47      /** @var array for convenience stores the list of types of review option. Initialised in the constructor. */
  48      protected static $reviewfields = [];
  49  
  50      /** @var int the max number of attempts allowed in any user or group override on this quiz. */
  51      protected $maxattemptsanyoverride = null;
  52  
  53      public function __construct($current, $section, $cm, $course) {
  54          self::$reviewfields = [
  55              'attempt'          => ['theattempt', 'quiz'],
  56              'correctness'      => ['whethercorrect', 'question'],
  57              'maxmarks'         => ['maxmarks', 'quiz'],
  58              'marks'            => ['marks', 'quiz'],
  59              'specificfeedback' => ['specificfeedback', 'question'],
  60              'generalfeedback'  => ['generalfeedback', 'question'],
  61              'rightanswer'      => ['rightanswer', 'question'],
  62              'overallfeedback'  => ['reviewoverallfeedback', 'quiz'],
  63          ];
  64          parent::__construct($current, $section, $cm, $course);
  65      }
  66  
  67      protected function definition() {
  68          global $CFG, $DB, $PAGE;
  69          $quizconfig = get_config('quiz');
  70          $mform = $this->_form;
  71  
  72          // -------------------------------------------------------------------------------
  73          $mform->addElement('header', 'general', get_string('general', 'form'));
  74  
  75          // Name.
  76          $mform->addElement('text', 'name', get_string('name'), ['size' => '64']);
  77          if (!empty($CFG->formatstringstriptags)) {
  78              $mform->setType('name', PARAM_TEXT);
  79          } else {
  80              $mform->setType('name', PARAM_CLEANHTML);
  81          }
  82          $mform->addRule('name', null, 'required', null, 'client');
  83          $mform->addRule('name', get_string('maximumchars', '', 255), 'maxlength', 255, 'client');
  84  
  85          // Introduction.
  86          $this->standard_intro_elements(get_string('introduction', 'quiz'));
  87  
  88          // -------------------------------------------------------------------------------
  89          $mform->addElement('header', 'timing', get_string('timing', 'quiz'));
  90  
  91          // Open and close dates.
  92          $mform->addElement('date_time_selector', 'timeopen', get_string('quizopen', 'quiz'),
  93                  self::$datefieldoptions);
  94          $mform->addHelpButton('timeopen', 'quizopenclose', 'quiz');
  95  
  96          $mform->addElement('date_time_selector', 'timeclose', get_string('quizclose', 'quiz'),
  97                  self::$datefieldoptions);
  98  
  99          // Time limit.
 100          $mform->addElement('duration', 'timelimit', get_string('timelimit', 'quiz'),
 101                  ['optional' => true]);
 102          $mform->addHelpButton('timelimit', 'timelimit', 'quiz');
 103  
 104          // What to do with overdue attempts.
 105          $mform->addElement('select', 'overduehandling', get_string('overduehandling', 'quiz'),
 106                  quiz_get_overdue_handling_options());
 107          $mform->addHelpButton('overduehandling', 'overduehandling', 'quiz');
 108          // TODO Formslib does OR logic on disableif, and we need AND logic here.
 109          // $mform->disabledIf('overduehandling', 'timelimit', 'eq', 0);
 110          // $mform->disabledIf('overduehandling', 'timeclose', 'eq', 0);
 111  
 112          // Grace period time.
 113          $mform->addElement('duration', 'graceperiod', get_string('graceperiod', 'quiz'),
 114                  ['optional' => true]);
 115          $mform->addHelpButton('graceperiod', 'graceperiod', 'quiz');
 116          $mform->hideIf('graceperiod', 'overduehandling', 'neq', 'graceperiod');
 117  
 118          // -------------------------------------------------------------------------------
 119          // Grade settings.
 120          $this->standard_grading_coursemodule_elements();
 121  
 122          $mform->removeElement('grade');
 123          if (property_exists($this->current, 'grade')) {
 124              $currentgrade = $this->current->grade;
 125          } else {
 126              $currentgrade = $quizconfig->maximumgrade;
 127          }
 128          $mform->addElement('hidden', 'grade', $currentgrade);
 129          $mform->setType('grade', PARAM_FLOAT);
 130  
 131          // Number of attempts.
 132          $attemptoptions = ['0' => get_string('unlimited')];
 133          for ($i = 1; $i <= QUIZ_MAX_ATTEMPT_OPTION; $i++) {
 134              $attemptoptions[$i] = $i;
 135          }
 136          $mform->addElement('select', 'attempts', get_string('attemptsallowed', 'quiz'),
 137                  $attemptoptions);
 138  
 139          // Grading method.
 140          $mform->addElement('select', 'grademethod', get_string('grademethod', 'quiz'),
 141                  quiz_get_grading_options());
 142          $mform->addHelpButton('grademethod', 'grademethod', 'quiz');
 143          if ($this->get_max_attempts_for_any_override() < 2) {
 144              $mform->hideIf('grademethod', 'attempts', 'eq', 1);
 145          }
 146  
 147          // -------------------------------------------------------------------------------
 148          $mform->addElement('header', 'layouthdr', get_string('layout', 'quiz'));
 149  
 150          $pagegroup = [];
 151          $pagegroup[] = $mform->createElement('select', 'questionsperpage',
 152                  get_string('newpage', 'quiz'), quiz_questions_per_page_options(), ['id' => 'id_questionsperpage']);
 153          $mform->setDefault('questionsperpage', $quizconfig->questionsperpage);
 154  
 155          if (!empty($this->_cm) && !quiz_has_attempts($this->_cm->instance)) {
 156              $pagegroup[] = $mform->createElement('checkbox', 'repaginatenow', '',
 157                      get_string('repaginatenow', 'quiz'), ['id' => 'id_repaginatenow']);
 158          }
 159  
 160          $mform->addGroup($pagegroup, 'questionsperpagegrp',
 161                  get_string('newpage', 'quiz'), null, false);
 162          $mform->addHelpButton('questionsperpagegrp', 'newpage', 'quiz');
 163          $mform->setAdvanced('questionsperpagegrp', $quizconfig->questionsperpage_adv);
 164  
 165          // Navigation method.
 166          $mform->addElement('select', 'navmethod', get_string('navmethod', 'quiz'),
 167                  quiz_get_navigation_options());
 168          $mform->addHelpButton('navmethod', 'navmethod', 'quiz');
 169  
 170          // -------------------------------------------------------------------------------
 171          $mform->addElement('header', 'interactionhdr', get_string('questionbehaviour', 'quiz'));
 172  
 173          // Shuffle within questions.
 174          $mform->addElement('selectyesno', 'shuffleanswers', get_string('shufflewithin', 'quiz'));
 175          $mform->addHelpButton('shuffleanswers', 'shufflewithin', 'quiz');
 176  
 177          // How questions behave (question behaviour).
 178          if (!empty($this->current->preferredbehaviour)) {
 179              $currentbehaviour = $this->current->preferredbehaviour;
 180          } else {
 181              $currentbehaviour = '';
 182          }
 183          $behaviours = question_engine::get_behaviour_options($currentbehaviour);
 184          $mform->addElement('select', 'preferredbehaviour',
 185                  get_string('howquestionsbehave', 'question'), $behaviours);
 186          $mform->addHelpButton('preferredbehaviour', 'howquestionsbehave', 'question');
 187  
 188          // Can redo completed questions.
 189          $redochoices = [0 => get_string('no'), 1 => get_string('canredoquestionsyes', 'quiz')];
 190          $mform->addElement('select', 'canredoquestions', get_string('canredoquestions', 'quiz'), $redochoices);
 191          $mform->addHelpButton('canredoquestions', 'canredoquestions', 'quiz');
 192          foreach ($behaviours as $behaviour => $notused) {
 193              if (!question_engine::can_questions_finish_during_the_attempt($behaviour)) {
 194                  $mform->hideIf('canredoquestions', 'preferredbehaviour', 'eq', $behaviour);
 195              }
 196          }
 197  
 198          // Each attempt builds on last.
 199          $mform->addElement('selectyesno', 'attemptonlast',
 200                  get_string('eachattemptbuildsonthelast', 'quiz'));
 201          $mform->addHelpButton('attemptonlast', 'eachattemptbuildsonthelast', 'quiz');
 202          if ($this->get_max_attempts_for_any_override() < 2) {
 203              $mform->hideIf('attemptonlast', 'attempts', 'eq', 1);
 204          }
 205  
 206          // -------------------------------------------------------------------------------
 207          $mform->addElement('header', 'reviewoptionshdr',
 208                  get_string('reviewoptionsheading', 'quiz'));
 209          $mform->addHelpButton('reviewoptionshdr', 'reviewoptionsheading', 'quiz');
 210  
 211          // Review options.
 212          $this->add_review_options_group($mform, $quizconfig, 'during',
 213                  display_options::DURING, true);
 214          $this->add_review_options_group($mform, $quizconfig, 'immediately',
 215                  display_options::IMMEDIATELY_AFTER);
 216          $this->add_review_options_group($mform, $quizconfig, 'open',
 217                  display_options::LATER_WHILE_OPEN);
 218          $this->add_review_options_group($mform, $quizconfig, 'closed',
 219                  display_options::AFTER_CLOSE);
 220  
 221          foreach ($behaviours as $behaviour => $notused) {
 222              $unusedoptions = question_engine::get_behaviour_unused_display_options($behaviour);
 223              foreach ($unusedoptions as $unusedoption) {
 224                  $mform->disabledIf($unusedoption . 'during', 'preferredbehaviour',
 225                          'eq', $behaviour);
 226              }
 227          }
 228          $mform->disabledIf('attemptduring', 'preferredbehaviour',
 229                  'neq', 'wontmatch');
 230          $mform->disabledIf('overallfeedbackduring', 'preferredbehaviour',
 231                  'neq', 'wontmatch');
 232          foreach (self::$reviewfields as $field => $notused) {
 233              $mform->disabledIf($field . 'closed', 'timeclose[enabled]');
 234          }
 235  
 236          // -------------------------------------------------------------------------------
 237          $mform->addElement('header', 'display', get_string('appearance'));
 238  
 239          // Show user picture.
 240          $mform->addElement('select', 'showuserpicture', get_string('showuserpicture', 'quiz'),
 241                  quiz_get_user_image_options());
 242          $mform->addHelpButton('showuserpicture', 'showuserpicture', 'quiz');
 243  
 244          // Overall decimal points.
 245          $options = [];
 246          for ($i = 0; $i <= QUIZ_MAX_DECIMAL_OPTION; $i++) {
 247              $options[$i] = $i;
 248          }
 249          $mform->addElement('select', 'decimalpoints', get_string('decimalplaces', 'quiz'),
 250                  $options);
 251          $mform->addHelpButton('decimalpoints', 'decimalplaces', 'quiz');
 252  
 253          // Question decimal points.
 254          $options = [-1 => get_string('sameasoverall', 'quiz')];
 255          for ($i = 0; $i <= QUIZ_MAX_Q_DECIMAL_OPTION; $i++) {
 256              $options[$i] = $i;
 257          }
 258          $mform->addElement('select', 'questiondecimalpoints',
 259                  get_string('decimalplacesquestion', 'quiz'), $options);
 260          $mform->addHelpButton('questiondecimalpoints', 'decimalplacesquestion', 'quiz');
 261  
 262          // Show blocks during quiz attempt.
 263          $mform->addElement('selectyesno', 'showblocks', get_string('showblocks', 'quiz'));
 264          $mform->addHelpButton('showblocks', 'showblocks', 'quiz');
 265  
 266          // -------------------------------------------------------------------------------
 267          $mform->addElement('header', 'security', get_string('extraattemptrestrictions', 'quiz'));
 268  
 269          // Require password to begin quiz attempt.
 270          $mform->addElement('passwordunmask', 'quizpassword', get_string('requirepassword', 'quiz'));
 271          $mform->setType('quizpassword', PARAM_TEXT);
 272          $mform->addHelpButton('quizpassword', 'requirepassword', 'quiz');
 273  
 274          // IP address.
 275          $mform->addElement('text', 'subnet', get_string('requiresubnet', 'quiz'));
 276          $mform->setType('subnet', PARAM_TEXT);
 277          $mform->addHelpButton('subnet', 'requiresubnet', 'quiz');
 278  
 279          // Enforced time delay between quiz attempts.
 280          $mform->addElement('duration', 'delay1', get_string('delay1st2nd', 'quiz'),
 281                  ['optional' => true]);
 282          $mform->addHelpButton('delay1', 'delay1st2nd', 'quiz');
 283          if ($this->get_max_attempts_for_any_override() < 2) {
 284              $mform->hideIf('delay1', 'attempts', 'eq', 1);
 285          }
 286  
 287          $mform->addElement('duration', 'delay2', get_string('delaylater', 'quiz'),
 288                  ['optional' => true]);
 289          $mform->addHelpButton('delay2', 'delaylater', 'quiz');
 290          if ($this->get_max_attempts_for_any_override() < 3) {
 291              $mform->hideIf('delay2', 'attempts', 'eq', 1);
 292              $mform->hideIf('delay2', 'attempts', 'eq', 2);
 293          }
 294  
 295          // Browser security choices.
 296          $mform->addElement('select', 'browsersecurity', get_string('browsersecurity', 'quiz'),
 297                  access_manager::get_browser_security_choices());
 298          $mform->addHelpButton('browsersecurity', 'browsersecurity', 'quiz');
 299  
 300          // Any other rule plugins.
 301          access_manager::add_settings_form_fields($this, $mform);
 302  
 303          // -------------------------------------------------------------------------------
 304          $mform->addElement('header', 'overallfeedbackhdr', get_string('overallfeedback', 'quiz'));
 305          $mform->addHelpButton('overallfeedbackhdr', 'overallfeedback', 'quiz');
 306  
 307          if (isset($this->current->grade)) {
 308              $needwarning = $this->current->grade === 0;
 309          } else {
 310              $needwarning = $quizconfig->maximumgrade == 0;
 311          }
 312          if ($needwarning) {
 313              $mform->addElement('static', 'nogradewarning', '',
 314                      get_string('nogradewarning', 'quiz'));
 315          }
 316  
 317          $mform->addElement('static', 'gradeboundarystatic1',
 318                  get_string('gradeboundary', 'quiz'), '100%');
 319  
 320          $repeatarray = [];
 321          $repeatedoptions = [];
 322          $repeatarray[] = $mform->createElement('editor', 'feedbacktext',
 323                  get_string('feedback', 'quiz'), ['rows' => 3], ['maxfiles' => EDITOR_UNLIMITED_FILES,
 324                          'noclean' => true, 'context' => $this->context]);
 325          $repeatarray[] = $mform->createElement('text', 'feedbackboundaries',
 326                  get_string('gradeboundary', 'quiz'), ['size' => 10]);
 327          $repeatedoptions['feedbacktext']['type'] = PARAM_RAW;
 328          $repeatedoptions['feedbackboundaries']['type'] = PARAM_RAW;
 329  
 330          if (!empty($this->_instance)) {
 331              $this->_feedbacks = $DB->get_records('quiz_feedback',
 332                      ['quizid' => $this->_instance], 'mingrade DESC');
 333              $numfeedbacks = count($this->_feedbacks);
 334          } else {
 335              $this->_feedbacks = [];
 336              $numfeedbacks = $quizconfig->initialnumfeedbacks;
 337          }
 338          $numfeedbacks = max($numfeedbacks, 1);
 339  
 340          $nextel = $this->repeat_elements($repeatarray, $numfeedbacks - 1,
 341                  $repeatedoptions, 'boundary_repeats', 'boundary_add_fields', 3,
 342                  get_string('addmoreoverallfeedbacks', 'quiz'), true);
 343  
 344          // Put some extra elements in before the button.
 345          $mform->insertElementBefore($mform->createElement('editor',
 346                  "feedbacktext[$nextel]", get_string('feedback', 'quiz'), ['rows' => 3],
 347                  ['maxfiles' => EDITOR_UNLIMITED_FILES, 'noclean' => true,
 348                        'context' => $this->context]),
 349                  'boundary_add_fields');
 350          $mform->insertElementBefore($mform->createElement('static',
 351                  'gradeboundarystatic2', get_string('gradeboundary', 'quiz'), '0%'),
 352                  'boundary_add_fields');
 353  
 354          // Add the disabledif rules. We cannot do this using the $repeatoptions parameter to
 355          // repeat_elements because we don't want to dissable the first feedbacktext.
 356          for ($i = 0; $i < $nextel; $i++) {
 357              $mform->disabledIf('feedbackboundaries[' . $i . ']', 'grade', 'eq', 0);
 358              $mform->disabledIf('feedbacktext[' . ($i + 1) . ']', 'grade', 'eq', 0);
 359          }
 360  
 361          // -------------------------------------------------------------------------------
 362          $this->standard_coursemodule_elements();
 363  
 364          // Check and act on whether setting outcomes is considered an advanced setting.
 365          $mform->setAdvanced('modoutcomes', !empty($quizconfig->outcomes_adv));
 366  
 367          // The standard_coursemodule_elements method sets this to 100, but the
 368          // quiz has its own setting, so use that.
 369          $mform->setDefault('grade', $quizconfig->maximumgrade);
 370  
 371          // -------------------------------------------------------------------------------
 372          $this->apply_admin_defaults();
 373          $this->add_action_buttons();
 374  
 375          $PAGE->requires->yui_module('moodle-mod_quiz-modform', 'M.mod_quiz.modform.init');
 376      }
 377  
 378      protected function add_review_options_group($mform, $quizconfig, $whenname,
 379              $when, $withhelp = false) {
 380          global $OUTPUT;
 381  
 382          $group = [];
 383          foreach (self::$reviewfields as $field => $string) {
 384              list($identifier, $component) = $string;
 385  
 386              $label = get_string($identifier, $component);
 387              $group[] = $mform->createElement('html', html_writer::start_div('review_option_item'));
 388              $el = $mform->createElement('checkbox', $field . $whenname, '', $label);
 389              if ($withhelp) {
 390                  $el->_helpbutton = $OUTPUT->render(new help_icon($identifier, $component));
 391              }
 392              $group[] = $el;
 393              $group[] = $mform->createElement('html', html_writer::end_div());
 394          }
 395          $mform->addGroup($group, $whenname . 'optionsgrp',
 396                  get_string('review' . $whenname, 'quiz'), null, false);
 397  
 398          foreach (self::$reviewfields as $field => $notused) {
 399              $cfgfield = 'review' . $field;
 400              if ($quizconfig->$cfgfield & $when) {
 401                  $mform->setDefault($field . $whenname, 1);
 402              } else {
 403                  $mform->setDefault($field . $whenname, 0);
 404              }
 405          }
 406  
 407          $mform->disabledIf('marks' . $whenname, 'maxmarks' . $whenname);
 408          if ($whenname != 'during') {
 409              $mform->disabledIf('correctness' . $whenname, 'attempt' . $whenname);
 410              $mform->disabledIf('specificfeedback' . $whenname, 'attempt' . $whenname);
 411              $mform->disabledIf('generalfeedback' . $whenname, 'attempt' . $whenname);
 412              $mform->disabledIf('rightanswer' . $whenname, 'attempt' . $whenname);
 413          }
 414      }
 415  
 416      protected function preprocessing_review_settings(&$toform, $whenname, $when) {
 417          foreach (self::$reviewfields as $field => $notused) {
 418              $fieldname = 'review' . $field;
 419              if (array_key_exists($fieldname, $toform)) {
 420                  $toform[$field . $whenname] = $toform[$fieldname] & $when;
 421              }
 422          }
 423      }
 424  
 425      public function data_preprocessing(&$toform) {
 426          if (isset($toform['grade'])) {
 427              // Convert to a real number, so we don't get 0.0000.
 428              $toform['grade'] = $toform['grade'] + 0;
 429          }
 430  
 431          if (count($this->_feedbacks)) {
 432              $key = 0;
 433              foreach ($this->_feedbacks as $feedback) {
 434                  $draftid = file_get_submitted_draft_itemid('feedbacktext['.$key.']');
 435                  $toform['feedbacktext['.$key.']']['text'] = file_prepare_draft_area(
 436                      $draftid,               // Draftid.
 437                      $this->context->id,     // Context.
 438                      'mod_quiz',             // Component.
 439                      'feedback',             // Filarea.
 440                      !empty($feedback->id) ? (int) $feedback->id : null, // Itemid.
 441                      null,
 442                      $feedback->feedbacktext // Text.
 443                  );
 444                  $toform['feedbacktext['.$key.']']['format'] = $feedback->feedbacktextformat;
 445                  $toform['feedbacktext['.$key.']']['itemid'] = $draftid;
 446  
 447                  if ($toform['grade'] == 0) {
 448                      // When a quiz is un-graded, there can only be one lot of
 449                      // feedback. If the quiz previously had a maximum grade and
 450                      // several lots of feedback, we must now avoid putting text
 451                      // into input boxes that are disabled, but which the
 452                      // validation will insist are blank.
 453                      break;
 454                  }
 455  
 456                  if ($feedback->mingrade > 0) {
 457                      $toform['feedbackboundaries['.$key.']'] =
 458                              round(100.0 * $feedback->mingrade / $toform['grade'], 6) . '%';
 459                  }
 460                  $key++;
 461              }
 462          }
 463  
 464          if (isset($toform['timelimit'])) {
 465              $toform['timelimitenable'] = $toform['timelimit'] > 0;
 466          }
 467  
 468          $this->preprocessing_review_settings($toform, 'during',
 469                  display_options::DURING);
 470          $this->preprocessing_review_settings($toform, 'immediately',
 471                  display_options::IMMEDIATELY_AFTER);
 472          $this->preprocessing_review_settings($toform, 'open',
 473                  display_options::LATER_WHILE_OPEN);
 474          $this->preprocessing_review_settings($toform, 'closed',
 475                  display_options::AFTER_CLOSE);
 476          $toform['attemptduring'] = true;
 477          $toform['overallfeedbackduring'] = false;
 478  
 479          // Password field - different in form to stop browsers that remember
 480          // passwords from getting confused.
 481          if (isset($toform['password'])) {
 482              $toform['quizpassword'] = $toform['password'];
 483              unset($toform['password']);
 484          }
 485  
 486          // Load any settings belonging to the access rules.
 487          if (!empty($toform['instance'])) {
 488              $accesssettings = access_manager::load_settings($toform['instance']);
 489              foreach ($accesssettings as $name => $value) {
 490                  $toform[$name] = $value;
 491              }
 492          }
 493  
 494          $suffix = $this->get_suffix();
 495          $completionminattemptsel = 'completionminattempts' . $suffix;
 496          if (empty($toform[$completionminattemptsel])) {
 497              $toform[$completionminattemptsel] = 1;
 498          } else {
 499              $completionminattemptsenabledel = 'completionminattemptsenabled' . $suffix;
 500              $toform[$completionminattemptsenabledel] = $toform[$completionminattemptsel] > 0;
 501          }
 502      }
 503  
 504      /**
 505       * Allows module to modify the data returned by form get_data().
 506       * This method is also called in the bulk activity completion form.
 507       *
 508       * Only available on moodleform_mod.
 509       *
 510       * @param stdClass $data the form data to be modified.
 511       */
 512      public function data_postprocessing($data) {
 513          parent::data_postprocessing($data);
 514          if (!empty($data->completionunlocked)) {
 515              // Turn off completion settings if the checkboxes aren't ticked.
 516              $suffix = $this->get_suffix();
 517              $completion = $data->{'completion' . $suffix};
 518              $autocompletion = !empty($completion) && $completion == COMPLETION_TRACKING_AUTOMATIC;
 519              if (empty($data->{'completionminattemptsenabled' . $suffix}) || !$autocompletion) {
 520                  $data->{'completionminattempts' . $suffix} = 0;
 521              }
 522          }
 523      }
 524  
 525      public function validation($data, $files) {
 526          $errors = parent::validation($data, $files);
 527  
 528          // Check open and close times are consistent.
 529          if ($data['timeopen'] != 0 && $data['timeclose'] != 0 &&
 530                  $data['timeclose'] < $data['timeopen']) {
 531              $errors['timeclose'] = get_string('closebeforeopen', 'quiz');
 532          }
 533  
 534          // Check that the grace period is not too short.
 535          if ($data['overduehandling'] == 'graceperiod') {
 536              $graceperiodmin = get_config('quiz', 'graceperiodmin');
 537              if ($data['graceperiod'] <= $graceperiodmin) {
 538                  $errors['graceperiod'] = get_string('graceperiodtoosmall', 'quiz', format_time($graceperiodmin));
 539              }
 540          }
 541  
 542          $suffix = $this->get_suffix();
 543          $completionminattemptsel = 'completionminattempts' . $suffix;
 544          if (!empty($data[$completionminattemptsel])) {
 545              if ($data['attempts'] > 0 && $data[$completionminattemptsel] > $data['attempts']) {
 546                  $completionminattemptsgroupel = 'completionminattemptsgroup' . $suffix;
 547                  $errors[$completionminattemptsgroupel] = get_string('completionminattemptserror', 'quiz');
 548              }
 549          }
 550  
 551          // Check the boundary value is a number or a percentage, and in range.
 552          $i = 0;
 553          while (!empty($data['feedbackboundaries'][$i] )) {
 554              $boundary = trim($data['feedbackboundaries'][$i]);
 555              if (strlen($boundary) > 0) {
 556                  if ($boundary[strlen($boundary) - 1] == '%') {
 557                      $boundary = trim(substr($boundary, 0, -1));
 558                      if (is_numeric($boundary)) {
 559                          $boundary = $boundary * $data['grade'] / 100.0;
 560                      } else {
 561                          $errors["feedbackboundaries[$i]"] =
 562                                  get_string('feedbackerrorboundaryformat', 'quiz', $i + 1);
 563                      }
 564                  } else if (!is_numeric($boundary)) {
 565                      $errors["feedbackboundaries[$i]"] =
 566                              get_string('feedbackerrorboundaryformat', 'quiz', $i + 1);
 567                  }
 568              }
 569              if (is_numeric($boundary) && $boundary <= 0 || $boundary >= $data['grade'] ) {
 570                  $errors["feedbackboundaries[$i]"] =
 571                          get_string('feedbackerrorboundaryoutofrange', 'quiz', $i + 1);
 572              }
 573              if (is_numeric($boundary) && $i > 0 &&
 574                      $boundary >= $data['feedbackboundaries'][$i - 1]) {
 575                  $errors["feedbackboundaries[$i]"] =
 576                          get_string('feedbackerrororder', 'quiz', $i + 1);
 577              }
 578              $data['feedbackboundaries'][$i] = $boundary;
 579              $i += 1;
 580          }
 581          $numboundaries = $i;
 582  
 583          // Check there is nothing in the remaining unused fields.
 584          if (!empty($data['feedbackboundaries'])) {
 585              for ($i = $numboundaries; $i < count($data['feedbackboundaries']); $i += 1) {
 586                  if (!empty($data['feedbackboundaries'][$i] ) &&
 587                          trim($data['feedbackboundaries'][$i] ) != '') {
 588                      $errors["feedbackboundaries[$i]"] =
 589                              get_string('feedbackerrorjunkinboundary', 'quiz', $i + 1);
 590                  }
 591              }
 592          }
 593          for ($i = $numboundaries + 1; $i < count($data['feedbacktext']); $i += 1) {
 594              if (!empty($data['feedbacktext'][$i]['text']) &&
 595                      trim($data['feedbacktext'][$i]['text'] ) != '') {
 596                  $errors["feedbacktext[$i]"] =
 597                          get_string('feedbackerrorjunkinfeedback', 'quiz', $i + 1);
 598              }
 599          }
 600  
 601          // If CBM is involved, don't show the warning for grade to pass being larger than the maximum grade.
 602          if (($data['preferredbehaviour'] == 'deferredcbm') OR ($data['preferredbehaviour'] == 'immediatecbm')) {
 603              unset($errors['gradepass']);
 604          }
 605          // Any other rule plugins.
 606          $errors = access_manager::validate_settings_form_fields($errors, $data, $files, $this);
 607  
 608          return $errors;
 609      }
 610  
 611      /**
 612       * Display module-specific activity completion rules.
 613       * Part of the API defined by moodleform_mod
 614       * @return array Array of string IDs of added items, empty array if none
 615       */
 616      public function add_completion_rules() {
 617          $mform = $this->_form;
 618          $suffix = $this->get_suffix();
 619  
 620          $group = [];
 621          $completionminattemptsenabledel = 'completionminattemptsenabled' . $suffix;
 622          $group[] = $mform->createElement(
 623              'checkbox',
 624              $completionminattemptsenabledel,
 625              '',
 626              get_string('completionminattempts', 'quiz')
 627          );
 628          $completionminattemptsel = 'completionminattempts' . $suffix;
 629          $group[] = $mform->createElement('text', $completionminattemptsel, '', ['size' => 3]);
 630          $mform->setType($completionminattemptsel, PARAM_INT);
 631          $completionminattemptsgroupel = 'completionminattemptsgroup' . $suffix;
 632          $mform->addGroup($group, $completionminattemptsgroupel, '', ' ', false);
 633          $mform->hideIf($completionminattemptsel, $completionminattemptsenabledel, 'notchecked');
 634  
 635          return [$completionminattemptsgroupel];
 636      }
 637  
 638      /**
 639       * Add completion grading elements to the form and return the list of element ids.
 640       *
 641       * @return array Array of string IDs of added items, empty array if none
 642       */
 643      public function add_completiongrade_rules(): array {
 644          $mform = $this->_form;
 645          $suffix = $this->get_suffix();
 646  
 647          $completionattemptsexhaustedel = 'completionattemptsexhausted' . $suffix;
 648          $mform->addElement(
 649              'advcheckbox',
 650              $completionattemptsexhaustedel,
 651              null,
 652              get_string('completionattemptsexhausted', 'quiz'),
 653              ['group' => 'cattempts', 'parentclass' => 'ml-4']
 654          );
 655          $completionpassgradeel = 'completionpassgrade' . $suffix;
 656          $mform->hideIf($completionattemptsexhaustedel, $completionpassgradeel, 'notchecked');
 657          $mform->hideIf($completionattemptsexhaustedel, $completionpassgradeel, 'notchecked');
 658  
 659          return [$completionattemptsexhaustedel];
 660      }
 661  
 662      /**
 663       * Called during validation. Indicates whether a module-specific completion rule is selected.
 664       *
 665       * @param array $data Input data (not yet validated)
 666       * @return bool True if one or more rules is enabled, false if none are.
 667       */
 668      public function completion_rule_enabled($data) {
 669          $suffix = $this->get_suffix();
 670          return  !empty($data['completionattemptsexhausted' . $suffix]) ||
 671                  !empty($data['completionminattemptsenabled' . $suffix]);
 672      }
 673  
 674      /**
 675       * Get the maximum number of attempts that anyone might have due to a user
 676       * or group override. Used to decide whether disabledIf rules should be applied.
 677       * @return int the number of attempts allowed. For the purpose of this method,
 678       * unlimited is returned as 1000, not 0.
 679       */
 680      public function get_max_attempts_for_any_override() {
 681          global $DB;
 682  
 683          if (empty($this->_instance)) {
 684              // Quiz not created yet, so no overrides.
 685              return 1;
 686          }
 687  
 688          if ($this->maxattemptsanyoverride === null) {
 689              $this->maxattemptsanyoverride = $DB->get_field_sql("
 690                      SELECT MAX(CASE WHEN attempts = 0 THEN 1000 ELSE attempts END)
 691                        FROM {quiz_overrides}
 692                       WHERE quiz = ?",
 693                      [$this->_instance]);
 694              if ($this->maxattemptsanyoverride < 1) {
 695                  // This happens when no override alters the number of attempts.
 696                  $this->maxattemptsanyoverride = 1;
 697              }
 698          }
 699  
 700          return $this->maxattemptsanyoverride;
 701      }
 702  }