[ Index ] |
PHP Cross Reference of Unnamed Project |
[Summary view] [Print] [Text view]
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; 323 $result = ''; 324 if ($this->is_enabled() && !$PAGE->user_is_editing() && isloggedin() && !isguestuser()) { 325 $result .= html_writer::tag('div', get_string('yourprogress','completion') . 326 $OUTPUT->help_icon('completionicons', 'completion'), array('id' => 'completionprogressid', 327 'class' => 'completionprogress')); 328 } 329 return $result; 330 } 331 332 /** 333 * Get a course completion for a user 334 * 335 * @param int $user_id User id 336 * @param int $criteriatype Specific criteria type to return 337 * @return bool|completion_criteria_completion returns false on fail 338 */ 339 public function get_completion($user_id, $criteriatype) { 340 $completions = $this->get_completions($user_id, $criteriatype); 341 342 if (empty($completions)) { 343 return false; 344 } elseif (count($completions) > 1) { 345 print_error('multipleselfcompletioncriteria', 'completion'); 346 } 347 348 return $completions[0]; 349 } 350 351 /** 352 * Get all course criteria's completion objects for a user 353 * 354 * @param int $user_id User id 355 * @param int $criteriatype Specific criteria type to return (optional) 356 * @return array 357 */ 358 public function get_completions($user_id, $criteriatype = null) { 359 $criterion = $this->get_criteria($criteriatype); 360 361 $completions = array(); 362 363 foreach ($criterion as $criteria) { 364 $params = array( 365 'course' => $this->course_id, 366 'userid' => $user_id, 367 'criteriaid' => $criteria->id 368 ); 369 370 $completion = new completion_criteria_completion($params); 371 $completion->attach_criteria($criteria); 372 373 $completions[] = $completion; 374 } 375 376 return $completions; 377 } 378 379 /** 380 * Get completion object for a user and a criteria 381 * 382 * @param int $user_id User id 383 * @param completion_criteria $criteria Criteria object 384 * @return completion_criteria_completion 385 */ 386 public function get_user_completion($user_id, $criteria) { 387 $params = array( 388 'course' => $this->course_id, 389 'userid' => $user_id, 390 'criteriaid' => $criteria->id, 391 ); 392 393 $completion = new completion_criteria_completion($params); 394 return $completion; 395 } 396 397 /** 398 * Check if course has completion criteria set 399 * 400 * @return bool Returns true if there are criteria 401 */ 402 public function has_criteria() { 403 $criteria = $this->get_criteria(); 404 405 return (bool) count($criteria); 406 } 407 408 /** 409 * Get course completion criteria 410 * 411 * @param int $criteriatype Specific criteria type to return (optional) 412 */ 413 public function get_criteria($criteriatype = null) { 414 415 // Fill cache if empty 416 if (!is_array($this->criteria)) { 417 global $DB; 418 419 $params = array( 420 'course' => $this->course->id 421 ); 422 423 // Load criteria from database 424 $records = (array)$DB->get_records('course_completion_criteria', $params); 425 426 // Build array of criteria objects 427 $this->criteria = array(); 428 foreach ($records as $record) { 429 $this->criteria[$record->id] = completion_criteria::factory((array)$record); 430 } 431 } 432 433 // If after all criteria 434 if ($criteriatype === null) { 435 return $this->criteria; 436 } 437 438 // If we are only after a specific criteria type 439 $criteria = array(); 440 foreach ($this->criteria as $criterion) { 441 442 if ($criterion->criteriatype != $criteriatype) { 443 continue; 444 } 445 446 $criteria[$criterion->id] = $criterion; 447 } 448 449 return $criteria; 450 } 451 452 /** 453 * Get aggregation method 454 * 455 * @param int $criteriatype If none supplied, get overall aggregation method (optional) 456 * @return int One of COMPLETION_AGGREGATION_ALL or COMPLETION_AGGREGATION_ANY 457 */ 458 public function get_aggregation_method($criteriatype = null) { 459 $params = array( 460 'course' => $this->course_id, 461 'criteriatype' => $criteriatype 462 ); 463 464 $aggregation = new completion_aggregation($params); 465 466 if (!$aggregation->id) { 467 $aggregation->method = COMPLETION_AGGREGATION_ALL; 468 } 469 470 return $aggregation->method; 471 } 472 473 /** 474 * @deprecated since Moodle 2.8 MDL-46290. 475 */ 476 public function get_incomplete_criteria() { 477 throw new coding_exception('completion_info->get_incomplete_criteria() is removed.'); 478 } 479 480 /** 481 * Clear old course completion criteria 482 */ 483 public function clear_criteria() { 484 global $DB; 485 $DB->delete_records('course_completion_criteria', array('course' => $this->course_id)); 486 $DB->delete_records('course_completion_aggr_methd', array('course' => $this->course_id)); 487 488 $this->delete_course_completion_data(); 489 } 490 491 /** 492 * Has the supplied user completed this course 493 * 494 * @param int $user_id User's id 495 * @return boolean 496 */ 497 public function is_course_complete($user_id) { 498 $params = array( 499 'userid' => $user_id, 500 'course' => $this->course_id 501 ); 502 503 $ccompletion = new completion_completion($params); 504 return $ccompletion->is_complete(); 505 } 506 507 /** 508 * Updates (if necessary) the completion state of activity $cm for the given 509 * user. 510 * 511 * For manual completion, this function is called when completion is toggled 512 * with $possibleresult set to the target state. 513 * 514 * For automatic completion, this function should be called every time a module 515 * does something which might influence a user's completion state. For example, 516 * if a forum provides options for marking itself 'completed' once a user makes 517 * N posts, this function should be called every time a user makes a new post. 518 * [After the post has been saved to the database]. When calling, you do not 519 * need to pass in the new completion state. Instead this function carries out 520 * completion calculation by checking grades and viewed state itself, and 521 * calling the involved module via modulename_get_completion_state() to check 522 * module-specific conditions. 523 * 524 * @param stdClass|cm_info $cm Course-module 525 * @param int $possibleresult Expected completion result. If the event that 526 * has just occurred (e.g. add post) can only result in making the activity 527 * complete when it wasn't before, use COMPLETION_COMPLETE. If the event that 528 * has just occurred (e.g. delete post) can only result in making the activity 529 * not complete when it was previously complete, use COMPLETION_INCOMPLETE. 530 * Otherwise use COMPLETION_UNKNOWN. Setting this value to something other than 531 * COMPLETION_UNKNOWN significantly improves performance because it will abandon 532 * processing early if the user's completion state already matches the expected 533 * result. For manual events, COMPLETION_COMPLETE or COMPLETION_INCOMPLETE 534 * must be used; these directly set the specified state. 535 * @param int $userid User ID to be updated. Default 0 = current user 536 * @return void 537 */ 538 public function update_state($cm, $possibleresult=COMPLETION_UNKNOWN, $userid=0) { 539 global $USER; 540 541 // Do nothing if completion is not enabled for that activity 542 if (!$this->is_enabled($cm)) { 543 return; 544 } 545 546 // Get current value of completion state and do nothing if it's same as 547 // the possible result of this change. If the change is to COMPLETE and the 548 // current value is one of the COMPLETE_xx subtypes, ignore that as well 549 $current = $this->get_data($cm, false, $userid); 550 if ($possibleresult == $current->completionstate || 551 ($possibleresult == COMPLETION_COMPLETE && 552 ($current->completionstate == COMPLETION_COMPLETE_PASS || 553 $current->completionstate == COMPLETION_COMPLETE_FAIL))) { 554 return; 555 } 556 557 if ($cm->completion == COMPLETION_TRACKING_MANUAL) { 558 // For manual tracking we set the result directly 559 switch($possibleresult) { 560 case COMPLETION_COMPLETE: 561 case COMPLETION_INCOMPLETE: 562 $newstate = $possibleresult; 563 break; 564 default: 565 $this->internal_systemerror("Unexpected manual completion state for {$cm->id}: $possibleresult"); 566 } 567 568 } else { 569 // Automatic tracking; get new state 570 $newstate = $this->internal_get_state($cm, $userid, $current); 571 } 572 573 // If changed, update 574 if ($newstate != $current->completionstate) { 575 $current->completionstate = $newstate; 576 $current->timemodified = time(); 577 $this->internal_set_data($cm, $current); 578 } 579 } 580 581 /** 582 * Calculates the completion state for an activity and user. 583 * 584 * Internal function. Not private, so we can unit-test it. 585 * 586 * @param stdClass|cm_info $cm Activity 587 * @param int $userid ID of user 588 * @param stdClass $current Previous completion information from database 589 * @return mixed 590 */ 591 public function internal_get_state($cm, $userid, $current) { 592 global $USER, $DB, $CFG; 593 594 // Get user ID 595 if (!$userid) { 596 $userid = $USER->id; 597 } 598 599 // Check viewed 600 if ($cm->completionview == COMPLETION_VIEW_REQUIRED && 601 $current->viewed == COMPLETION_NOT_VIEWED) { 602 603 return COMPLETION_INCOMPLETE; 604 } 605 606 // Modname hopefully is provided in $cm but just in case it isn't, let's grab it 607 if (!isset($cm->modname)) { 608 $cm->modname = $DB->get_field('modules', 'name', array('id'=>$cm->module)); 609 } 610 611 $newstate = COMPLETION_COMPLETE; 612 613 // Check grade 614 if (!is_null($cm->completiongradeitemnumber)) { 615 require_once($CFG->libdir.'/gradelib.php'); 616 $item = grade_item::fetch(array('courseid'=>$cm->course, 'itemtype'=>'mod', 617 'itemmodule'=>$cm->modname, 'iteminstance'=>$cm->instance, 618 'itemnumber'=>$cm->completiongradeitemnumber)); 619 if ($item) { 620 // Fetch 'grades' (will be one or none) 621 $grades = grade_grade::fetch_users_grades($item, array($userid), false); 622 if (empty($grades)) { 623 // No grade for user 624 return COMPLETION_INCOMPLETE; 625 } 626 if (count($grades) > 1) { 627 $this->internal_systemerror("Unexpected result: multiple grades for 628 item '{$item->id}', user '{$userid}'"); 629 } 630 $newstate = self::internal_get_grade_state($item, reset($grades)); 631 if ($newstate == COMPLETION_INCOMPLETE) { 632 return COMPLETION_INCOMPLETE; 633 } 634 635 } else { 636 $this->internal_systemerror("Cannot find grade item for '{$cm->modname}' 637 cm '{$cm->id}' matching number '{$cm->completiongradeitemnumber}'"); 638 } 639 } 640 641 if (plugin_supports('mod', $cm->modname, FEATURE_COMPLETION_HAS_RULES)) { 642 $function = $cm->modname.'_get_completion_state'; 643 if (!function_exists($function)) { 644 $this->internal_systemerror("Module {$cm->modname} claims to support 645 FEATURE_COMPLETION_HAS_RULES but does not have required 646 {$cm->modname}_get_completion_state function"); 647 } 648 if (!$function($this->course, $cm, $userid, COMPLETION_AND)) { 649 return COMPLETION_INCOMPLETE; 650 } 651 } 652 653 return $newstate; 654 655 } 656 657 /** 658 * Marks a module as viewed. 659 * 660 * Should be called whenever a module is 'viewed' (it is up to the module how to 661 * determine that). Has no effect if viewing is not set as a completion condition. 662 * 663 * Note that this function must be called before you print the page header because 664 * it is possible that the navigation block may depend on it. If you call it after 665 * printing the header, it shows a developer debug warning. 666 * 667 * @param stdClass|cm_info $cm Activity 668 * @param int $userid User ID or 0 (default) for current user 669 * @return void 670 */ 671 public function set_module_viewed($cm, $userid=0) { 672 global $PAGE; 673 if ($PAGE->headerprinted) { 674 debugging('set_module_viewed must be called before header is printed', 675 DEBUG_DEVELOPER); 676 } 677 678 // Don't do anything if view condition is not turned on 679 if ($cm->completionview == COMPLETION_VIEW_NOT_REQUIRED || !$this->is_enabled($cm)) { 680 return; 681 } 682 683 // Get current completion state 684 $data = $this->get_data($cm, false, $userid); 685 686 // If we already viewed it, don't do anything 687 if ($data->viewed == COMPLETION_VIEWED) { 688 return; 689 } 690 691 // OK, change state, save it, and update completion 692 $data->viewed = COMPLETION_VIEWED; 693 $this->internal_set_data($cm, $data); 694 $this->update_state($cm, COMPLETION_COMPLETE, $userid); 695 } 696 697 /** 698 * Determines how much completion data exists for an activity. This is used when 699 * deciding whether completion information should be 'locked' in the module 700 * editing form. 701 * 702 * @param cm_info $cm Activity 703 * @return int The number of users who have completion data stored for this 704 * activity, 0 if none 705 */ 706 public function count_user_data($cm) { 707 global $DB; 708 709 return $DB->get_field_sql(" 710 SELECT 711 COUNT(1) 712 FROM 713 {course_modules_completion} 714 WHERE 715 coursemoduleid=? AND completionstate<>0", array($cm->id)); 716 } 717 718 /** 719 * Determines how much course completion data exists for a course. This is used when 720 * deciding whether completion information should be 'locked' in the completion 721 * settings form and activity completion settings. 722 * 723 * @param int $user_id Optionally only get course completion data for a single user 724 * @return int The number of users who have completion data stored for this 725 * course, 0 if none 726 */ 727 public function count_course_user_data($user_id = null) { 728 global $DB; 729 730 $sql = ' 731 SELECT 732 COUNT(1) 733 FROM 734 {course_completion_crit_compl} 735 WHERE 736 course = ? 737 '; 738 739 $params = array($this->course_id); 740 741 // Limit data to a single user if an ID is supplied 742 if ($user_id) { 743 $sql .= ' AND userid = ?'; 744 $params[] = $user_id; 745 } 746 747 return $DB->get_field_sql($sql, $params); 748 } 749 750 /** 751 * Check if this course's completion criteria should be locked 752 * 753 * @return boolean 754 */ 755 public function is_course_locked() { 756 return (bool) $this->count_course_user_data(); 757 } 758 759 /** 760 * Deletes all course completion completion data. 761 * 762 * Intended to be used when unlocking completion criteria settings. 763 */ 764 public function delete_course_completion_data() { 765 global $DB; 766 767 $DB->delete_records('course_completions', array('course' => $this->course_id)); 768 $DB->delete_records('course_completion_crit_compl', array('course' => $this->course_id)); 769 770 // Difficult to find affected users, just purge all completion cache. 771 cache::make('core', 'completion')->purge(); 772 } 773 774 /** 775 * Deletes all activity and course completion data for an entire course 776 * (the below delete_all_state function does this for a single activity). 777 * 778 * Used by course reset page. 779 */ 780 public function delete_all_completion_data() { 781 global $DB; 782 783 // Delete from database. 784 $DB->delete_records_select('course_modules_completion', 785 'coursemoduleid IN (SELECT id FROM {course_modules} WHERE course=?)', 786 array($this->course_id)); 787 788 // Wipe course completion data too. 789 $this->delete_course_completion_data(); 790 } 791 792 /** 793 * Deletes completion state related to an activity for all users. 794 * 795 * Intended for use only when the activity itself is deleted. 796 * 797 * @param stdClass|cm_info $cm Activity 798 */ 799 public function delete_all_state($cm) { 800 global $DB; 801 802 // Delete from database 803 $DB->delete_records('course_modules_completion', array('coursemoduleid'=>$cm->id)); 804 805 // Check if there is an associated course completion criteria 806 $criteria = $this->get_criteria(COMPLETION_CRITERIA_TYPE_ACTIVITY); 807 $acriteria = false; 808 foreach ($criteria as $criterion) { 809 if ($criterion->moduleinstance == $cm->id) { 810 $acriteria = $criterion; 811 break; 812 } 813 } 814 815 if ($acriteria) { 816 // Delete all criteria completions relating to this activity 817 $DB->delete_records('course_completion_crit_compl', array('course' => $this->course_id, 'criteriaid' => $acriteria->id)); 818 $DB->delete_records('course_completions', array('course' => $this->course_id)); 819 } 820 821 // Difficult to find affected users, just purge all completion cache. 822 cache::make('core', 'completion')->purge(); 823 } 824 825 /** 826 * Recalculates completion state related to an activity for all users. 827 * 828 * Intended for use if completion conditions change. (This should be avoided 829 * as it may cause some things to become incomplete when they were previously 830 * complete, with the effect - for example - of hiding a later activity that 831 * was previously available.) 832 * 833 * Resetting state of manual tickbox has same result as deleting state for 834 * it. 835 * 836 * @param stcClass|cm_info $cm Activity 837 */ 838 public function reset_all_state($cm) { 839 global $DB; 840 841 if ($cm->completion == COMPLETION_TRACKING_MANUAL) { 842 $this->delete_all_state($cm); 843 return; 844 } 845 // Get current list of users with completion state 846 $rs = $DB->get_recordset('course_modules_completion', array('coursemoduleid'=>$cm->id), '', 'userid'); 847 $keepusers = array(); 848 foreach ($rs as $rec) { 849 $keepusers[] = $rec->userid; 850 } 851 $rs->close(); 852 853 // Delete all existing state. 854 $this->delete_all_state($cm); 855 856 // Merge this with list of planned users (according to roles) 857 $trackedusers = $this->get_tracked_users(); 858 foreach ($trackedusers as $trackeduser) { 859 $keepusers[] = $trackeduser->id; 860 } 861 $keepusers = array_unique($keepusers); 862 863 // Recalculate state for each kept user 864 foreach ($keepusers as $keepuser) { 865 $this->update_state($cm, COMPLETION_UNKNOWN, $keepuser); 866 } 867 } 868 869 /** 870 * Obtains completion data for a particular activity and user (from the 871 * completion cache if available, or by SQL query) 872 * 873 * @param stcClass|cm_info $cm Activity; only required field is ->id 874 * @param bool $wholecourse If true (default false) then, when necessary to 875 * fill the cache, retrieves information from the entire course not just for 876 * this one activity 877 * @param int $userid User ID or 0 (default) for current user 878 * @param array $modinfo Supply the value here - this is used for unit 879 * testing and so that it can be called recursively from within 880 * get_fast_modinfo. (Needs only list of all CMs with IDs.) 881 * Otherwise the method calls get_fast_modinfo itself. 882 * @return object Completion data (record from course_modules_completion) 883 */ 884 public function get_data($cm, $wholecourse = false, $userid = 0, $modinfo = null) { 885 global $USER, $CFG, $DB; 886 $completioncache = cache::make('core', 'completion'); 887 888 // Get user ID 889 if (!$userid) { 890 $userid = $USER->id; 891 } 892 893 // See if requested data is present in cache (use cache for current user only). 894 $usecache = $userid == $USER->id; 895 $cacheddata = array(); 896 if ($usecache) { 897 if (!isset($this->course->cacherev)) { 898 $this->course = get_course($this->course_id); 899 } 900 if ($cacheddata = $completioncache->get($userid . '_' . $this->course->id)) { 901 if ($cacheddata['cacherev'] != $this->course->cacherev) { 902 // Course structure has been changed since the last caching, forget the cache. 903 $cacheddata = array(); 904 } else if (array_key_exists($cm->id, $cacheddata)) { 905 return $cacheddata[$cm->id]; 906 } 907 } 908 } 909 910 // Not there, get via SQL 911 if ($usecache && $wholecourse) { 912 // Get whole course data for cache 913 $alldatabycmc = $DB->get_records_sql(" 914 SELECT 915 cmc.* 916 FROM 917 {course_modules} cm 918 INNER JOIN {course_modules_completion} cmc ON cmc.coursemoduleid=cm.id 919 WHERE 920 cm.course=? AND cmc.userid=?", array($this->course->id, $userid)); 921 922 // Reindex by cm id 923 $alldata = array(); 924 if ($alldatabycmc) { 925 foreach ($alldatabycmc as $data) { 926 $alldata[$data->coursemoduleid] = $data; 927 } 928 } 929 930 // Get the module info and build up condition info for each one 931 if (empty($modinfo)) { 932 $modinfo = get_fast_modinfo($this->course, $userid); 933 } 934 foreach ($modinfo->cms as $othercm) { 935 if (array_key_exists($othercm->id, $alldata)) { 936 $data = $alldata[$othercm->id]; 937 } else { 938 // Row not present counts as 'not complete' 939 $data = new StdClass; 940 $data->id = 0; 941 $data->coursemoduleid = $othercm->id; 942 $data->userid = $userid; 943 $data->completionstate = 0; 944 $data->viewed = 0; 945 $data->timemodified = 0; 946 } 947 $cacheddata[$othercm->id] = $data; 948 } 949 950 if (!isset($cacheddata[$cm->id])) { 951 $this->internal_systemerror("Unexpected error: course-module {$cm->id} could not be found on course {$this->course->id}"); 952 } 953 954 } else { 955 // Get single record 956 $data = $DB->get_record('course_modules_completion', array('coursemoduleid'=>$cm->id, 'userid'=>$userid)); 957 if ($data == false) { 958 // Row not present counts as 'not complete' 959 $data = new StdClass; 960 $data->id = 0; 961 $data->coursemoduleid = $cm->id; 962 $data->userid = $userid; 963 $data->completionstate = 0; 964 $data->viewed = 0; 965 $data->timemodified = 0; 966 } 967 968 // Put in cache 969 $cacheddata[$cm->id] = $data; 970 } 971 972 if ($usecache) { 973 $cacheddata['cacherev'] = $this->course->cacherev; 974 $completioncache->set($userid . '_' . $this->course->id, $cacheddata); 975 } 976 return $cacheddata[$cm->id]; 977 } 978 979 /** 980 * Updates completion data for a particular coursemodule and user (user is 981 * determined from $data). 982 * 983 * (Internal function. Not private, so we can unit-test it.) 984 * 985 * @param stdClass|cm_info $cm Activity 986 * @param stdClass $data Data about completion for that user 987 */ 988 public function internal_set_data($cm, $data) { 989 global $USER, $DB; 990 991 $transaction = $DB->start_delegated_transaction(); 992 if (!$data->id) { 993 // Check there isn't really a row 994 $data->id = $DB->get_field('course_modules_completion', 'id', 995 array('coursemoduleid'=>$data->coursemoduleid, 'userid'=>$data->userid)); 996 } 997 if (!$data->id) { 998 // Didn't exist before, needs creating 999 $data->id = $DB->insert_record('course_modules_completion', $data); 1000 } else { 1001 // Has real (nonzero) id meaning that a database row exists, update 1002 $DB->update_record('course_modules_completion', $data); 1003 } 1004 $transaction->allow_commit(); 1005 1006 $cmcontext = context_module::instance($data->coursemoduleid, MUST_EXIST); 1007 $coursecontext = $cmcontext->get_parent_context(); 1008 1009 $completioncache = cache::make('core', 'completion'); 1010 if ($data->userid == $USER->id) { 1011 // Update module completion in user's cache. 1012 if (!($cachedata = $completioncache->get($data->userid . '_' . $cm->course)) 1013 || $cachedata['cacherev'] != $this->course->cacherev) { 1014 $cachedata = array('cacherev' => $this->course->cacherev); 1015 } 1016 $cachedata[$cm->id] = $data; 1017 $completioncache->set($data->userid . '_' . $cm->course, $cachedata); 1018 1019 // reset modinfo for user (no need to call rebuild_course_cache()) 1020 get_fast_modinfo($cm->course, 0, true); 1021 } else { 1022 // Remove another user's completion cache for this course. 1023 $completioncache->delete($data->userid . '_' . $cm->course); 1024 } 1025 1026 // Trigger an event for course module completion changed. 1027 $event = \core\event\course_module_completion_updated::create(array( 1028 'objectid' => $data->id, 1029 'context' => $cmcontext, 1030 'relateduserid' => $data->userid, 1031 'other' => array( 1032 'relateduserid' => $data->userid 1033 ) 1034 )); 1035 $event->add_record_snapshot('course_modules_completion', $data); 1036 $event->trigger(); 1037 } 1038 1039 /** 1040 * Return whether or not the course has activities with completion enabled. 1041 * 1042 * @return boolean true when there is at least one activity with completion enabled. 1043 */ 1044 public function has_activities() { 1045 $modinfo = get_fast_modinfo($this->course); 1046 foreach ($modinfo->get_cms() as $cm) { 1047 if ($cm->completion != COMPLETION_TRACKING_NONE) { 1048 return true; 1049 } 1050 } 1051 return false; 1052 } 1053 1054 /** 1055 * Obtains a list of activities for which completion is enabled on the 1056 * course. The list is ordered by the section order of those activities. 1057 * 1058 * @return cm_info[] Array from $cmid => $cm of all activities with completion enabled, 1059 * empty array if none 1060 */ 1061 public function get_activities() { 1062 $modinfo = get_fast_modinfo($this->course); 1063 $result = array(); 1064 foreach ($modinfo->get_cms() as $cm) { 1065 if ($cm->completion != COMPLETION_TRACKING_NONE) { 1066 $result[$cm->id] = $cm; 1067 } 1068 } 1069 return $result; 1070 } 1071 1072 /** 1073 * Checks to see if the userid supplied has a tracked role in 1074 * this course 1075 * 1076 * @param int $userid User id 1077 * @return bool 1078 */ 1079 public function is_tracked_user($userid) { 1080 return is_enrolled(context_course::instance($this->course->id), $userid, 'moodle/course:isincompletionreports', true); 1081 } 1082 1083 /** 1084 * Returns the number of users whose progress is tracked in this course. 1085 * 1086 * Optionally supply a search's where clause, or a group id. 1087 * 1088 * @param string $where Where clause sql (use 'u.whatever' for user table fields) 1089 * @param array $whereparams Where clause params 1090 * @param int $groupid Group id 1091 * @return int Number of tracked users 1092 */ 1093 public function get_num_tracked_users($where = '', $whereparams = array(), $groupid = 0) { 1094 global $DB; 1095 1096 list($enrolledsql, $enrolledparams) = get_enrolled_sql( 1097 context_course::instance($this->course->id), 'moodle/course:isincompletionreports', $groupid, true); 1098 $sql = 'SELECT COUNT(eu.id) FROM (' . $enrolledsql . ') eu JOIN {user} u ON u.id = eu.id'; 1099 if ($where) { 1100 $sql .= " WHERE $where"; 1101 } 1102 1103 $params = array_merge($enrolledparams, $whereparams); 1104 return $DB->count_records_sql($sql, $params); 1105 } 1106 1107 /** 1108 * Return array of users whose progress is tracked in this course. 1109 * 1110 * Optionally supply a search's where clause, group id, sorting, paging. 1111 * 1112 * @param string $where Where clause sql, referring to 'u.' fields (optional) 1113 * @param array $whereparams Where clause params (optional) 1114 * @param int $groupid Group ID to restrict to (optional) 1115 * @param string $sort Order by clause (optional) 1116 * @param int $limitfrom Result start (optional) 1117 * @param int $limitnum Result max size (optional) 1118 * @param context $extracontext If set, includes extra user information fields 1119 * as appropriate to display for current user in this context 1120 * @return array Array of user objects with standard user fields 1121 */ 1122 public function get_tracked_users($where = '', $whereparams = array(), $groupid = 0, 1123 $sort = '', $limitfrom = '', $limitnum = '', context $extracontext = null) { 1124 1125 global $DB; 1126 1127 list($enrolledsql, $params) = get_enrolled_sql( 1128 context_course::instance($this->course->id), 1129 'moodle/course:isincompletionreports', $groupid, true); 1130 1131 $allusernames = get_all_user_name_fields(true, 'u'); 1132 $sql = 'SELECT u.id, u.idnumber, ' . $allusernames; 1133 if ($extracontext) { 1134 $sql .= get_extra_user_fields_sql($extracontext, 'u', '', array('idnumber')); 1135 } 1136 $sql .= ' FROM (' . $enrolledsql . ') eu JOIN {user} u ON u.id = eu.id'; 1137 1138 if ($where) { 1139 $sql .= " AND $where"; 1140 $params = array_merge($params, $whereparams); 1141 } 1142 1143 if ($sort) { 1144 $sql .= " ORDER BY $sort"; 1145 } 1146 1147 return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum); 1148 } 1149 1150 /** 1151 * Obtains progress information across a course for all users on that course, or 1152 * for all users in a specific group. Intended for use when displaying progress. 1153 * 1154 * This includes only users who, in course context, have one of the roles for 1155 * which progress is tracked (the gradebookroles admin option) and are enrolled in course. 1156 * 1157 * Users are included (in the first array) even if they do not have 1158 * completion progress for any course-module. 1159 * 1160 * @param bool $sortfirstname If true, sort by first name, otherwise sort by 1161 * last name 1162 * @param string $where Where clause sql (optional) 1163 * @param array $where_params Where clause params (optional) 1164 * @param int $groupid Group ID or 0 (default)/false for all groups 1165 * @param int $pagesize Number of users to actually return (optional) 1166 * @param int $start User to start at if paging (optional) 1167 * @param context $extracontext If set, includes extra user information fields 1168 * as appropriate to display for current user in this context 1169 * @return stdClass with ->total and ->start (same as $start) and ->users; 1170 * an array of user objects (like mdl_user id, firstname, lastname) 1171 * containing an additional ->progress array of coursemoduleid => completionstate 1172 */ 1173 public function get_progress_all($where = '', $where_params = array(), $groupid = 0, 1174 $sort = '', $pagesize = '', $start = '', context $extracontext = null) { 1175 global $CFG, $DB; 1176 1177 // Get list of applicable users 1178 $users = $this->get_tracked_users($where, $where_params, $groupid, $sort, 1179 $start, $pagesize, $extracontext); 1180 1181 // Get progress information for these users in groups of 1, 000 (if needed) 1182 // to avoid making the SQL IN too long 1183 $results = array(); 1184 $userids = array(); 1185 foreach ($users as $user) { 1186 $userids[] = $user->id; 1187 $results[$user->id] = $user; 1188 $results[$user->id]->progress = array(); 1189 } 1190 1191 for($i=0; $i<count($userids); $i+=1000) { 1192 $blocksize = count($userids)-$i < 1000 ? count($userids)-$i : 1000; 1193 1194 list($insql, $params) = $DB->get_in_or_equal(array_slice($userids, $i, $blocksize)); 1195 array_splice($params, 0, 0, array($this->course->id)); 1196 $rs = $DB->get_recordset_sql(" 1197 SELECT 1198 cmc.* 1199 FROM 1200 {course_modules} cm 1201 INNER JOIN {course_modules_completion} cmc ON cm.id=cmc.coursemoduleid 1202 WHERE 1203 cm.course=? AND cmc.userid $insql", $params); 1204 foreach ($rs as $progress) { 1205 $progress = (object)$progress; 1206 $results[$progress->userid]->progress[$progress->coursemoduleid] = $progress; 1207 } 1208 $rs->close(); 1209 } 1210 1211 return $results; 1212 } 1213 1214 /** 1215 * Called by grade code to inform the completion system when a grade has 1216 * been changed. If the changed grade is used to determine completion for 1217 * the course-module, then the completion status will be updated. 1218 * 1219 * @param stdClass|cm_info $cm Course-module for item that owns grade 1220 * @param grade_item $item Grade item 1221 * @param stdClass $grade 1222 * @param bool $deleted 1223 */ 1224 public function inform_grade_changed($cm, $item, $grade, $deleted) { 1225 // Bail out now if completion is not enabled for course-module, it is enabled 1226 // but is set to manual, grade is not used to compute completion, or this 1227 // is a different numbered grade 1228 if (!$this->is_enabled($cm) || 1229 $cm->completion == COMPLETION_TRACKING_MANUAL || 1230 is_null($cm->completiongradeitemnumber) || 1231 $item->itemnumber != $cm->completiongradeitemnumber) { 1232 return; 1233 } 1234 1235 // What is the expected result based on this grade? 1236 if ($deleted) { 1237 // Grade being deleted, so only change could be to make it incomplete 1238 $possibleresult = COMPLETION_INCOMPLETE; 1239 } else { 1240 $possibleresult = self::internal_get_grade_state($item, $grade); 1241 } 1242 1243 // OK, let's update state based on this 1244 $this->update_state($cm, $possibleresult, $grade->userid); 1245 } 1246 1247 /** 1248 * Calculates the completion state that would result from a graded item 1249 * (where grade-based completion is turned on) based on the actual grade 1250 * and settings. 1251 * 1252 * Internal function. Not private, so we can unit-test it. 1253 * 1254 * @param grade_item $item an instance of grade_item 1255 * @param grade_grade $grade an instance of grade_grade 1256 * @return int Completion state e.g. COMPLETION_INCOMPLETE 1257 */ 1258 public static function internal_get_grade_state($item, $grade) { 1259 // If no grade is supplied or the grade doesn't have an actual value, then 1260 // this is not complete. 1261 if (!$grade || (is_null($grade->finalgrade) && is_null($grade->rawgrade))) { 1262 return COMPLETION_INCOMPLETE; 1263 } 1264 1265 // Conditions to show pass/fail: 1266 // a) Grade has pass mark (default is 0.00000 which is boolean true so be careful) 1267 // b) Grade is visible (neither hidden nor hidden-until) 1268 if ($item->gradepass && $item->gradepass > 0.000009 && !$item->hidden) { 1269 // Use final grade if set otherwise raw grade 1270 $score = !is_null($grade->finalgrade) ? $grade->finalgrade : $grade->rawgrade; 1271 1272 // We are displaying and tracking pass/fail 1273 if ($score >= $item->gradepass) { 1274 return COMPLETION_COMPLETE_PASS; 1275 } else { 1276 return COMPLETION_COMPLETE_FAIL; 1277 } 1278 } else { 1279 // Not displaying pass/fail, so just if there is a grade 1280 if (!is_null($grade->finalgrade) || !is_null($grade->rawgrade)) { 1281 // Grade exists, so maybe complete now 1282 return COMPLETION_COMPLETE; 1283 } else { 1284 // Grade does not exist, so maybe incomplete now 1285 return COMPLETION_INCOMPLETE; 1286 } 1287 } 1288 } 1289 1290 /** 1291 * Aggregate activity completion state 1292 * 1293 * @param int $type Aggregation type (COMPLETION_* constant) 1294 * @param bool $old Old state 1295 * @param bool $new New state 1296 * @return bool 1297 */ 1298 public static function aggregate_completion_states($type, $old, $new) { 1299 if ($type == COMPLETION_AND) { 1300 return $old && $new; 1301 } else { 1302 return $old || $new; 1303 } 1304 } 1305 1306 /** 1307 * This is to be used only for system errors (things that shouldn't happen) 1308 * and not user-level errors. 1309 * 1310 * @global type $CFG 1311 * @param string $error Error string (will not be displayed to user unless debugging is enabled) 1312 * @throws moodle_exception Exception with the error string as debug info 1313 */ 1314 public function internal_systemerror($error) { 1315 global $CFG; 1316 throw new moodle_exception('err_system','completion', 1317 $CFG->wwwroot.'/course/view.php?id='.$this->course->id,null,$error); 1318 } 1319 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Thu Aug 11 10:00:09 2016 | Cross-referenced by PHPXref 0.7.1 |