diff --git a/blocks/myoverview/classes/output/courses_view.php b/blocks/myoverview/classes/output/courses_view.php index 31dc8c62813..798eb7b826c 100644 --- a/blocks/myoverview/classes/output/courses_view.php +++ b/blocks/myoverview/classes/output/courses_view.php @@ -63,8 +63,6 @@ class courses_view implements renderable, templatable { * @return array */ public function export_for_template(renderer_base $output) { - $today = time(); - // Build courses view data structure. $coursesview = [ 'hascourses' => !empty($this->courses) @@ -73,8 +71,6 @@ class courses_view implements renderable, templatable { // How many courses we have per status? $coursesbystatus = ['past' => 0, 'inprogress' => 0, 'future' => 0]; foreach ($this->courses as $course) { - $startdate = $course->startdate; - $enddate = $course->enddate; $courseid = $course->id; $context = \context_course::instance($courseid); $exporter = new course_summary_exporter($course, [ @@ -84,14 +80,17 @@ class courses_view implements renderable, templatable { // Convert summary to plain text. $exportedcourse->summary = content_to_text($exportedcourse->summary, $exportedcourse->summaryformat); + $courseprogress = null; + + $classified = course_classify_for_timeline($course); + if (isset($this->coursesprogress[$courseid])) { - $coursecompleted = $this->coursesprogress[$courseid]['completed']; $courseprogress = $this->coursesprogress[$courseid]['progress']; $exportedcourse->hasprogress = !is_null($courseprogress); $exportedcourse->progress = $courseprogress; } - if ((isset($coursecompleted) && $coursecompleted) || (!empty($enddate) && $enddate < $today)) { + if ($classified == COURSE_TIMELINE_PAST) { // Courses that have already ended. $pastpages = floor($coursesbystatus['past'] / $this::COURSES_PER_PAGE); @@ -100,7 +99,7 @@ class courses_view implements renderable, templatable { $coursesview['past']['pages'][$pastpages]['page'] = $pastpages + 1; $coursesview['past']['haspages'] = true; $coursesbystatus['past']++; - } else if ($startdate > $today) { + } else if ($classified == COURSE_TIMELINE_FUTURE) { // Courses that have not started yet. $futurepages = floor($coursesbystatus['future'] / $this::COURSES_PER_PAGE); diff --git a/completion/completion_completion.php b/completion/completion_completion.php index 4e0a33c4436..04b05c4abf7 100644 --- a/completion/completion_completion.php +++ b/completion/completion_completion.php @@ -74,7 +74,16 @@ class completion_completion extends data_object { * @return data_object instance of data_object or false if none found. */ public static function fetch($params) { - return self::fetch_helper('course_completions', __CLASS__, $params); + $cache = cache::make('core', 'coursecompletion'); + + $key = $params['userid'] . '_' . $params['course']; + if ($hit = $cache->get($key)) { + return $hit['value']; + } + + $tocache = self::fetch_helper('course_completions', __CLASS__, $params); + $cache->set($key, ['value' => $tocache]); + return $tocache; } /** @@ -179,9 +188,10 @@ class completion_completion extends data_object { $this->timeenrolled = 0; } + $result = false; // Save record if ($this->id) { - return $this->update(); + $result = $this->update(); } else { // Make sure reaggregate field is not null if (!$this->reaggregate) { @@ -193,7 +203,17 @@ class completion_completion extends data_object { $this->timestarted = 0; } - return $this->insert(); + $result = $this->insert(); } + + if ($result) { + // Update the cached record. + $cache = cache::make('core', 'coursecompletion'); + $data = $this->get_record_data(); + $key = $data->userid . '_' . $data->course; + $cache->set($key, ['value' => $data]); + } + + return $result; } } diff --git a/course/lib.php b/course/lib.php index 6e7a5b704b9..f079a742d7f 100644 --- a/course/lib.php +++ b/course/lib.php @@ -55,6 +55,10 @@ define('FIRSTUSEDEXCELROW', 3); define('MOD_CLASS_ACTIVITY', 0); define('MOD_CLASS_RESOURCE', 1); +define('COURSE_TIMELINE_PAST', 'past'); +define('COURSE_TIMELINE_INPROGRESS', 'inprogress'); +define('COURSE_TIMELINE_FUTURE', 'future'); + function make_log_url($module, $url) { switch ($module) { case 'course': @@ -4003,6 +4007,46 @@ function course_check_updates($course, $tocheck, $filter = array()) { return array($instances, $warnings); } +/** + * This function classifies a course as past, in progress or future. + * + * This function may incur a DB hit to calculate course completion. + * @param stdClass $course Course record + * @param stdClass $user User record (optional - defaults to $USER). + * @param completion_info $completioninfo Completion record for the user (optional - will be fetched if required). + * @return string (one of COURSE_TIMELINE_FUTURE, COURSE_TIMELINE_INPROGRESS or COURSE_TIMELINE_PAST) + */ +function course_classify_for_timeline($course, $user = null, $completioninfo = null) { + global $USER; + + if ($user == null) { + $user = $USER; + } + + $today = time(); + // End date past. + if (!empty($course->enddate) && $course->enddate < $today) { + return COURSE_TIMELINE_PAST; + } + + if ($completioninfo == null) { + $completioninfo = new completion_info($course); + } + + // Course was completed. + if ($completioninfo->is_enabled() && $completioninfo->is_course_complete($user->id)) { + return COURSE_TIMELINE_PAST; + } + + // Start date not reached. + if (!empty($course->startdate) && $course->startdate > $today) { + return COURSE_TIMELINE_FUTURE; + } + + // Everything else is in progress. + return COURSE_TIMELINE_INPROGRESS; +} + /** * Check module updates since a given time. * This function checks for updates in the module config, file areas, completion, grades, comments and ratings. diff --git a/course/tests/courselib_test.php b/course/tests/courselib_test.php index 374c508ef12..5ce1ffab17d 100644 --- a/course/tests/courselib_test.php +++ b/course/tests/courselib_test.php @@ -3707,4 +3707,52 @@ class core_course_courselib_testcase extends advanced_testcase { } $this->assertEquals(2, $count); } + + public function test_classify_course_for_timeline() { + global $DB, $CFG; + + require_once($CFG->dirroot.'/completion/criteria/completion_criteria_self.php'); + + set_config('enablecompletion', COMPLETION_ENABLED); + + $this->resetAfterTest(true); + $this->setAdminUser(); + + // Create courses for testing. + $generator = $this->getDataGenerator(); + $future = time() + 3600; + $past = time() - 3600; + $futurecourse = $generator->create_course(['startdate' => $future]); + $pastcourse = $generator->create_course(['startdate' => $past - 60, 'enddate' => $past]); + $completedcourse = $generator->create_course(['enablecompletion' => COMPLETION_ENABLED]); + $inprogresscourse = $generator->create_course(); + + // Set completion rules. + $criteriadata = new stdClass(); + $criteriadata->id = $completedcourse->id; + + // Self completion. + $criteriadata->criteria_self = COMPLETION_CRITERIA_TYPE_SELF; + $class = 'completion_criteria_self'; + $criterion = new $class(); + $criterion->update_config($criteriadata); + + $user = $this->getDataGenerator()->create_user(); + $studentrole = $DB->get_record('role', array('shortname' => 'student')); + $this->getDataGenerator()->enrol_user($user->id, $futurecourse->id, $studentrole->id); + $this->getDataGenerator()->enrol_user($user->id, $pastcourse->id, $studentrole->id); + $this->getDataGenerator()->enrol_user($user->id, $completedcourse->id, $studentrole->id); + $this->getDataGenerator()->enrol_user($user->id, $inprogresscourse->id, $studentrole->id); + + $this->setUser($user); + core_completion_external::mark_course_self_completed($completedcourse->id); + $ccompletion = new completion_completion(array('course' => $completedcourse->id, 'userid' => $user->id)); + $ccompletion->mark_complete(); + + // Aggregate the completions. + $this->assertEquals(COURSE_TIMELINE_PAST, course_classify_for_timeline($pastcourse)); + $this->assertEquals(COURSE_TIMELINE_FUTURE, course_classify_for_timeline($futurecourse)); + $this->assertEquals(COURSE_TIMELINE_PAST, course_classify_for_timeline($completedcourse)); + $this->assertEquals(COURSE_TIMELINE_INPROGRESS, course_classify_for_timeline($inprogresscourse)); + } } diff --git a/lang/en/cache.php b/lang/en/cache.php index e811f039859..545a2306b9e 100644 --- a/lang/en/cache.php +++ b/lang/en/cache.php @@ -41,8 +41,9 @@ $string['cachedef_capabilities'] = 'System capabilities list'; $string['cachedef_config'] = 'Config settings'; $string['cachedef_coursecat'] = 'Course categories lists for particular user'; $string['cachedef_coursecatrecords'] = 'Course categories records'; -$string['cachedef_coursecontacts'] = 'List of course contacts'; $string['cachedef_coursecattree'] = 'Course categories tree'; +$string['cachedef_coursecompletion'] = 'Course completion status'; +$string['cachedef_coursecontacts'] = 'List of course contacts'; $string['cachedef_coursemodinfo'] = 'Accumulated information about modules and sections for each course'; $string['cachedef_completion'] = 'Activity completion status'; $string['cachedef_databasemeta'] = 'Database meta information'; diff --git a/lib/completionlib.php b/lib/completionlib.php index 80c854242db..c32ad4c4bfe 100644 --- a/lib/completionlib.php +++ b/lib/completionlib.php @@ -769,6 +769,7 @@ class completion_info { // Difficult to find affected users, just purge all completion cache. cache::make('core', 'completion')->purge(); + cache::make('core', 'coursecompletion')->purge(); } /** @@ -820,6 +821,7 @@ class completion_info { // Difficult to find affected users, just purge all completion cache. cache::make('core', 'completion')->purge(); + cache::make('core', 'coursecompletion')->purge(); } /** diff --git a/lib/db/caches.php b/lib/db/caches.php index 42d98d42aab..f5fcff08447 100644 --- a/lib/db/caches.php +++ b/lib/db/caches.php @@ -229,6 +229,16 @@ $definitions = array( 'staticaccelerationsize' => 2, // Should be current course and site course. ), + // Used to cache course completion status. + 'coursecompletion' => array( + 'mode' => cache_store::MODE_APPLICATION, + 'simplekeys' => true, + 'simpledata' => true, + 'ttl' => 3600, + 'staticacceleration' => true, + 'staticaccelerationsize' => 30, // Will be users list of current courses in nav. + ), + // A simple cache that stores whether a user can expand a course in the navigation. // The key is the course ID and the value will either be 1 or 0 (cast to bool). // The cache isn't always up to date, it should only ever be used to save a costly call to diff --git a/lib/navigationlib.php b/lib/navigationlib.php index aa29bd931fa..8f536cd5373 100644 --- a/lib/navigationlib.php +++ b/lib/navigationlib.php @@ -2567,7 +2567,16 @@ class global_navigation extends navigation_node { } $coursenode = $parent->add($coursename, $url, self::TYPE_COURSE, $shortname, $course->id); - $coursenode->showinflatnavigation = $coursetype == self::COURSE_MY; + + // Do some calculation to see if the course is past, current or future. + if ($coursetype == self::COURSE_MY) { + $classify = course_classify_for_timeline($course); + + if ($classify == COURSE_TIMELINE_INPROGRESS) { + $coursenode->showinflatnavigation = true; + } + } + $coursenode->hidden = (!$course->visible); $coursenode->title(format_string($course->fullname, true, array('context' => $coursecontext, 'escape' => false))); if ($canexpandcourse) { @@ -2890,7 +2899,7 @@ class global_navigation extends navigation_node { } // Append the chosen sortorder. $sortorder = $sortorder . ',' . $CFG->navsortmycoursessort . ' ASC'; - $courses = enrol_get_my_courses(null, $sortorder); + $courses = enrol_get_my_courses('*', $sortorder); if (count($courses) && $this->show_my_categories()) { // Generate an array containing unique values of all the courses' categories. $categoryids = array(); @@ -3140,7 +3149,7 @@ class global_navigation_for_ajax extends global_navigation { // If category is shown in MyHome then only show enrolled courses and hide empty subcategories, // else show all courses. if ($nodetype === self::TYPE_MY_CATEGORY) { - $courses = enrol_get_my_courses(); + $courses = enrol_get_my_courses('*'); $categoryids = array(); // Only search for categories if basecategory was found. diff --git a/version.php b/version.php index 6247e8a6588..d9cc08bb9eb 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2017061201.00; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2017061300.00; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes.