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.
   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  namespace core_completion\form;
  18  
  19  use core_grades\component_gradeitems;
  20  use cm_info;
  21  
  22  /**
  23   * Completion trait helper, with methods to add completion elements and validate them.
  24   *
  25   * @package    core_completion
  26   * @since      Moodle 4.3
  27   * @copyright  2023 Sara Arjona (sara@moodle.com)
  28   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  29   */
  30  trait form_trait {
  31  
  32      /** @var string The suffix to be added to the completion elements when creating them (for example, 'completion_assign'). */
  33      protected $suffix = '';
  34  
  35      /**
  36       * Called during validation.
  37       * Override this method to indicate, based on the data, whether a custom completion rule is selected or not.
  38       *
  39       * @param array $data Input data (not yet validated)
  40       * @return bool True if one or more rules are enabled; false if none are.
  41       */
  42      abstract protected function completion_rule_enabled($data);
  43  
  44      /**
  45       * Add completion elements to the form and return the list of element ids.
  46       *
  47       * @return array Array of string IDs of added items, empty array if none
  48       */
  49      abstract protected function add_completion_rules();
  50  
  51      /**
  52       * Get the form associated to this class, where the completion elements will be added.
  53       * This method must be overriden by the class using this trait if it doesn't include a _form property.
  54       *
  55       * @return \MoodleQuickForm
  56       * @throws \coding_exception If the class does not have a _form property.
  57       */
  58      protected function get_form(): \MoodleQuickForm {
  59          if (property_exists($this, '_form')) {
  60              return $this->_form;
  61          }
  62  
  63          throw new \coding_exception('This class does not have a _form property. Please, add it or override the get_form() method.');
  64      }
  65  
  66      /**
  67       * Set the suffix to be added to the completion elements when creating them (for example, 'completion_assign').
  68       *
  69       * @param string $suffix
  70       */
  71      public function set_suffix(string $suffix): void {
  72          $this->suffix = $suffix;
  73      }
  74  
  75      /**
  76       * Get the suffix to be added to the completion elements when creating them (for example, 'completion_assign').
  77       *
  78       * @return string The suffix
  79       */
  80      public function get_suffix(): string {
  81          return $this->suffix;
  82      }
  83  
  84      /**
  85       * Add completion elements to the form.
  86       *
  87       * @param string|null $modname The module name (for example, 'assign'). If null and form is moodleform_mod, the parameters are
  88       *                             overriden with the expected values from the form.
  89       * @param bool $supportviews True if the module supports views and false otherwise.
  90       * @param bool $supportgrades True if the module supports grades and false otherwise.
  91       * @param bool $rating True if the rating feature is enabled and false otherwise.
  92       * @param int|null $courseid Course where to add completion elements.
  93       * @throws \coding_exception If the form is not moodleform_mod and $modname is null.
  94       */
  95      protected function add_completion_elements(
  96          string $modname = null,
  97          bool $supportviews = false,
  98          bool $supportgrades = false,
  99          bool $rating = false,
 100          ?int $courseid = null
 101      ): void {
 102          global $SITE;
 103  
 104          $mform = $this->get_form();
 105          if ($modname === null) {
 106              if ($this instanceof \moodleform_mod) {
 107                  // By default, all the modules can be initiatized with the same parameters.
 108                  $modname = $this->_modname;
 109                  $supportviews = plugin_supports('mod', $modname, FEATURE_COMPLETION_TRACKS_VIEWS, false);
 110                  $supportgrades = plugin_supports('mod', $modname, FEATURE_GRADE_HAS_GRADE, false);
 111                  $rating = $this->_features->rating;
 112              } else {
 113                  throw new \coding_exception('You must specify the modname parameter if you are not using a moodleform_mod.');
 114              }
 115          }
 116  
 117          // Unlock button if people have completed it. The button will be removed later in definition_after_data if they haven't.
 118          // The unlock buttons don't need suffix because they are only displayed in the module settings page.
 119          $mform->addElement('submit', 'unlockcompletion', get_string('unlockcompletion', 'completion'));
 120          $mform->registerNoSubmitButton('unlockcompletion');
 121          $mform->addElement('hidden', 'completionunlocked', 0);
 122          $mform->setType('completionunlocked', PARAM_INT);
 123  
 124          $trackingdefault = COMPLETION_TRACKING_NONE;
 125  
 126          // Get the sufix to add to the completion elements name.
 127          $suffix = $this->get_suffix();
 128  
 129          $completionel = 'completion' . $suffix;
 130          $mform->addElement(
 131              'radio',
 132              $completionel,
 133              '',
 134              get_string('completion_none', 'completion'),
 135              COMPLETION_TRACKING_NONE,
 136              ['class' => 'left-indented']
 137          );
 138          $mform->addElement(
 139              'radio',
 140              $completionel,
 141              '',
 142              get_string('completion_manual', 'completion'),
 143              COMPLETION_TRACKING_MANUAL,
 144              ['class' => 'left-indented']
 145          );
 146  
 147          $allconditionsel = 'allconditions' . $suffix;
 148          $allconditions = $mform->createElement(
 149              'static',
 150              $allconditionsel,
 151              '',
 152              get_string('allconditions', 'completion'));
 153  
 154          $conditionsgroupel = 'conditionsgroup' . $suffix;
 155          $mform->addGroup([$allconditions], $conditionsgroupel, '', null, false);
 156          $mform->hideIf($conditionsgroupel, $completionel, 'ne', COMPLETION_TRACKING_AUTOMATIC);
 157  
 158          $mform->setType($completionel, PARAM_INT);
 159          $mform->setDefault($completionel, COMPLETION_TRACKING_NONE);
 160  
 161          // Automatic completion once you view it.
 162          if ($supportviews) {
 163              $completionviewel = 'completionview' . $suffix;
 164              $mform->addElement(
 165                  'checkbox',
 166                  $completionviewel,
 167                  '',
 168                  get_string('completionview_desc', 'completion')
 169              );
 170              $mform->hideIf($completionviewel, $completionel, 'ne', COMPLETION_TRACKING_AUTOMATIC);
 171              // Check by default if automatic completion tracking is set.
 172              if ($trackingdefault == COMPLETION_TRACKING_AUTOMATIC) {
 173                  $mform->setDefault($completionviewel, 1);
 174              }
 175          }
 176  
 177          // Automatic completion according to module-specific rules.
 178          $customcompletionelements = $this->add_completion_rules();
 179          if (property_exists($this, '_customcompletionelements')) {
 180              $this->_customcompletionelements = $customcompletionelements;
 181          }
 182  
 183          if ($customcompletionelements !== null) {
 184              foreach ($customcompletionelements as $element) {
 185                  $mform->hideIf($element, $completionel, 'ne', COMPLETION_TRACKING_AUTOMATIC);
 186              }
 187          }
 188  
 189          // If the activity supports grading, the grade elements must be added.
 190          if ($supportgrades) {
 191              $this->add_completiongrade_elements($modname, $rating);
 192          }
 193  
 194          $autocompletionpossible = $supportviews || $supportgrades || (count($customcompletionelements) > 0);
 195  
 196          // Automatic option only appears if possible.
 197          if ($autocompletionpossible) {
 198              $automatic = $mform->createElement(
 199                  'radio',
 200                  $completionel,
 201                  '',
 202                  get_string('completion_automatic', 'completion'),
 203                  COMPLETION_TRACKING_AUTOMATIC,
 204                  ['class' => 'left-indented']
 205              );
 206              $mform->insertElementBefore($automatic, $conditionsgroupel);
 207          }
 208  
 209          // Completion expected at particular date? (For progress tracking).
 210          // We don't show completion expected at site level default completion.
 211          if ($courseid != $SITE->id) {
 212              $completionexpectedel = 'completionexpected' . $suffix;
 213              $mform->addElement('date_time_selector', $completionexpectedel, get_string('completionexpected', 'completion'),
 214                  ['optional' => true]);
 215              $a = get_string('pluginname', $modname);
 216              $mform->addHelpButton($completionexpectedel, 'completionexpected', 'completion', '', false, $a);
 217              $mform->hideIf($completionexpectedel, $completionel, 'eq', COMPLETION_TRACKING_NONE);
 218          }
 219      }
 220  
 221      /**
 222       * Add completion grade elements to the form.
 223       *
 224       * @param string $modname The name of the module (for example, 'assign').
 225       * @param bool $rating True if the rating feature is enabled and false otherwise.
 226       */
 227      protected function add_completiongrade_elements(
 228          string $modname,
 229          bool $rating = false
 230      ): void {
 231          $mform = $this->get_form();
 232  
 233          // Get the sufix to add to the completion elements name.
 234          $suffix = $this->get_suffix();
 235  
 236          $completionel = 'completion' . $suffix;
 237          $completionelementexists = $mform->elementExists($completionel);
 238          $component = "mod_{$modname}";
 239          $itemnames = component_gradeitems::get_itemname_mapping_for_component($component);
 240  
 241          $indentation = ['parentclass' => 'ml-2'];
 242          $receiveagradeel = 'receiveagrade' . $suffix;
 243          $completionusegradeel = 'completionusegrade' . $suffix;
 244          $completionpassgradeel = 'completionpassgrade' . $suffix;
 245  
 246          if (count($itemnames) === 1) {
 247              // Only one gradeitem in this activity.
 248              // We use the completionusegrade field here.
 249              $mform->addElement(
 250                  'checkbox',
 251                  $completionusegradeel,
 252                  '',
 253                  get_string('completionusegrade_desc', 'completion')
 254              );
 255  
 256              // Complete if the user has reached any grade.
 257              $mform->addElement(
 258                  'radio',
 259                  $completionpassgradeel,
 260                  null,
 261                  get_string('completionanygrade_desc', 'completion'),
 262                  0,
 263                  $indentation
 264              );
 265  
 266              // Complete if the user has reached the pass grade.
 267              $mform->addElement(
 268                  'radio',
 269                  $completionpassgradeel,
 270                  null,
 271                  get_string('completionpassgrade_desc', 'completion'),
 272                  1,
 273                  $indentation
 274              );
 275              $mform->hideIf($completionpassgradeel, $completionusegradeel, 'notchecked');
 276  
 277              if ($completionelementexists) {
 278                  $mform->hideIf($completionpassgradeel, $completionel, 'ne', COMPLETION_TRACKING_AUTOMATIC);
 279                  $mform->hideIf($completionusegradeel, $completionel, 'ne', COMPLETION_TRACKING_AUTOMATIC);
 280              }
 281  
 282              // The disabledIf logic differs between ratings and other grade items due to different field types.
 283              if ($rating) {
 284                  // If using the rating system, there is no grade unless ratings are enabled.
 285                  $mform->hideIf($completionusegradeel, 'assessed', 'eq', 0);
 286                  $mform->hideIf($completionusegradeel, 'assessed', 'eq', 0);
 287              } else {
 288                  // All other field types use the '$gradefieldname' field's modgrade_type.
 289                  $itemnumbers = array_keys($itemnames);
 290                  $itemnumber = array_shift($itemnumbers);
 291                  $gradefieldname = component_gradeitems::get_field_name_for_itemnumber($component, $itemnumber, 'grade');
 292                  $mform->hideIf($completionusegradeel, "{$gradefieldname}[modgrade_type]", 'eq', 'none');
 293                  $mform->hideIf($completionusegradeel, "{$gradefieldname}[modgrade_type]", 'eq', 'none');
 294              }
 295          } else if (count($itemnames) > 1) {
 296              // There are multiple grade items in this activity.
 297              // Show them all.
 298              $options = [];
 299              foreach ($itemnames as $itemnumber => $itemname) {
 300                  $options[$itemnumber] = get_string("grade_{$itemname}_name", $component);
 301              }
 302  
 303              $group = [$mform->createElement(
 304                  'checkbox',
 305                  $completionusegradeel,
 306                  null,
 307                  get_string('completionusegrade_desc', 'completion')
 308              )];
 309              $completiongradeitemnumberel = 'completiongradeitemnumber' . $suffix;
 310              $group[] =& $mform->createElement(
 311                  'select',
 312                  $completiongradeitemnumberel,
 313                  '',
 314                  $options
 315              );
 316              $receiveagradegroupel = 'receiveagradegroup' . $suffix;
 317              $mform->addGroup($group, $receiveagradegroupel, '', [' '], false);
 318              if ($completionelementexists) {
 319                  $mform->hideIf($completionusegradeel, $completionel, 'ne', COMPLETION_TRACKING_AUTOMATIC);
 320                  $mform->hideIf($receiveagradegroupel, $completionel, 'ne', COMPLETION_TRACKING_AUTOMATIC);
 321              }
 322              $mform->hideIf($completiongradeitemnumberel, $completionusegradeel, 'notchecked');
 323  
 324              // Complete if the user has reached any grade.
 325              $mform->addElement(
 326                  'radio',
 327                  $completionpassgradeel,
 328                  null,
 329                  get_string('completionanygrade_desc', 'completion'),
 330                  0,
 331                  $indentation
 332              );
 333              // Complete if the user has reached the pass grade.
 334              $mform->addElement(
 335                  'radio',
 336                  $completionpassgradeel,
 337                  null,
 338                  get_string('completionpassgrade_desc', 'completion'),
 339                  1,
 340                  $indentation
 341              );
 342              $mform->hideIf($completionpassgradeel, $completionusegradeel, 'notchecked');
 343  
 344              if ($completionelementexists) {
 345                  $mform->hideIf($completiongradeitemnumberel, $completionel, 'ne', COMPLETION_TRACKING_AUTOMATIC);
 346                  $mform->hideIf($completionpassgradeel, $completionel, 'ne', COMPLETION_TRACKING_AUTOMATIC);
 347              }
 348          }
 349  
 350          $customgradingelements = $this->add_completiongrade_rules();
 351          if (property_exists($this, '_customcompletionelements')) {
 352              $this->_customcompletionelements = array_merge($this->_customcompletionelements, $customgradingelements);
 353          }
 354          if ($completionelementexists) {
 355              foreach ($customgradingelements as $customgradingelement) {
 356                  $mform->hideIf($customgradingelement, $completionel, 'ne', COMPLETION_TRACKING_AUTOMATIC);
 357              }
 358          }
 359      }
 360  
 361      /**
 362       * Add completion grading elements to the form and return the list of element ids.
 363       *
 364       * @return array Array of string IDs of added items, empty array if none
 365       */
 366      abstract public function add_completiongrade_rules(): array;
 367  
 368      /**
 369       * Perform some extra validation for completion settings.
 370       *
 371       * @param array $data Array of ["fieldname" => value] of submitted data.
 372       * @return array List of ["element_name" => "error_description"] if there are errors or an empty array if everything is OK.
 373       */
 374      protected function validate_completion(array $data): array {
 375          $errors = [];
 376  
 377          // Get the sufix to add to the completion elements name.
 378          $suffix = $this->get_suffix();
 379  
 380          $completionel = 'completion' . $suffix;
 381          // Completion: Don't let them choose automatic completion without turning on some conditions.
 382          $automaticcompletion = array_key_exists($completionel, $data) && $data[$completionel] == COMPLETION_TRACKING_AUTOMATIC;
 383          // Ignore this check when completion settings are locked, as the options are then disabled.
 384          // The unlock buttons don't need suffix because they are only displayed in the module settings page.
 385          $automaticcompletion = $automaticcompletion && !empty($data['completionunlocked']);
 386          if ($automaticcompletion) {
 387              // View to complete.
 388              $completionviewel = 'completionview' . $suffix;
 389              $rulesenabled = !empty($data[$completionviewel]);
 390  
 391              // Use grade to complete (only one grade item).
 392              $completionusegradeel = 'completionusegrade' . $suffix;
 393              $completionpassgradeel = 'completionpassgrade' . $suffix;
 394              $rulesenabled = $rulesenabled || !empty($data[$completionusegradeel]) || !empty($data[$completionpassgradeel]);
 395  
 396              // Use grade to complete (specific grade item).
 397              $completiongradeitemnumberel = 'completiongradeitemnumber' . $suffix;
 398              if (!$rulesenabled && isset($data[$completiongradeitemnumberel])) {
 399                  $rulesenabled = $data[$completiongradeitemnumberel] != '';
 400              }
 401  
 402              // Module-specific completion rules.
 403              $rulesenabled = $rulesenabled || $this->completion_rule_enabled($data);
 404  
 405              if (!$rulesenabled) {
 406                  // No rules are enabled. Can't set automatically completed without rules.
 407                  $errors[$completionel] = get_string('badautocompletion', 'completion');
 408              }
 409          }
 410  
 411          return $errors;
 412      }
 413  
 414      /**
 415       * It should be called from the definition_after_data() to setup the completion settings in the form.
 416       *
 417       * @param cm_info|null $cm The course module associated to this form.
 418       */
 419      protected function definition_after_data_completion(?cm_info $cm = null): void {
 420          global $COURSE, $SITE;
 421          $mform = $this->get_form();
 422  
 423          $completion = new \completion_info($COURSE);
 424          // We use $SITE course for site default activity completion,
 425          // so users could set default values regardless of whether completion is enabled or not.".
 426          if ($completion->is_enabled() || $COURSE->id == $SITE->id) {
 427              $suffix = $this->get_suffix();
 428  
 429              // If anybody has completed the activity, these options will be 'locked'.
 430              // We use $SITE course for site default activity completion, so we don't need any unlock button.
 431              $completedcount = (empty($cm) || $COURSE->id == $SITE->id) ? 0 : $completion->count_user_data($cm);
 432              $freeze = false;
 433              if (!$completedcount) {
 434                  // The unlock buttons don't need suffix because they are only displayed in the module settings page.
 435                  if ($mform->elementExists('unlockcompletion')) {
 436                      $mform->removeElement('unlockcompletion');
 437                  }
 438                  // Automatically set to unlocked. Note: this is necessary in order to make it recalculate completion once
 439                  // the option is changed, maybe someone has completed it now.
 440                  if ($mform->elementExists('completionunlocked')) {
 441                      $mform->getElement('completionunlocked')->setValue(1);
 442                  }
 443              } else {
 444                  // Has the element been unlocked, either by the button being pressed in this request, or the field already
 445                  // being set from a previous one?
 446                  if ($mform->exportValue('unlockcompletion') || $mform->exportValue('completionunlocked')) {
 447                      // Yes, add in warning text and set the hidden variable.
 448                      $completedunlockedel = $mform->createElement(
 449                          'static',
 450                          'completedunlocked',
 451                          get_string('completedunlocked', 'completion'),
 452                          get_string('completedunlockedtext', 'completion')
 453                      );
 454                      $mform->insertElementBefore($completedunlockedel, 'unlockcompletion');
 455                      $mform->removeElement('unlockcompletion');
 456                      $mform->getElement('completionunlocked')->setValue(1);
 457                  } else {
 458                      // No, add in the warning text with the count (now we know it) before the unlock button.
 459                      $completedwarningel = $mform->createElement(
 460                          'static',
 461                          'completedwarning',
 462                          get_string('completedwarning', 'completion'),
 463                          get_string('completedwarningtext', 'completion', $completedcount)
 464                      );
 465                      $mform->insertElementBefore($completedwarningel, 'unlockcompletion');
 466                      $freeze = true;
 467                  }
 468              }
 469  
 470              if ($freeze) {
 471                  $completionel = 'completion' . $suffix;
 472                  $mform->freeze($completionel);
 473                  $completionviewel = 'completionview' . $suffix;
 474                  if ($mform->elementExists($completionviewel)) {
 475                      // Don't use hardFreeze or checkbox value gets lost.
 476                      $mform->freeze($completionviewel);
 477                  }
 478                  $completionusegradeel = 'completionusegrade' . $suffix;
 479                  if ($mform->elementExists($completionusegradeel)) {
 480                      $mform->freeze($completionusegradeel);
 481                  }
 482                  $completionpassgradeel = 'completionpassgrade' . $suffix;
 483                  if ($mform->elementExists($completionpassgradeel)) {
 484                      $mform->freeze($completionpassgradeel);
 485  
 486                      // Has the completion pass grade completion criteria been set? If it has, then we shouldn't change
 487                      // the gradepass field.
 488                      if ($mform->exportValue($completionpassgradeel)) {
 489                          $mform->freeze('gradepass');
 490                      }
 491                  }
 492                  $completiongradeitemnumberel = 'completiongradeitemnumber' . $suffix;
 493                  if ($mform->elementExists($completiongradeitemnumberel)) {
 494                      $mform->freeze($completiongradeitemnumberel);
 495                  }
 496                  if (property_exists($this, '_customcompletionelements')) {
 497                      $mform->freeze($this->_customcompletionelements);
 498                  }
 499              }
 500          }
 501      }
 502  }