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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body