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