See Release Notes
Long Term Support Release
Differences Between: [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 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 * Contains classes, functions and constants used during the tracking 19 * of activity completion for users. 20 * 21 * Completion top-level options (admin setting enablecompletion) 22 * 23 * @package core_completion 24 * @category completion 25 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} 26 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 27 */ 28 29 defined('MOODLE_INTERNAL') || die(); 30 31 /** 32 * Include the required completion libraries 33 */ 34 require_once $CFG->dirroot.'/completion/completion_aggregation.php'; 35 require_once $CFG->dirroot.'/completion/criteria/completion_criteria.php'; 36 require_once $CFG->dirroot.'/completion/completion_completion.php'; 37 require_once $CFG->dirroot.'/completion/completion_criteria_completion.php'; 38 39 40 /** 41 * The completion system is enabled in this site/course 42 */ 43 define('COMPLETION_ENABLED', 1); 44 /** 45 * The completion system is not enabled in this site/course 46 */ 47 define('COMPLETION_DISABLED', 0); 48 49 /** 50 * Completion tracking is disabled for this activity 51 * This is a completion tracking option per-activity (course_modules/completion) 52 */ 53 define('COMPLETION_TRACKING_NONE', 0); 54 55 /** 56 * Manual completion tracking (user ticks box) is enabled for this activity 57 * This is a completion tracking option per-activity (course_modules/completion) 58 */ 59 define('COMPLETION_TRACKING_MANUAL', 1); 60 /** 61 * Automatic completion tracking (system ticks box) is enabled for this activity 62 * This is a completion tracking option per-activity (course_modules/completion) 63 */ 64 define('COMPLETION_TRACKING_AUTOMATIC', 2); 65 66 /** 67 * The user has not completed this activity. 68 * This is a completion state value (course_modules_completion/completionstate) 69 */ 70 define('COMPLETION_INCOMPLETE', 0); 71 /** 72 * The user has completed this activity. It is not specified whether they have 73 * passed or failed it. 74 * This is a completion state value (course_modules_completion/completionstate) 75 */ 76 define('COMPLETION_COMPLETE', 1); 77 /** 78 * The user has completed this activity with a grade above the pass mark. 79 * This is a completion state value (course_modules_completion/completionstate) 80 */ 81 define('COMPLETION_COMPLETE_PASS', 2); 82 /** 83 * The user has completed this activity but their grade is less than the pass mark 84 * This is a completion state value (course_modules_completion/completionstate) 85 */ 86 define('COMPLETION_COMPLETE_FAIL', 3); 87 88 /** 89 * The effect of this change to completion status is unknown. 90 * A completion effect changes (used only in update_state) 91 */ 92 define('COMPLETION_UNKNOWN', -1); 93 /** 94 * The user's grade has changed, so their new state might be 95 * COMPLETION_COMPLETE_PASS or COMPLETION_COMPLETE_FAIL. 96 * A completion effect changes (used only in update_state) 97 */ 98 define('COMPLETION_GRADECHANGE', -2); 99 100 /** 101 * User must view this activity. 102 * Whether view is required to create an activity (course_modules/completionview) 103 */ 104 define('COMPLETION_VIEW_REQUIRED', 1); 105 /** 106 * User does not need to view this activity 107 * Whether view is required to create an activity (course_modules/completionview) 108 */ 109 define('COMPLETION_VIEW_NOT_REQUIRED', 0); 110 111 /** 112 * User has viewed this activity. 113 * Completion viewed state (course_modules_completion/viewed) 114 */ 115 define('COMPLETION_VIEWED', 1); 116 /** 117 * User has not viewed this activity. 118 * Completion viewed state (course_modules_completion/viewed) 119 */ 120 define('COMPLETION_NOT_VIEWED', 0); 121 122 /** 123 * Completion details should be ORed together and you should return false if 124 * none apply. 125 */ 126 define('COMPLETION_OR', false); 127 /** 128 * Completion details should be ANDed together and you should return true if 129 * none apply 130 */ 131 define('COMPLETION_AND', true); 132 133 /** 134 * Course completion criteria aggregation method. 135 */ 136 define('COMPLETION_AGGREGATION_ALL', 1); 137 /** 138 * Course completion criteria aggregation method. 139 */ 140 define('COMPLETION_AGGREGATION_ANY', 2); 141 142 143 /** 144 * Utility function for checking if the logged in user can view 145 * another's completion data for a particular course 146 * 147 * @access public 148 * @param int $userid Completion data's owner 149 * @param mixed $course Course object or Course ID (optional) 150 * @return boolean 151 */ 152 function completion_can_view_data($userid, $course = null) { 153 global $USER; 154 155 if (!isloggedin()) { 156 return false; 157 } 158 159 if (!is_object($course)) { 160 $cid = $course; 161 $course = new stdClass(); 162 $course->id = $cid; 163 } 164 165 // Check if this is the site course 166 if ($course->id == SITEID) { 167 $course = null; 168 } 169 170 // Check if completion is enabled 171 if ($course) { 172 $cinfo = new completion_info($course); 173 if (!$cinfo->is_enabled()) { 174 return false; 175 } 176 } else { 177 if (!completion_info::is_enabled_for_site()) { 178 return false; 179 } 180 } 181 182 // Is own user's data? 183 if ($USER->id == $userid) { 184 return true; 185 } 186 187 // Check capabilities 188 $personalcontext = context_user::instance($userid); 189 190 if (has_capability('moodle/user:viewuseractivitiesreport', $personalcontext)) { 191 return true; 192 } elseif (has_capability('report/completion:view', $personalcontext)) { 193 return true; 194 } 195 196 if ($course->id) { 197 $coursecontext = context_course::instance($course->id); 198 } else { 199 $coursecontext = context_system::instance(); 200 } 201 202 if (has_capability('report/completion:view', $coursecontext)) { 203 return true; 204 } 205 206 return false; 207 } 208 209 210 /** 211 * Class represents completion information for a course. 212 * 213 * Does not contain any data, so you can safely construct it multiple times 214 * without causing any problems. 215 * 216 * @package core 217 * @category completion 218 * @copyright 2008 Sam Marshall 219 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 220 */ 221 class completion_info { 222 223 /* @var stdClass Course object passed during construction */ 224 private $course; 225 226 /* @var int Course id */ 227 public $course_id; 228 229 /* @var array Completion criteria {@link completion_info::get_criteria()} */ 230 private $criteria; 231 232 /** 233 * Return array of aggregation methods 234 * @return array 235 */ 236 public static function get_aggregation_methods() { 237 return array( 238 COMPLETION_AGGREGATION_ALL => get_string('all'), 239 COMPLETION_AGGREGATION_ANY => get_string('any', 'completion'), 240 ); 241 } 242 243 /** 244 * Constructs with course details. 245 * 246 * When instantiating a new completion info object you must provide a course 247 * object with at least id, and enablecompletion properties. Property 248 * cacherev is needed if you check completion of the current user since 249 * it is used for cache validation. 250 * 251 * @param stdClass $course Moodle course object. 252 */ 253 public function __construct($course) { 254 $this->course = $course; 255 $this->course_id = $course->id; 256 } 257 258 /** 259 * Determines whether completion is enabled across entire site. 260 * 261 * @return bool COMPLETION_ENABLED (true) if completion is enabled for the site, 262 * COMPLETION_DISABLED (false) if it's complete 263 */ 264 public static function is_enabled_for_site() { 265 global $CFG; 266 return !empty($CFG->enablecompletion); 267 } 268 269 /** 270 * Checks whether completion is enabled in a particular course and possibly 271 * activity. 272 * 273 * @param stdClass|cm_info $cm Course-module object. If not specified, returns the course 274 * completion enable state. 275 * @return mixed COMPLETION_ENABLED or COMPLETION_DISABLED (==0) in the case of 276 * site and course; COMPLETION_TRACKING_MANUAL, _AUTOMATIC or _NONE (==0) 277 * for a course-module. 278 */ 279 public function is_enabled($cm = null) { 280 global $CFG, $DB; 281 282 // First check global completion 283 if (!isset($CFG->enablecompletion) || $CFG->enablecompletion == COMPLETION_DISABLED) { 284 return COMPLETION_DISABLED; 285 } 286 287 // Load data if we do not have enough 288 if (!isset($this->course->enablecompletion)) { 289 $this->course = get_course($this->course_id); 290 } 291 292 // Check course completion 293 if ($this->course->enablecompletion == COMPLETION_DISABLED) { 294 return COMPLETION_DISABLED; 295 } 296 297 // If there was no $cm and we got this far, then it's enabled 298 if (!$cm) { 299 return COMPLETION_ENABLED; 300 } 301 302 // Return course-module completion value 303 return $cm->completion; 304 } 305 306 /** 307 * Displays the 'Your progress' help icon, if completion tracking is enabled. 308 * Just prints the result of display_help_icon(). 309 * 310 * @deprecated since Moodle 2.0 - Use display_help_icon instead. 311 */ 312 public function print_help_icon() { 313 print $this->display_help_icon(); 314 } 315 316 /** 317 * Returns the 'Your progress' help icon, if completion tracking is enabled. 318 * 319 * @return string HTML code for help icon, or blank if not needed 320 */ 321 public function display_help_icon() { 322 global $PAGE, $OUTPUT, $USER; 323 $result = ''; 324 if ($this->is_enabled() && !$PAGE->user_is_editing() && $this->is_tracked_user($USER->id) && isloggedin() && 325 !isguestuser()) { 326 $result .= html_writer::tag('div', get_string('yourprogress','completion') . 327 $OUTPUT->help_icon('completionicons', 'completion'), array('id' => 'completionprogressid', 328 'class' => 'completionprogress')); 329 } 330 return $result; 331 } 332 333 /** 334 * Get a course completion for a user 335 * 336 * @param int $user_id User id 337 * @param int $criteriatype Specific criteria type to return 338 * @return bool|completion_criteria_completion returns false on fail 339 */ 340 public function get_completion($user_id, $criteriatype) { 341 $completions = $this->get_completions($user_id, $criteriatype); 342 343 if (empty($completions)) { 344 return false; 345 } elseif (count($completions) > 1) { 346 print_error('multipleselfcompletioncriteria', 'completion'); 347 } 348 349 return $completions[0]; 350 } 351 352 /** 353 * Get all course criteria's completion objects for a user 354 * 355 * @param int $user_id User id 356 * @param int $criteriatype Specific criteria type to return (optional) 357 * @return array 358 */ 359 public function get_completions($user_id, $criteriatype = null) { 360 $criteria = $this->get_criteria($criteriatype); 361 362 $completions = array(); 363 364 foreach ($criteria as $criterion) { 365 $params = array( 366 'course' => $this->course_id, 367 'userid' => $user_id, 368 'criteriaid' => $criterion->id 369 ); 370 371 $completion = new completion_criteria_completion($params); 372 $completion->attach_criteria($criterion); 373 374 $completions[] = $completion; 375 } 376 377 return $completions; 378 } 379 380 /** 381 * Get completion object for a user and a criteria 382 * 383 * @param int $user_id User id 384 * @param completion_criteria $criteria Criteria object 385 * @return completion_criteria_completion 386 */ 387 public function get_user_completion($user_id, $criteria) { 388 $params = array( 389 'course' => $this->course_id, 390 'userid' => $user_id, 391 'criteriaid' => $criteria->id, 392 ); 393 394 $completion = new completion_criteria_completion($params); 395 return $completion; 396 } 397 398 /** 399 * Check if course has completion criteria set 400 * 401 * @return bool Returns true if there are criteria 402 */ 403 public function has_criteria() { 404 $criteria = $this->get_criteria(); 405 406 return (bool) count($criteria); 407 } 408 409 /** 410 * Get course completion criteria 411 * 412 * @param int $criteriatype Specific criteria type to return (optional) 413 */ 414 public function get_criteria($criteriatype = null) { 415 416 // Fill cache if empty 417 if (!is_array($this->criteria)) { 418 global $DB; 419 420 $params = array( 421 'course' => $this->course->id 422 ); 423 424 // Load criteria from database 425 $records = (array)$DB->get_records('course_completion_criteria', $params); 426 427 // Order records so activities are in the same order as they appear on the course view page. 428 if ($records) { 429 $activitiesorder = array_keys(get_fast_modinfo($this->course)->get_cms()); 430 usort($records, function ($a, $b) use ($activitiesorder) { 431 $aidx = ($a->criteriatype == COMPLETION_CRITERIA_TYPE_ACTIVITY) ? 432 array_search($a->moduleinstance, $activitiesorder) : false; 433 $bidx = ($b->criteriatype == COMPLETION_CRITERIA_TYPE_ACTIVITY) ? 434 array_search($b->moduleinstance, $activitiesorder) : false; 435 if ($aidx === false || $bidx === false || $aidx == $bidx) { 436 return 0; 437 } 438 return ($aidx < $bidx) ? -1 : 1; 439 }); 440 } 441 442 // Build array of criteria objects 443 $this->criteria = array(); 444 foreach ($records as $record) { 445 $this->criteria[$record->id] = completion_criteria::factory((array)$record); 446 } 447 } 448 449 // If after all criteria 450 if ($criteriatype === null) { 451 return $this->criteria; 452 } 453 454 // If we are only after a specific criteria type 455 $criteria = array(); 456 foreach ($this->criteria as $criterion) { 457 458 if ($criterion->criteriatype != $criteriatype) { 459 continue; 460 } 461 462 $criteria[$criterion->id] = $criterion; 463 } 464 465 return $criteria; 466 } 467 468 /** 469 * Get aggregation method 470 * 471 * @param int $criteriatype If none supplied, get overall aggregation method (optional) 472 * @return int One of COMPLETION_AGGREGATION_ALL or COMPLETION_AGGREGATION_ANY 473 */ 474 public function get_aggregation_method($criteriatype = null) { 475 $params = array( 476 'course' => $this->course_id, 477 'criteriatype' => $criteriatype 478 ); 479 480 $aggregation = new completion_aggregation($params); 481 482 if (!$aggregation->id) { 483 $aggregation->method = COMPLETION_AGGREGATION_ALL; 484 } 485 486 return $aggregation->method; 487 } 488 489 /** 490 * @deprecated since Moodle 2.8 MDL-46290. 491 */ 492 public function get_incomplete_criteria() { 493 throw new coding_exception('completion_info->get_incomplete_criteria() is removed.'); 494 } 495 496 /** 497 * Clear old course completion criteria 498 */ 499 public function clear_criteria() { 500 global $DB; 501 502 // Remove completion criteria records for the course itself, and any records that refer to the course. 503 $select = 'course = :course OR (criteriatype = :type AND courseinstance = :courseinstance)'; 504 $params = [ 505 'course' => $this->course_id, 506 'type' => COMPLETION_CRITERIA_TYPE_COURSE, 507 'courseinstance' => $this->course_id, 508 ]; 509 510 $DB->delete_records_select('course_completion_criteria', $select, $params); 511 $DB->delete_records('course_completion_aggr_methd', array('course' => $this->course_id)); 512 513 $this->delete_course_completion_data(); 514 } 515 516 /** 517 * Has the supplied user completed this course 518 * 519 * @param int $user_id User's id 520 * @return boolean 521 */ 522 public function is_course_complete($user_id) { 523 $params = array( 524 'userid' => $user_id, 525 'course' => $this->course_id 526 ); 527 528 $ccompletion = new completion_completion($params); 529 return $ccompletion->is_complete(); 530 } 531 532 /** 533 * Check whether the supplied user can override the activity completion statuses within the current course. 534 * 535 * @param stdClass $user The user object. 536 * @return bool True if the user can override, false otherwise. 537 */ 538 public function user_can_override_completion($user) { 539 return has_capability('moodle/course:overridecompletion', context_course::instance($this->course_id), $user); 540 } 541 542 /** 543 * Updates (if necessary) the completion state of activity $cm for the given 544 * user. 545 * 546 * For manual completion, this function is called when completion is toggled 547 * with $possibleresult set to the target state. 548 * 549 * For automatic completion, this function should be called every time a module 550 * does something which might influence a user's completion state. For example, 551 * if a forum provides options for marking itself 'completed' once a user makes 552 * N posts, this function should be called every time a user makes a new post. 553 * [After the post has been saved to the database]. When calling, you do not 554 * need to pass in the new completion state. Instead this function carries out 555 * completion calculation by checking grades and viewed state itself, and 556 * calling the involved module via modulename_get_completion_state() to check 557 * module-specific conditions. 558 * 559 * @param stdClass|cm_info $cm Course-module 560 * @param int $possibleresult Expected completion result. If the event that 561 * has just occurred (e.g. add post) can only result in making the activity 562 * complete when it wasn't before, use COMPLETION_COMPLETE. If the event that 563 * has just occurred (e.g. delete post) can only result in making the activity 564 * not complete when it was previously complete, use COMPLETION_INCOMPLETE. 565 * Otherwise use COMPLETION_UNKNOWN. Setting this value to something other than 566 * COMPLETION_UNKNOWN significantly improves performance because it will abandon 567 * processing early if the user's completion state already matches the expected 568 * result. For manual events, COMPLETION_COMPLETE or COMPLETION_INCOMPLETE 569 * must be used; these directly set the specified state. 570 * @param int $userid User ID to be updated. Default 0 = current user 571 * @param bool $override Whether manually overriding the existing completion state. 572 * @return void 573 * @throws moodle_exception if trying to override without permission. 574 */ 575 public function update_state($cm, $possibleresult=COMPLETION_UNKNOWN, $userid=0, $override = false) { 576 global $USER; 577 578 // Do nothing if completion is not enabled for that activity 579 if (!$this->is_enabled($cm)) { 580 return; 581 } 582 583 // If we're processing an override and the current user isn't allowed to do so, then throw an exception. 584 if ($override) { 585 if (!$this->user_can_override_completion($USER)) { 586 throw new required_capability_exception(context_course::instance($this->course_id), 587 'moodle/course:overridecompletion', 'nopermission', ''); 588 } 589 } 590 591 // Get current value of completion state and do nothing if it's same as 592 // the possible result of this change. If the change is to COMPLETE and the 593 // current value is one of the COMPLETE_xx subtypes, ignore that as well 594 $current = $this->get_data($cm, false, $userid); 595 if ($possibleresult == $current->completionstate || 596 ($possibleresult == COMPLETION_COMPLETE && 597 ($current->completionstate == COMPLETION_COMPLETE_PASS || 598 $current->completionstate == COMPLETION_COMPLETE_FAIL))) { 599 return; 600 } 601 602 // For auto tracking, if the status is overridden to 'COMPLETION_COMPLETE', then disallow further changes, 603 // unless processing another override. 604 // Basically, we want those activities which have been overridden to COMPLETE to hold state, and those which have been 605 // overridden to INCOMPLETE to still be processed by normal completion triggers. 606 if ($cm->completion == COMPLETION_TRACKING_AUTOMATIC && !is_null($current->overrideby) 607 && $current->completionstate == COMPLETION_COMPLETE && !$override) { 608 return; 609 } 610 611 // For manual tracking, or if overriding the completion state, we set the state directly. 612 if ($cm->completion == COMPLETION_TRACKING_MANUAL || $override) { 613 switch($possibleresult) { 614 case COMPLETION_COMPLETE: 615 case COMPLETION_INCOMPLETE: 616 $newstate = $possibleresult; 617 break; 618 default: 619 $this->internal_systemerror("Unexpected manual completion state for {$cm->id}: $possibleresult"); 620 } 621 622 } else { 623 $newstate = $this->internal_get_state($cm, $userid, $current); 624 } 625 626 // If changed, update 627 if ($newstate != $current->completionstate) { 628 $current->completionstate = $newstate; 629 $current->timemodified = time(); 630 $current->overrideby = $override ? $USER->id : null; 631 $this->internal_set_data($cm, $current); 632 } 633 } 634 635 /** 636 * Calculates the completion state for an activity and user. 637 * 638 * Internal function. Not private, so we can unit-test it. 639 * 640 * @param stdClass|cm_info $cm Activity 641 * @param int $userid ID of user 642 * @param stdClass $current Previous completion information from database 643 * @return mixed 644 */ 645 public function internal_get_state($cm, $userid, $current) { 646 global $USER, $DB, $CFG; 647 648 // Get user ID 649 if (!$userid) { 650 $userid = $USER->id; 651 } 652 653 // Check viewed 654 if ($cm->completionview == COMPLETION_VIEW_REQUIRED && 655 $current->viewed == COMPLETION_NOT_VIEWED) { 656 657 return COMPLETION_INCOMPLETE; 658 } 659 660 // Modname hopefully is provided in $cm but just in case it isn't, let's grab it 661 if (!isset($cm->modname)) { 662 $cm->modname = $DB->get_field('modules', 'name', array('id'=>$cm->module)); 663 } 664 665 $newstate = COMPLETION_COMPLETE; 666 667 // Check grade 668 if (!is_null($cm->completiongradeitemnumber)) { 669 require_once($CFG->libdir.'/gradelib.php'); 670 $item = grade_item::fetch(array('courseid'=>$cm->course, 'itemtype'=>'mod', 671 'itemmodule'=>$cm->modname, 'iteminstance'=>$cm->instance, 672 'itemnumber'=>$cm->completiongradeitemnumber)); 673 if ($item) { 674 // Fetch 'grades' (will be one or none) 675 $grades = grade_grade::fetch_users_grades($item, array($userid), false); 676 if (empty($grades)) { 677 // No grade for user 678 return COMPLETION_INCOMPLETE; 679 } 680 if (count($grades) > 1) { 681 $this->internal_systemerror("Unexpected result: multiple grades for 682 item '{$item->id}', user '{$userid}'"); 683 } 684 $newstate = self::internal_get_grade_state($item, reset($grades)); 685 if ($newstate == COMPLETION_INCOMPLETE) { 686 return COMPLETION_INCOMPLETE; 687 } 688 689 } else { 690 $this->internal_systemerror("Cannot find grade item for '{$cm->modname}' 691 cm '{$cm->id}' matching number '{$cm->completiongradeitemnumber}'"); 692 } 693 } 694 695 if (plugin_supports('mod', $cm->modname, FEATURE_COMPLETION_HAS_RULES)) { 696 $function = $cm->modname.'_get_completion_state'; 697 if (!function_exists($function)) { 698 $this->internal_systemerror("Module {$cm->modname} claims to support 699 FEATURE_COMPLETION_HAS_RULES but does not have required 700 {$cm->modname}_get_completion_state function"); 701 } 702 if (!$function($this->course, $cm, $userid, COMPLETION_AND)) { 703 return COMPLETION_INCOMPLETE; 704 } 705 } 706 707 return $newstate; 708 709 } 710 711 /** 712 * Marks a module as viewed. 713 * 714 * Should be called whenever a module is 'viewed' (it is up to the module how to 715 * determine that). Has no effect if viewing is not set as a completion condition. 716 * 717 * Note that this function must be called before you print the page header because 718 * it is possible that the navigation block may depend on it. If you call it after 719 * printing the header, it shows a developer debug warning. 720 * 721 * @param stdClass|cm_info $cm Activity 722 * @param int $userid User ID or 0 (default) for current user 723 * @return void 724 */ 725 public function set_module_viewed($cm, $userid=0) { 726 global $PAGE; 727 if ($PAGE->headerprinted) { 728 debugging('set_module_viewed must be called before header is printed', 729 DEBUG_DEVELOPER); 730 } 731 732 // Don't do anything if view condition is not turned on 733 if ($cm->completionview == COMPLETION_VIEW_NOT_REQUIRED || !$this->is_enabled($cm)) { 734 return; 735 } 736 737 // Get current completion state 738 $data = $this->get_data($cm, false, $userid); 739 740 // If we already viewed it, don't do anything unless the completion status is overridden. 741 // If the completion status is overridden, then we need to allow this 'view' to trigger automatic completion again. 742 if ($data->viewed == COMPLETION_VIEWED && empty($data->overrideby)) { 743 return; 744 } 745 746 // OK, change state, save it, and update completion 747 $data->viewed = COMPLETION_VIEWED; 748 $this->internal_set_data($cm, $data); 749 $this->update_state($cm, COMPLETION_COMPLETE, $userid); 750 } 751 752 /** 753 * Determines how much completion data exists for an activity. This is used when 754 * deciding whether completion information should be 'locked' in the module 755 * editing form. 756 * 757 * @param cm_info $cm Activity 758 * @return int The number of users who have completion data stored for this 759 * activity, 0 if none 760 */ 761 public function count_user_data($cm) { 762 global $DB; 763 764 return $DB->get_field_sql(" 765 SELECT 766 COUNT(1) 767 FROM 768 {course_modules_completion} 769 WHERE 770 coursemoduleid=? AND completionstate<>0", array($cm->id)); 771 } 772 773 /** 774 * Determines how much course completion data exists for a course. This is used when 775 * deciding whether completion information should be 'locked' in the completion 776 * settings form and activity completion settings. 777 * 778 * @param int $user_id Optionally only get course completion data for a single user 779 * @return int The number of users who have completion data stored for this 780 * course, 0 if none 781 */ 782 public function count_course_user_data($user_id = null) { 783 global $DB; 784 785 $sql = ' 786 SELECT 787 COUNT(1) 788 FROM 789 {course_completion_crit_compl} 790 WHERE 791 course = ? 792 '; 793 794 $params = array($this->course_id); 795 796 // Limit data to a single user if an ID is supplied 797 if ($user_id) { 798 $sql .= ' AND userid = ?'; 799 $params[] = $user_id; 800 } 801 802 return $DB->get_field_sql($sql, $params); 803 } 804 805 /** 806 * Check if this course's completion criteria should be locked 807 * 808 * @return boolean 809 */ 810 public function is_course_locked() { 811 return (bool) $this->count_course_user_data(); 812 } 813 814 /** 815 * Deletes all course completion completion data. 816 * 817 * Intended to be used when unlocking completion criteria settings. 818 */ 819 public function delete_course_completion_data() { 820 global $DB; 821 822 $DB->delete_records('course_completions', array('course' => $this->course_id)); 823 $DB->delete_records('course_completion_crit_compl', array('course' => $this->course_id)); 824 825 // Difficult to find affected users, just purge all completion cache. 826 cache::make('core', 'completion')->purge(); 827 cache::make('core', 'coursecompletion')->purge(); 828 } 829 830 /** 831 * Deletes all activity and course completion data for an entire course 832 * (the below delete_all_state function does this for a single activity). 833 * 834 * Used by course reset page. 835 */ 836 public function delete_all_completion_data() { 837 global $DB; 838 839 // Delete from database. 840 $DB->delete_records_select('course_modules_completion', 841 'coursemoduleid IN (SELECT id FROM {course_modules} WHERE course=?)', 842 array($this->course_id)); 843 844 // Wipe course completion data too. 845 $this->delete_course_completion_data(); 846 } 847 848 /** 849 * Deletes completion state related to an activity for all users. 850 * 851 * Intended for use only when the activity itself is deleted. 852 * 853 * @param stdClass|cm_info $cm Activity 854 */ 855 public function delete_all_state($cm) { 856 global $DB; 857 858 // Delete from database 859 $DB->delete_records('course_modules_completion', array('coursemoduleid'=>$cm->id)); 860 861 // Check if there is an associated course completion criteria 862 $criteria = $this->get_criteria(COMPLETION_CRITERIA_TYPE_ACTIVITY); 863 $acriteria = false; 864 foreach ($criteria as $criterion) { 865 if ($criterion->moduleinstance == $cm->id) { 866 $acriteria = $criterion; 867 break; 868 } 869 } 870 871 if ($acriteria) { 872 // Delete all criteria completions relating to this activity 873 $DB->delete_records('course_completion_crit_compl', array('course' => $this->course_id, 'criteriaid' => $acriteria->id)); 874 $DB->delete_records('course_completions', array('course' => $this->course_id)); 875 } 876 877 // Difficult to find affected users, just purge all completion cache. 878 cache::make('core', 'completion')->purge(); 879 cache::make('core', 'coursecompletion')->purge(); 880 } 881 882 /** 883 * Recalculates completion state related to an activity for all users. 884 * 885 * Intended for use if completion conditions change. (This should be avoided 886 * as it may cause some things to become incomplete when they were previously 887 * complete, with the effect - for example - of hiding a later activity that 888 * was previously available.) 889 * 890 * Resetting state of manual tickbox has same result as deleting state for 891 * it. 892 * 893 * @param stcClass|cm_info $cm Activity 894 */ 895 public function reset_all_state($cm) { 896 global $DB; 897 898 if ($cm->completion == COMPLETION_TRACKING_MANUAL) { 899 $this->delete_all_state($cm); 900 return; 901 } 902 // Get current list of users with completion state 903 $rs = $DB->get_recordset('course_modules_completion', array('coursemoduleid'=>$cm->id), '', 'userid'); 904 $keepusers = array(); 905 foreach ($rs as $rec) { 906 $keepusers[] = $rec->userid; 907 } 908 $rs->close(); 909 910 // Delete all existing state. 911 $this->delete_all_state($cm); 912 913 // Merge this with list of planned users (according to roles) 914 $trackedusers = $this->get_tracked_users(); 915 foreach ($trackedusers as $trackeduser) { 916 $keepusers[] = $trackeduser->id; 917 } 918 $keepusers = array_unique($keepusers); 919 920 // Recalculate state for each kept user 921 foreach ($keepusers as $keepuser) { 922 $this->update_state($cm, COMPLETION_UNKNOWN, $keepuser); 923 } 924 } 925 926 /** 927 * Obtains completion data for a particular activity and user (from the 928 * completion cache if available, or by SQL query) 929 * 930 * @param stdClass|cm_info $cm Activity; only required field is ->id 931 * @param bool $wholecourse If true (default false) then, when necessary to 932 * fill the cache, retrieves information from the entire course not just for 933 * this one activity 934 * @param int $userid User ID or 0 (default) for current user 935 * @param array $modinfo Supply the value here - this is used for unit 936 * testing and so that it can be called recursively from within 937 * get_fast_modinfo. (Needs only list of all CMs with IDs.) 938 * Otherwise the method calls get_fast_modinfo itself. 939 * @return object Completion data (record from course_modules_completion) 940 */ 941 public function get_data($cm, $wholecourse = false, $userid = 0, $modinfo = null) { 942 global $USER, $CFG, $DB; 943 $completioncache = cache::make('core', 'completion'); 944 945 // Get user ID 946 if (!$userid) { 947 $userid = $USER->id; 948 } 949 950 // See if requested data is present in cache (use cache for current user only). 951 $usecache = $userid == $USER->id; 952 $cacheddata = array(); 953 if ($usecache) { 954 $key = $userid . '_' . $this->course->id; 955 if (!isset($this->course->cacherev)) { 956 $this->course = get_course($this->course_id); 957 } 958 if ($cacheddata = $completioncache->get($key)) { 959 if ($cacheddata['cacherev'] != $this->course->cacherev) { 960 // Course structure has been changed since the last caching, forget the cache. 961 $cacheddata = array(); 962 } else if (isset($cacheddata[$cm->id])) { 963 return (object)$cacheddata[$cm->id]; 964 } 965 } 966 } 967 968 // If cached completion data is not found, fetch via SQL. 969 // Fetch completion data for all of the activities in the course ONLY if we're caching the fetched completion data. 970 // If we're not caching the completion data, then just fetch the completion data for the user in this course module. 971 if ($usecache && $wholecourse) { 972 // Get whole course data for cache 973 $alldatabycmc = $DB->get_records_sql(" 974 SELECT 975 cmc.* 976 FROM 977 {course_modules} cm 978 INNER JOIN {course_modules_completion} cmc ON cmc.coursemoduleid=cm.id 979 WHERE 980 cm.course=? AND cmc.userid=?", array($this->course->id, $userid)); 981 982 // Reindex by cm id 983 $alldata = array(); 984 foreach ($alldatabycmc as $data) { 985 $alldata[$data->coursemoduleid] = (array)$data; 986 } 987 988 // Get the module info and build up condition info for each one 989 if (empty($modinfo)) { 990 $modinfo = get_fast_modinfo($this->course, $userid); 991 } 992 foreach ($modinfo->cms as $othercm) { 993 if (isset($alldata[$othercm->id])) { 994 $data = $alldata[$othercm->id]; 995 } else { 996 // Row not present counts as 'not complete' 997 $data = array(); 998 $data['id'] = 0; 999 $data['coursemoduleid'] = $othercm->id; 1000 $data['userid'] = $userid; 1001 $data['completionstate'] = 0; 1002 $data['viewed'] = 0; 1003 $data['overrideby'] = null; 1004 $data['timemodified'] = 0; 1005 } 1006 $cacheddata[$othercm->id] = $data; 1007 } 1008 1009 if (!isset($cacheddata[$cm->id])) { 1010 $this->internal_systemerror("Unexpected error: course-module {$cm->id} could not be found on course {$this->course->id}"); 1011 } 1012 1013 } else { 1014 // Get single record 1015 $data = $DB->get_record('course_modules_completion', array('coursemoduleid'=>$cm->id, 'userid'=>$userid)); 1016 if ($data) { 1017 $data = (array)$data; 1018 } else { 1019 // Row not present counts as 'not complete' 1020 $data = array(); 1021 $data['id'] = 0; 1022 $data['coursemoduleid'] = $cm->id; 1023 $data['userid'] = $userid; 1024 $data['completionstate'] = 0; 1025 $data['viewed'] = 0; 1026 $data['overrideby'] = null; 1027 $data['timemodified'] = 0; 1028 } 1029 1030 // Put in cache 1031 $cacheddata[$cm->id] = $data; 1032 } 1033 1034 if ($usecache) { 1035 $cacheddata['cacherev'] = $this->course->cacherev; 1036 $completioncache->set($key, $cacheddata); 1037 } 1038 return (object)$cacheddata[$cm->id]; 1039 } 1040 1041 /** 1042 * Updates completion data for a particular coursemodule and user (user is 1043 * determined from $data). 1044 * 1045 * (Internal function. Not private, so we can unit-test it.) 1046 * 1047 * @param stdClass|cm_info $cm Activity 1048 * @param stdClass $data Data about completion for that user 1049 */ 1050 public function internal_set_data($cm, $data) { 1051 global $USER, $DB; 1052 1053 $transaction = $DB->start_delegated_transaction(); 1054 if (!$data->id) { 1055 // Check there isn't really a row 1056 $data->id = $DB->get_field('course_modules_completion', 'id', 1057 array('coursemoduleid'=>$data->coursemoduleid, 'userid'=>$data->userid)); 1058 } 1059 if (!$data->id) { 1060 // Didn't exist before, needs creating 1061 $data->id = $DB->insert_record('course_modules_completion', $data); 1062 } else { 1063 // Has real (nonzero) id meaning that a database row exists, update 1064 $DB->update_record('course_modules_completion', $data); 1065 } 1066 $transaction->allow_commit(); 1067 1068 $cmcontext = context_module::instance($data->coursemoduleid, MUST_EXIST); 1069 $coursecontext = $cmcontext->get_parent_context(); 1070 1071 $completioncache = cache::make('core', 'completion'); 1072 if ($data->userid == $USER->id) { 1073 // Update module completion in user's cache. 1074 if (!($cachedata = $completioncache->get($data->userid . '_' . $cm->course)) 1075 || $cachedata['cacherev'] != $this->course->cacherev) { 1076 $cachedata = array('cacherev' => $this->course->cacherev); 1077 } 1078 $cachedata[$cm->id] = $data; 1079 $completioncache->set($data->userid . '_' . $cm->course, $cachedata); 1080 1081 // reset modinfo for user (no need to call rebuild_course_cache()) 1082 get_fast_modinfo($cm->course, 0, true); 1083 } else { 1084 // Remove another user's completion cache for this course. 1085 $completioncache->delete($data->userid . '_' . $cm->course); 1086 } 1087 1088 // Trigger an event for course module completion changed. 1089 $event = \core\event\course_module_completion_updated::create(array( 1090 'objectid' => $data->id, 1091 'context' => $cmcontext, 1092 'relateduserid' => $data->userid, 1093 'other' => array( 1094 'relateduserid' => $data->userid, 1095 'overrideby' => $data->overrideby, 1096 'completionstate' => $data->completionstate 1097 ) 1098 )); 1099 $event->add_record_snapshot('course_modules_completion', $data); 1100 $event->trigger(); 1101 } 1102 1103 /** 1104 * Return whether or not the course has activities with completion enabled. 1105 * 1106 * @return boolean true when there is at least one activity with completion enabled. 1107 */ 1108 public function has_activities() { 1109 $modinfo = get_fast_modinfo($this->course); 1110 foreach ($modinfo->get_cms() as $cm) { 1111 if ($cm->completion != COMPLETION_TRACKING_NONE) { 1112 return true; 1113 } 1114 } 1115 return false; 1116 } 1117 1118 /** 1119 * Obtains a list of activities for which completion is enabled on the 1120 * course. The list is ordered by the section order of those activities. 1121 * 1122 * @return cm_info[] Array from $cmid => $cm of all activities with completion enabled, 1123 * empty array if none 1124 */ 1125 public function get_activities() { 1126 $modinfo = get_fast_modinfo($this->course); 1127 $result = array(); 1128 foreach ($modinfo->get_cms() as $cm) { 1129 if ($cm->completion != COMPLETION_TRACKING_NONE && !$cm->deletioninprogress) { 1130 $result[$cm->id] = $cm; 1131 } 1132 } 1133 return $result; 1134 } 1135 1136 /** 1137 * Checks to see if the userid supplied has a tracked role in 1138 * this course 1139 * 1140 * @param int $userid User id 1141 * @return bool 1142 */ 1143 public function is_tracked_user($userid) { 1144 return is_enrolled(context_course::instance($this->course->id), $userid, 'moodle/course:isincompletionreports', true); 1145 } 1146 1147 /** 1148 * Returns the number of users whose progress is tracked in this course. 1149 * 1150 * Optionally supply a search's where clause, or a group id. 1151 * 1152 * @param string $where Where clause sql (use 'u.whatever' for user table fields) 1153 * @param array $whereparams Where clause params 1154 * @param int $groupid Group id 1155 * @return int Number of tracked users 1156 */ 1157 public function get_num_tracked_users($where = '', $whereparams = array(), $groupid = 0) { 1158 global $DB; 1159 1160 list($enrolledsql, $enrolledparams) = get_enrolled_sql( 1161 context_course::instance($this->course->id), 'moodle/course:isincompletionreports', $groupid, true); 1162 $sql = 'SELECT COUNT(eu.id) FROM (' . $enrolledsql . ') eu JOIN {user} u ON u.id = eu.id'; 1163 if ($where) { 1164 $sql .= " WHERE $where"; 1165 } 1166 1167 $params = array_merge($enrolledparams, $whereparams); 1168 return $DB->count_records_sql($sql, $params); 1169 } 1170 1171 /** 1172 * Return array of users whose progress is tracked in this course. 1173 * 1174 * Optionally supply a search's where clause, group id, sorting, paging. 1175 * 1176 * @param string $where Where clause sql, referring to 'u.' fields (optional) 1177 * @param array $whereparams Where clause params (optional) 1178 * @param int $groupid Group ID to restrict to (optional) 1179 * @param string $sort Order by clause (optional) 1180 * @param int $limitfrom Result start (optional) 1181 * @param int $limitnum Result max size (optional) 1182 * @param context $extracontext If set, includes extra user information fields 1183 * as appropriate to display for current user in this context 1184 * @return array Array of user objects with standard user fields 1185 */ 1186 public function get_tracked_users($where = '', $whereparams = array(), $groupid = 0, 1187 $sort = '', $limitfrom = '', $limitnum = '', context $extracontext = null) { 1188 1189 global $DB; 1190 1191 list($enrolledsql, $params) = get_enrolled_sql( 1192 context_course::instance($this->course->id), 1193 'moodle/course:isincompletionreports', $groupid, true); 1194 1195 $allusernames = get_all_user_name_fields(true, 'u'); 1196 $sql = 'SELECT u.id, u.idnumber, ' . $allusernames; 1197 if ($extracontext) { 1198 $sql .= get_extra_user_fields_sql($extracontext, 'u', '', array('idnumber')); 1199 } 1200 $sql .= ' FROM (' . $enrolledsql . ') eu JOIN {user} u ON u.id = eu.id'; 1201 1202 if ($where) { 1203 $sql .= " AND $where"; 1204 $params = array_merge($params, $whereparams); 1205 } 1206 1207 if ($sort) { 1208 $sql .= " ORDER BY $sort"; 1209 } 1210 1211 return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum); 1212 } 1213 1214 /** 1215 * Obtains progress information across a course for all users on that course, or 1216 * for all users in a specific group. Intended for use when displaying progress. 1217 * 1218 * This includes only users who, in course context, have one of the roles for 1219 * which progress is tracked (the gradebookroles admin option) and are enrolled in course. 1220 * 1221 * Users are included (in the first array) even if they do not have 1222 * completion progress for any course-module. 1223 * 1224 * @param bool $sortfirstname If true, sort by first name, otherwise sort by 1225 * last name 1226 * @param string $where Where clause sql (optional) 1227 * @param array $where_params Where clause params (optional) 1228 * @param int $groupid Group ID or 0 (default)/false for all groups 1229 * @param int $pagesize Number of users to actually return (optional) 1230 * @param int $start User to start at if paging (optional) 1231 * @param context $extracontext If set, includes extra user information fields 1232 * as appropriate to display for current user in this context 1233 * @return stdClass with ->total and ->start (same as $start) and ->users; 1234 * an array of user objects (like mdl_user id, firstname, lastname) 1235 * containing an additional ->progress array of coursemoduleid => completionstate 1236 */ 1237 public function get_progress_all($where = '', $where_params = array(), $groupid = 0, 1238 $sort = '', $pagesize = '', $start = '', context $extracontext = null) { 1239 global $CFG, $DB; 1240 1241 // Get list of applicable users 1242 $users = $this->get_tracked_users($where, $where_params, $groupid, $sort, 1243 $start, $pagesize, $extracontext); 1244 1245 // Get progress information for these users in groups of 1, 000 (if needed) 1246 // to avoid making the SQL IN too long 1247 $results = array(); 1248 $userids = array(); 1249 foreach ($users as $user) { 1250 $userids[] = $user->id; 1251 $results[$user->id] = $user; 1252 $results[$user->id]->progress = array(); 1253 } 1254 1255 for($i=0; $i<count($userids); $i+=1000) { 1256 $blocksize = count($userids)-$i < 1000 ? count($userids)-$i : 1000; 1257 1258 list($insql, $params) = $DB->get_in_or_equal(array_slice($userids, $i, $blocksize)); 1259 array_splice($params, 0, 0, array($this->course->id)); 1260 $rs = $DB->get_recordset_sql(" 1261 SELECT 1262 cmc.* 1263 FROM 1264 {course_modules} cm 1265 INNER JOIN {course_modules_completion} cmc ON cm.id=cmc.coursemoduleid 1266 WHERE 1267 cm.course=? AND cmc.userid $insql", $params); 1268 foreach ($rs as $progress) { 1269 $progress = (object)$progress; 1270 $results[$progress->userid]->progress[$progress->coursemoduleid] = $progress; 1271 } 1272 $rs->close(); 1273 } 1274 1275 return $results; 1276 } 1277 1278 /** 1279 * Called by grade code to inform the completion system when a grade has 1280 * been changed. If the changed grade is used to determine completion for 1281 * the course-module, then the completion status will be updated. 1282 * 1283 * @param stdClass|cm_info $cm Course-module for item that owns grade 1284 * @param grade_item $item Grade item 1285 * @param stdClass $grade 1286 * @param bool $deleted 1287 */ 1288 public function inform_grade_changed($cm, $item, $grade, $deleted) { 1289 // Bail out now if completion is not enabled for course-module, it is enabled 1290 // but is set to manual, grade is not used to compute completion, or this 1291 // is a different numbered grade 1292 if (!$this->is_enabled($cm) || 1293 $cm->completion == COMPLETION_TRACKING_MANUAL || 1294 is_null($cm->completiongradeitemnumber) || 1295 $item->itemnumber != $cm->completiongradeitemnumber) { 1296 return; 1297 } 1298 1299 // What is the expected result based on this grade? 1300 if ($deleted) { 1301 // Grade being deleted, so only change could be to make it incomplete 1302 $possibleresult = COMPLETION_INCOMPLETE; 1303 } else { 1304 $possibleresult = self::internal_get_grade_state($item, $grade); 1305 } 1306 1307 // OK, let's update state based on this 1308 $this->update_state($cm, $possibleresult, $grade->userid); 1309 } 1310 1311 /** 1312 * Calculates the completion state that would result from a graded item 1313 * (where grade-based completion is turned on) based on the actual grade 1314 * and settings. 1315 * 1316 * Internal function. Not private, so we can unit-test it. 1317 * 1318 * @param grade_item $item an instance of grade_item 1319 * @param grade_grade $grade an instance of grade_grade 1320 * @return int Completion state e.g. COMPLETION_INCOMPLETE 1321 */ 1322 public static function internal_get_grade_state($item, $grade) { 1323 // If no grade is supplied or the grade doesn't have an actual value, then 1324 // this is not complete. 1325 if (!$grade || (is_null($grade->finalgrade) && is_null($grade->rawgrade))) { 1326 return COMPLETION_INCOMPLETE; 1327 } 1328 1329 // Conditions to show pass/fail: 1330 // a) Grade has pass mark (default is 0.00000 which is boolean true so be careful) 1331 // b) Grade is visible (neither hidden nor hidden-until) 1332 if ($item->gradepass && $item->gradepass > 0.000009 && !$item->hidden) { 1333 // Use final grade if set otherwise raw grade 1334 $score = !is_null($grade->finalgrade) ? $grade->finalgrade : $grade->rawgrade; 1335 1336 // We are displaying and tracking pass/fail 1337 if ($score >= $item->gradepass) { 1338 return COMPLETION_COMPLETE_PASS; 1339 } else { 1340 return COMPLETION_COMPLETE_FAIL; 1341 } 1342 } else { 1343 // Not displaying pass/fail, so just if there is a grade 1344 if (!is_null($grade->finalgrade) || !is_null($grade->rawgrade)) { 1345 // Grade exists, so maybe complete now 1346 return COMPLETION_COMPLETE; 1347 } else { 1348 // Grade does not exist, so maybe incomplete now 1349 return COMPLETION_INCOMPLETE; 1350 } 1351 } 1352 } 1353 1354 /** 1355 * Aggregate activity completion state 1356 * 1357 * @param int $type Aggregation type (COMPLETION_* constant) 1358 * @param bool $old Old state 1359 * @param bool $new New state 1360 * @return bool 1361 */ 1362 public static function aggregate_completion_states($type, $old, $new) { 1363 if ($type == COMPLETION_AND) { 1364 return $old && $new; 1365 } else { 1366 return $old || $new; 1367 } 1368 } 1369 1370 /** 1371 * This is to be used only for system errors (things that shouldn't happen) 1372 * and not user-level errors. 1373 * 1374 * @global type $CFG 1375 * @param string $error Error string (will not be displayed to user unless debugging is enabled) 1376 * @throws moodle_exception Exception with the error string as debug info 1377 */ 1378 public function internal_systemerror($error) { 1379 global $CFG; 1380 throw new moodle_exception('err_system','completion', 1381 $CFG->wwwroot.'/course/view.php?id='.$this->course->id,null,$error); 1382 } 1383 } 1384 1385 /** 1386 * Aggregate criteria status's as per configured aggregation method. 1387 * 1388 * @param int $method COMPLETION_AGGREGATION_* constant. 1389 * @param bool $data Criteria completion status. 1390 * @param bool|null $state Aggregation state. 1391 */ 1392 function completion_cron_aggregate($method, $data, &$state) { 1393 if ($method == COMPLETION_AGGREGATION_ALL) { 1394 if ($data && $state !== false) { 1395 $state = true; 1396 } else { 1397 $state = false; 1398 } 1399 } else if ($method == COMPLETION_AGGREGATION_ANY) { 1400 if ($data) { 1401 $state = true; 1402 } else if (!$data && $state === null) { 1403 $state = false; 1404 } 1405 } 1406 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body