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 * Bulk activity completion manager class 19 * 20 * @package core_completion 21 * @category completion 22 * @copyright 2017 Adrian Greeve 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 */ 25 26 namespace core_completion; 27 28 use core\context; 29 use stdClass; 30 use context_course; 31 use cm_info; 32 use tabobject; 33 use lang_string; 34 use moodle_url; 35 defined('MOODLE_INTERNAL') || die; 36 37 /** 38 * Bulk activity completion manager class 39 * 40 * @package core_completion 41 * @category completion 42 * @copyright 2017 Adrian Greeve 43 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 44 */ 45 class manager { 46 47 /** 48 * @var int $courseid the course id. 49 */ 50 protected $courseid; 51 52 /** 53 * manager constructor. 54 * @param int $courseid the course id. 55 */ 56 public function __construct($courseid) { 57 $this->courseid = $courseid; 58 } 59 60 /** 61 * Returns current course context or system level for $SITE courseid. 62 * 63 * @return context The course based on current courseid or system context. 64 */ 65 protected function get_context(): context { 66 global $SITE; 67 68 if ($this->courseid && $this->courseid != $SITE->id) { 69 return context_course::instance($this->courseid); 70 } 71 return \context_system::instance(); 72 } 73 74 /** 75 * Gets the data (context) to be used with the bulkactivitycompletion template. 76 * 77 * @return stdClass data for use with the bulkactivitycompletion template. 78 */ 79 public function get_activities_and_headings() { 80 global $OUTPUT; 81 $moduleinfo = get_fast_modinfo($this->courseid); 82 $sections = $moduleinfo->get_sections(); 83 $data = new stdClass; 84 $data->courseid = $this->courseid; 85 $data->sesskey = sesskey(); 86 $data->helpicon = $OUTPUT->help_icon('bulkcompletiontracking', 'core_completion'); 87 $data->sections = []; 88 foreach ($sections as $sectionnumber => $section) { 89 $sectioninfo = $moduleinfo->get_section_info($sectionnumber); 90 91 $sectionobject = new stdClass(); 92 $sectionobject->sectionnumber = $sectionnumber; 93 $sectionobject->name = get_section_name($this->courseid, $sectioninfo); 94 $sectionobject->activities = $this->get_activities($section, true); 95 $data->sections[] = $sectionobject; 96 } 97 return $data; 98 } 99 100 /** 101 * Gets the data (context) to be used with the activityinstance template 102 * 103 * @param array $cmids list of course module ids 104 * @param bool $withcompletiondetails include completion details 105 * @return array 106 */ 107 public function get_activities($cmids, $withcompletiondetails = false) { 108 $moduleinfo = get_fast_modinfo($this->courseid); 109 $activities = []; 110 foreach ($cmids as $cmid) { 111 $mod = $moduleinfo->get_cm($cmid); 112 if (!$mod->uservisible) { 113 continue; 114 } 115 $moduleobject = new stdClass(); 116 $moduleobject->cmid = $cmid; 117 $moduleobject->modname = $mod->get_formatted_name(); 118 $moduleobject->icon = $mod->get_icon_url()->out(); 119 $moduleobject->url = $mod->url; 120 $moduleobject->canmanage = $withcompletiondetails && self::can_edit_bulk_completion($this->courseid, $mod); 121 122 // Get activity completion information. 123 if ($moduleobject->canmanage) { 124 $moduleobject->completionstatus = $this->get_completion_detail($mod); 125 } else { 126 $moduleobject->completionstatus = ['icon' => null, 'string' => null]; 127 } 128 if (self::can_edit_bulk_completion($this->courseid, $mod)) { 129 $activities[] = $moduleobject; 130 } 131 } 132 return $activities; 133 } 134 135 136 /** 137 * Get completion information on the selected module or module type 138 * 139 * @param cm_info|stdClass $mod either instance of cm_info (with 'customcompletionrules' in customdata) or 140 * object with fields ->completion, ->completionview, ->completionexpected, ->completionusegrade 141 * and ->customdata['customcompletionrules'] 142 * @return array 143 */ 144 private function get_completion_detail($mod) { 145 global $OUTPUT; 146 $strings = []; 147 switch ($mod->completion) { 148 case COMPLETION_TRACKING_NONE: 149 $strings['string'] = get_string('none'); 150 break; 151 152 case COMPLETION_TRACKING_MANUAL: 153 $strings['string'] = get_string('manual', 'completion'); 154 $strings['icon'] = $OUTPUT->pix_icon('i/completion-manual-y', get_string('completion_manual', 'completion')); 155 break; 156 157 case COMPLETION_TRACKING_AUTOMATIC: 158 $strings['string'] = get_string('withconditions', 'completion'); 159 $strings['icon'] = $OUTPUT->pix_icon('i/completion-auto-y', get_string('completion_automatic', 'completion')); 160 break; 161 162 default: 163 $strings['string'] = get_string('none'); 164 break; 165 } 166 167 // Get the descriptions for all the active completion rules for the module. 168 if ($ruledescriptions = $this->get_completion_active_rule_descriptions($mod)) { 169 foreach ($ruledescriptions as $ruledescription) { 170 $strings['string'] .= \html_writer::empty_tag('br') . $ruledescription; 171 } 172 } 173 return $strings; 174 } 175 176 /** 177 * Get the descriptions for all active conditional completion rules for the current module. 178 * 179 * @param cm_info|stdClass $moduledata either instance of cm_info (with 'customcompletionrules' in customdata) or 180 * object with fields ->completion, ->completionview, ->completionexpected, ->completionusegrade 181 * and ->customdata['customcompletionrules'] 182 * @return array $activeruledescriptions an array of strings describing the active completion rules. 183 */ 184 protected function get_completion_active_rule_descriptions($moduledata) { 185 $activeruledescriptions = []; 186 187 if ($moduledata->completion == COMPLETION_TRACKING_AUTOMATIC) { 188 // Generate the description strings for the core conditional completion rules (if set). 189 if (!empty($moduledata->completionview)) { 190 $activeruledescriptions[] = get_string('completionview_desc', 'completion'); 191 } 192 if ($moduledata instanceof cm_info && !is_null($moduledata->completiongradeitemnumber) || 193 ($moduledata instanceof stdClass && !empty($moduledata->completionusegrade))) { 194 195 $description = 'completionusegrade_desc'; 196 if (!empty($moduledata->completionpassgrade)) { 197 $description = 'completionpassgrade_desc'; 198 } 199 200 $activeruledescriptions[] = get_string($description, 'completion'); 201 } 202 203 // Now, ask the module to provide descriptions for its custom conditional completion rules. 204 if ($customruledescriptions = component_callback($moduledata->modname, 205 'get_completion_active_rule_descriptions', [$moduledata])) { 206 $activeruledescriptions = array_merge($activeruledescriptions, $customruledescriptions); 207 } 208 } 209 210 if ($moduledata->completion != COMPLETION_TRACKING_NONE) { 211 if (!empty($moduledata->completionexpected)) { 212 $activeruledescriptions[] = get_string('completionexpecteddesc', 'completion', 213 userdate($moduledata->completionexpected)); 214 } 215 } 216 217 return $activeruledescriptions; 218 } 219 220 /** 221 * Gets the course modules for the current course. 222 * 223 * @param bool $includedefaults Whether the default values should be included or not. 224 * @return stdClass $data containing the modules 225 */ 226 public function get_activities_and_resources(bool $includedefaults = true) { 227 global $DB, $OUTPUT, $CFG; 228 require_once($CFG->dirroot.'/course/lib.php'); 229 230 // Get enabled activities and resources. 231 $modules = $DB->get_records('modules', ['visible' => 1], 'name ASC'); 232 $data = new stdClass(); 233 $data->courseid = $this->courseid; 234 $data->sesskey = sesskey(); 235 $data->helpicon = $OUTPUT->help_icon('bulkcompletiontracking', 'core_completion'); 236 // Add icon information. 237 $data->modules = array_values($modules); 238 $context = $this->get_context(); 239 $canmanage = has_capability('moodle/course:manageactivities', $context); 240 $course = get_course($this->courseid); 241 foreach ($data->modules as $module) { 242 $module->icon = $OUTPUT->image_url('monologo', $module->name)->out(); 243 $module->formattedname = format_string(get_string('modulename', 'mod_' . $module->name), 244 true, ['context' => $context]); 245 $module->canmanage = $canmanage && course_allowed_module($course, $module->name); 246 if ($includedefaults) { 247 $defaults = self::get_default_completion($course, $module, false); 248 $defaults->modname = $module->name; 249 $module->completionstatus = $this->get_completion_detail($defaults); 250 } 251 } 252 // Order modules by displayed name. 253 $modules = (array) $data->modules; 254 usort($modules, function($a, $b) { 255 return strcmp($a->formattedname, $b->formattedname); 256 }); 257 $data->modules = $modules; 258 259 return $data; 260 } 261 262 /** 263 * Checks if current user can edit activity completion 264 * 265 * @param int|stdClass $courseorid 266 * @param \cm_info|null $cm if specified capability for a given coursemodule will be check, 267 * if not specified capability to edit at least one activity is checked. 268 */ 269 public static function can_edit_bulk_completion($courseorid, $cm = null) { 270 if ($cm) { 271 return $cm->uservisible && has_capability('moodle/course:manageactivities', $cm->context); 272 } 273 $coursecontext = context_course::instance(is_object($courseorid) ? $courseorid->id : $courseorid); 274 if (has_capability('moodle/course:manageactivities', $coursecontext)) { 275 return true; 276 } 277 $modinfo = get_fast_modinfo($courseorid); 278 foreach ($modinfo->cms as $mod) { 279 if ($mod->uservisible && has_capability('moodle/course:manageactivities', $mod->context)) { 280 return true; 281 } 282 } 283 return false; 284 } 285 286 /** 287 * Gets the available completion tabs for the current course and user. 288 * 289 * @deprecated since Moodle 4.0 290 * @param stdClass|int $courseorid the course object or id. 291 * @return tabobject[] 292 */ 293 public static function get_available_completion_tabs($courseorid) { 294 debugging('get_available_completion_tabs() has been deprecated. Please use ' . 295 'core_completion\manager::get_available_completion_options() instead.', DEBUG_DEVELOPER); 296 297 $tabs = []; 298 299 $courseid = is_object($courseorid) ? $courseorid->id : $courseorid; 300 $coursecontext = context_course::instance($courseid); 301 302 if (has_capability('moodle/course:update', $coursecontext)) { 303 $tabs[] = new tabobject( 304 'completion', 305 new moodle_url('/course/completion.php', ['id' => $courseid]), 306 new lang_string('coursecompletion', 'completion') 307 ); 308 } 309 310 if (has_capability('moodle/course:manageactivities', $coursecontext)) { 311 $tabs[] = new tabobject( 312 'defaultcompletion', 313 new moodle_url('/course/defaultcompletion.php', ['id' => $courseid]), 314 new lang_string('defaultcompletion', 'completion') 315 ); 316 } 317 318 if (self::can_edit_bulk_completion($courseorid)) { 319 $tabs[] = new tabobject( 320 'bulkcompletion', 321 new moodle_url('/course/bulkcompletion.php', ['id' => $courseid]), 322 new lang_string('bulkactivitycompletion', 'completion') 323 ); 324 } 325 326 return $tabs; 327 } 328 329 /** 330 * Returns an array with the available completion options (url => name) for the current course and user. 331 * 332 * @param int $courseid The course id. 333 * @return array 334 */ 335 public static function get_available_completion_options(int $courseid): array { 336 $coursecontext = context_course::instance($courseid); 337 $options = []; 338 339 if (has_capability('moodle/course:update', $coursecontext)) { 340 $completionlink = new moodle_url('/course/completion.php', ['id' => $courseid]); 341 $options[$completionlink->out(false)] = get_string('coursecompletionsettings', 'completion'); 342 } 343 344 if (has_capability('moodle/course:manageactivities', $coursecontext)) { 345 $defaultcompletionlink = new moodle_url('/course/defaultcompletion.php', ['id' => $courseid]); 346 $options[$defaultcompletionlink->out(false)] = get_string('defaultcompletion', 'completion'); 347 } 348 349 if (self::can_edit_bulk_completion($courseid)) { 350 $bulkcompletionlink = new moodle_url('/course/bulkcompletion.php', ['id' => $courseid]); 351 $options[$bulkcompletionlink->out(false)] = get_string('bulkactivitycompletion', 'completion'); 352 } 353 354 return $options; 355 } 356 357 /** 358 * Applies completion from the bulk edit form to all selected modules 359 * 360 * @param stdClass $data data received from the core_completion_bulkedit_form 361 * @param bool $updateinstances if we need to update the instance tables of the module (i.e. 'assign', 'forum', etc.) - 362 * if no module-specific completion rules were added to the form, update of the module table is not needed. 363 */ 364 public function apply_completion($data, $updateinstances) { 365 $updated = false; 366 $needreset = []; 367 $modinfo = get_fast_modinfo($this->courseid); 368 369 $cmids = $data->cmid; 370 371 $data = (array)$data; 372 unset($data['id']); // This is a course id, we don't want to confuse it with cmid or instance id. 373 unset($data['cmid']); 374 unset($data['submitbutton']); 375 376 foreach ($cmids as $cmid) { 377 $cm = $modinfo->get_cm($cmid); 378 if (self::can_edit_bulk_completion($this->courseid, $cm) && $this->apply_completion_cm($cm, $data, $updateinstances)) { 379 $updated = true; 380 if ($cm->completion != COMPLETION_TRACKING_MANUAL || $data['completion'] != COMPLETION_TRACKING_MANUAL) { 381 // If completion was changed we will need to reset it's state. Exception is when completion was and remains as manual. 382 $needreset[] = $cm->id; 383 } 384 } 385 // Update completion calendar events. 386 $completionexpected = ($data['completionexpected']) ? $data['completionexpected'] : null; 387 \core_completion\api::update_completion_date_event($cm->id, $cm->modname, $cm->instance, $completionexpected); 388 } 389 if ($updated) { 390 // Now that modules are fully updated, also update completion data if required. 391 // This will wipe all user completion data and recalculate it. 392 rebuild_course_cache($this->courseid, true); 393 $modinfo = get_fast_modinfo($this->courseid); 394 $completion = new \completion_info($modinfo->get_course()); 395 foreach ($needreset as $cmid) { 396 $completion->reset_all_state($modinfo->get_cm($cmid)); 397 } 398 399 // And notify the user of the result. 400 \core\notification::add(get_string('activitycompletionupdated', 'core_completion'), \core\notification::SUCCESS); 401 } 402 } 403 404 /** 405 * Applies new completion rules to one course module 406 * 407 * @param \cm_info $cm 408 * @param array $data 409 * @param bool $updateinstance if we need to update the instance table of the module (i.e. 'assign', 'forum', etc.) - 410 * if no module-specific completion rules were added to the form, update of the module table is not needed. 411 * @return bool if module was updated 412 */ 413 protected function apply_completion_cm(\cm_info $cm, $data, $updateinstance) { 414 global $DB; 415 416 $defaults = [ 417 'completion' => COMPLETION_DISABLED, 'completionview' => COMPLETION_VIEW_NOT_REQUIRED, 418 'completionexpected' => 0, 'completiongradeitemnumber' => null, 419 'completionpassgrade' => 0 420 ]; 421 422 $data += ['completion' => $cm->completion, 423 'completionexpected' => $cm->completionexpected, 424 'completionview' => $cm->completionview]; 425 426 if ($cm->completion == $data['completion'] && $cm->completion == COMPLETION_TRACKING_NONE) { 427 // If old and new completion are both "none" - no changes are needed. 428 return false; 429 } 430 431 if ($cm->completion == $data['completion'] && $cm->completion == COMPLETION_TRACKING_NONE && 432 $cm->completionexpected == $data['completionexpected']) { 433 // If old and new completion are both "manual" and completion expected date is not changed - no changes are needed. 434 return false; 435 } 436 437 if (array_key_exists('completionusegrade', $data)) { 438 // Convert the 'use grade' checkbox into a grade-item number: 0 if checked, null if not. 439 $data['completiongradeitemnumber'] = !empty($data['completionusegrade']) ? 0 : null; 440 unset($data['completionusegrade']); 441 } else { 442 // Completion grade item number is classified in mod_edit forms as 'use grade'. 443 $data['completionusegrade'] = is_null($cm->completiongradeitemnumber) ? 0 : 1; 444 $data['completiongradeitemnumber'] = $cm->completiongradeitemnumber; 445 } 446 447 // Update module instance table. 448 if ($updateinstance) { 449 $moddata = ['id' => $cm->instance, 'timemodified' => time()] + array_diff_key($data, $defaults); 450 $DB->update_record($cm->modname, $moddata); 451 } 452 453 // Update course modules table. 454 $cmdata = ['id' => $cm->id, 'timemodified' => time()] + array_intersect_key($data, $defaults); 455 $DB->update_record('course_modules', $cmdata); 456 457 \core\event\course_module_updated::create_from_cm($cm, $cm->context)->trigger(); 458 459 // We need to reset completion data for this activity. 460 return true; 461 } 462 463 464 /** 465 * Saves default completion from edit form to all selected module types 466 * 467 * @param stdClass $data data received from the core_completion_bulkedit_form 468 * @param bool $updatecustomrules if we need to update the custom rules of the module - 469 * if no module-specific completion rules were added to the form, update of the module table is not needed. 470 * @param string $suffix the suffix to add to the name of the completion rules. 471 */ 472 public function apply_default_completion($data, $updatecustomrules, string $suffix = '') { 473 global $DB; 474 475 if (!empty($suffix)) { 476 // Fields were renamed to avoid conflicts, but they need to be stored in DB with the original name. 477 $modules = property_exists($data, 'modules') ? $data->modules : null; 478 if ($modules !== null) { 479 unset($data->modules); 480 $data = (array)$data; 481 foreach ($data as $name => $value) { 482 if (str_ends_with($name, $suffix)) { 483 $data[substr($name, 0, strpos($name, $suffix))] = $value; 484 unset($data[$name]); 485 } else if ($name == 'customdata') { 486 $customrules = $value['customcompletionrules']; 487 foreach ($customrules as $rulename => $rulevalue) { 488 if (str_ends_with($rulename, $suffix)) { 489 $customrules[substr($rulename, 0, strpos($rulename, $suffix))] = $rulevalue; 490 unset($customrules[$rulename]); 491 } 492 } 493 $data['customdata'] = $customrules; 494 } 495 } 496 $data = (object)$data; 497 } 498 } 499 500 $courseid = $data->id; 501 // MDL-72375 Unset the id here, it should not be stored in customrules. 502 unset($data->id); 503 $coursecontext = context_course::instance($courseid); 504 if (!$modids = $data->modids) { 505 return; 506 } 507 $defaults = [ 508 'completion' => COMPLETION_DISABLED, 509 'completionview' => COMPLETION_VIEW_NOT_REQUIRED, 510 'completionexpected' => 0, 511 'completionusegrade' => 0, 512 'completionpassgrade' => 0 513 ]; 514 515 $data = (array)$data; 516 if (!array_key_exists('completionusegrade', $data)) { 517 $data['completionusegrade'] = 0; 518 } 519 if (!array_key_exists('completionpassgrade', $data)) { 520 $data['completionpassgrade'] = 0; 521 } 522 if ($data['completionusegrade'] == 0) { 523 $data['completionpassgrade'] = 0; 524 } 525 526 if ($updatecustomrules) { 527 $customdata = array_diff_key($data, $defaults); 528 $data['customrules'] = $customdata ? json_encode($customdata) : null; 529 $defaults['customrules'] = null; 530 } 531 $data = array_merge($defaults, $data); 532 533 // Get names of the affected modules. 534 list($modidssql, $params) = $DB->get_in_or_equal($modids); 535 $params[] = 1; 536 $modules = $DB->get_records_select_menu('modules', 'id ' . $modidssql . ' and visible = ?', $params, '', 'id, name'); 537 538 // Get an associative array of [module_id => course_completion_defaults_id]. 539 list($in, $params) = $DB->get_in_or_equal($modids); 540 $params[] = $courseid; 541 $defaultsids = $DB->get_records_select_menu('course_completion_defaults', 'module ' . $in . ' and course = ?', $params, '', 542 'module, id'); 543 544 foreach ($modids as $modid) { 545 if (!array_key_exists($modid, $modules)) { 546 continue; 547 } 548 if (isset($defaultsids[$modid])) { 549 $DB->update_record('course_completion_defaults', $data + ['id' => $defaultsids[$modid]]); 550 } else { 551 $defaultsids[$modid] = $DB->insert_record('course_completion_defaults', $data + ['course' => $courseid, 552 'module' => $modid]); 553 } 554 // Trigger event. 555 \core\event\completion_defaults_updated::create([ 556 'objectid' => $defaultsids[$modid], 557 'context' => $coursecontext, 558 'other' => ['modulename' => $modules[$modid]], 559 ])->trigger(); 560 } 561 562 // Add notification. 563 \core\notification::add(get_string('defaultcompletionupdated', 'completion'), \core\notification::SUCCESS); 564 } 565 566 /** 567 * Returns default completion rules for given module type in the given course 568 * 569 * @param stdClass $course 570 * @param stdClass $module 571 * @param bool $flatten if true all module custom completion rules become properties of the same object, 572 * otherwise they can be found as array in ->customdata['customcompletionrules'] 573 * @param string $suffix the suffix to add to the name of the completion rules. 574 * @return stdClass 575 */ 576 public static function get_default_completion($course, $module, $flatten = true, string $suffix = '') { 577 global $DB, $CFG, $SITE; 578 579 $fields = 'completion, completionview, completionexpected, completionusegrade, completionpassgrade, customrules'; 580 // Check course default completion values. 581 $params = ['course' => $course->id, 'module' => $module->id]; 582 $data = $DB->get_record('course_completion_defaults', $params, $fields); 583 if (!$data && $course->id != $SITE->id) { 584 // If there is no course default completion, check site level default completion values ($SITE->id). 585 $params['course'] = $SITE->id; 586 $data = $DB->get_record('course_completion_defaults', $params, $fields); 587 } 588 if ($data) { 589 if ($data->customrules && ($customrules = @json_decode($data->customrules, true))) { 590 // MDL-72375 This will override activity id for new mods. Skip this field, it is already exposed as courseid. 591 unset($customrules['id']); 592 593 if ($flatten) { 594 foreach ($customrules as $key => $value) { 595 $data->$key = $value; 596 } 597 } else { 598 $data->customdata['customcompletionrules'] = $customrules; 599 } 600 } 601 unset($data->customrules); 602 } else { 603 $data = new stdClass(); 604 $data->completion = COMPLETION_TRACKING_NONE; 605 } 606 607 // If the suffix is not empty, the completion rules need to be renamed to avoid conflicts. 608 if (!empty($suffix)) { 609 $data = (array)$data; 610 foreach ($data as $name => $value) { 611 if (str_starts_with($name, 'completion')) { 612 $data[$name . $suffix] = $value; 613 unset($data[$name]); 614 } else if ($name == 'customdata') { 615 $customrules = $value['customcompletionrules']; 616 foreach ($customrules as $rulename => $rulevalue) { 617 if (str_starts_with($rulename, 'completion')) { 618 $customrules[$rulename . $suffix] = $rulevalue; 619 unset($customrules[$rulename]); 620 } 621 } 622 $data['customdata'] = $customrules; 623 } 624 } 625 $data = (object)$data; 626 } 627 628 return $data; 629 } 630 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body