diff --git a/course/lib.php b/course/lib.php index 18bcfe9212c..7268824761a 100644 --- a/course/lib.php +++ b/course/lib.php @@ -2612,31 +2612,68 @@ function update_course($data, $editoroptions = NULL) { } /** - * Average number of participants - * @return integer + * Calculate the average number of enrolled participants per course. + * + * This is intended for statistics purposes during the site registration. Only visible courses are taken into account. + * Front page enrolments are excluded. + * + * @param bool $onlyactive Consider only active enrolments in enabled plugins and obey the enrolment time restrictions. + * @param int $lastloginsince If specified, count only users who logged in after this timestamp. + * @return float */ -function average_number_of_participants() { - global $DB, $SITE; +function average_number_of_participants(bool $onlyactive = false, int $lastloginsince = null): float { + global $DB; + + $params = [ + 'siteid' => SITEID, + ]; + + $sql = "SELECT DISTINCT ue.userid, e.courseid + FROM {user_enrolments} ue + JOIN {enrol} e ON e.id = ue.enrolid + JOIN {course} c ON c.id = e.courseid "; + + if ($onlyactive || $lastloginsince) { + $sql .= "JOIN {user} u ON u.id = ue.userid "; + } + + $sql .= "WHERE e.courseid <> :siteid + AND c.visible = 1 "; + + if ($onlyactive) { + $sql .= "AND ue.status = :active + AND e.status = :enabled + AND ue.timestart < :now1 + AND (ue.timeend = 0 OR ue.timeend > :now2) "; + + // Same as in the enrollib - the rounding should help caching in the database. + $now = round(time(), -2); + + $params += [ + 'active' => ENROL_USER_ACTIVE, + 'enabled' => ENROL_INSTANCE_ENABLED, + 'now1' => $now, + 'now2' => $now, + ]; + } + + if ($lastloginsince) { + $sql .= "AND u.lastlogin > :lastlogin "; + $params['lastlogin'] = $lastloginsince; + } + + $sql = "SELECT COUNT(*) + FROM ($sql) total"; - //count total of enrolments for visible course (except front page) - $sql = 'SELECT COUNT(*) FROM ( - SELECT DISTINCT ue.userid, e.courseid - FROM {user_enrolments} ue, {enrol} e, {course} c - WHERE ue.enrolid = e.id - AND e.courseid <> :siteid - AND c.id = e.courseid - AND c.visible = 1) total'; - $params = array('siteid' => $SITE->id); $enrolmenttotal = $DB->count_records_sql($sql, $params); + // Get the number of visible courses (exclude the front page). + $coursetotal = $DB->count_records('course', ['visible' => 1]); + $coursetotal = $coursetotal - 1; - //count total of visible courses (minus front page) - $coursetotal = $DB->count_records('course', array('visible' => 1)); - $coursetotal = $coursetotal - 1 ; - - //average of enrolment if (empty($coursetotal)) { $participantaverage = 0; + } else { $participantaverage = $enrolmenttotal / $coursetotal; } diff --git a/course/tests/courselib_test.php b/course/tests/courselib_test.php index 2fcc9bee1b8..fef3933a62f 100644 --- a/course/tests/courselib_test.php +++ b/course/tests/courselib_test.php @@ -6945,4 +6945,78 @@ class core_course_courselib_testcase extends advanced_testcase { // Manager has permissions. $this->assertTrue(course_allowed_module($course, 'assign', $manager)); } + + /** + * Test the {@link average_number_of_participants()} function. + */ + public function test_average_number_of_participants() { + global $DB; + $this->resetAfterTest(true); + + $generator = $this->getDataGenerator(); + $now = time(); + + // If there are no courses, expect zero number of participants per course. + $this->assertEquals(0, average_number_of_participants()); + + $c1 = $generator->create_course(); + $c2 = $generator->create_course(); + + // If there are no users, expect zero number of participants per course. + $this->assertEquals(0, average_number_of_participants()); + + $t1 = $generator->create_user(['lastlogin' => $now]); + $s1 = $generator->create_user(['lastlogin' => $now]); + $s2 = $generator->create_user(['lastlogin' => $now - WEEKSECS]); + $s3 = $generator->create_user(['lastlogin' => $now - WEEKSECS]); + $s4 = $generator->create_user(['lastlogin' => $now - YEARSECS]); + + // We have courses, we have users, but no enrolments yet. + $this->assertEquals(0, average_number_of_participants()); + + // Front page enrolments are ignored. + $generator->enrol_user($t1->id, SITEID, 'teacher'); + $this->assertEquals(0, average_number_of_participants()); + + // The teacher enrolled into one of the two courses. + $generator->enrol_user($t1->id, $c1->id, 'editingteacher'); + $this->assertEquals(0.5, average_number_of_participants()); + + // The teacher enrolled into both courses. + $generator->enrol_user($t1->id, $c2->id, 'editingteacher'); + $this->assertEquals(1, average_number_of_participants()); + + // Student 1 enrolled in the Course 1 only. + $generator->enrol_user($s1->id, $c1->id, 'student'); + $this->assertEquals(1.5, average_number_of_participants()); + + // Student 2 enrolled in both courses, but the enrolment in the Course 2 not active yet (enrolment starts in the future). + $generator->enrol_user($s2->id, $c1->id, 'student'); + $generator->enrol_user($s2->id, $c2->id, 'student', 'manual', $now + WEEKSECS); + $this->assertEquals(2.5, average_number_of_participants()); + $this->assertEquals(2, average_number_of_participants(true)); + + // Student 3 enrolled in the Course 1, but the enrolment already expired. + $generator->enrol_user($s3->id, $c1->id, 'student', 'manual', 0, $now - YEARSECS); + $this->assertEquals(3, average_number_of_participants()); + $this->assertEquals(2, average_number_of_participants(true)); + + // Student 4 enrolled in both courses, but the enrolment has been suspended. + $generator->enrol_user($s4->id, $c1->id, 'student', 'manual', 0, 0, ENROL_USER_SUSPENDED); + $generator->enrol_user($s4->id, $c2->id, 'student', 'manual', $now - DAYSECS, $now + YEARSECS, ENROL_USER_SUSPENDED); + $this->assertEquals(4, average_number_of_participants()); + $this->assertEquals(2, average_number_of_participants(true)); + + // Consider only t1 and s1 who logged in recently. + $this->assertEquals(1.5, average_number_of_participants(false, $now - DAYSECS)); + + // Consider only t1, s1, s2 and s3 who logged in in recent weeks. + $this->assertEquals(3, average_number_of_participants(false, $now - 4 * WEEKSECS)); + + // Hidden courses are excluded from stats. + $DB->set_field('course', 'visible', 0, ['id' => $c1->id]); + $this->assertEquals(3, average_number_of_participants()); + $this->assertEquals(1, average_number_of_participants(true)); + } + } diff --git a/lang/en/hub.php b/lang/en/hub.php index 3df03bab86e..f931fd98038 100644 --- a/lang/en/hub.php +++ b/lang/en/hub.php @@ -22,6 +22,8 @@ * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +$string['activeparticipantnumberaverage'] = 'Average number of recently active participants ({$a})'; +$string['activeusersnumber'] = 'Number of recently active users ({$a})'; $string['analyticsactions'] = 'Number of actions taken on generated predictions ({$a})'; $string['analyticsactionsnotuseful'] = 'Number of actions marking a prediction as not useful ({$a})'; $string['analyticsenabledmodels'] = 'Number of enabled prediction models ({$a})'; diff --git a/lib/classes/hub/registration.php b/lib/classes/hub/registration.php index cd716ef0084..1fb94e3a54a 100644 --- a/lib/classes/hub/registration.php +++ b/lib/classes/hub/registration.php @@ -58,6 +58,8 @@ class registration { ], // Analytics stats added in Moodle 3.7. 2019022200 => ['analyticsenabledmodels', 'analyticspredictions', 'analyticsactions', 'analyticsactionsnotuseful'], + // Active users stats added in Moodle 3.9. + 2020022600 => ['activeusers', 'activeparticipantnumberaverage'], ]; /** @var Site privacy: not displayed */ @@ -168,6 +170,7 @@ class registration { // Statistical data. $siteinfo['courses'] = $DB->count_records('course') - 1; $siteinfo['users'] = $DB->count_records('user', array('deleted' => 0)); + $siteinfo['activeusers'] = $DB->count_records_select('user', 'deleted = ? AND lastlogin > ?', [0, time() - DAYSECS * 30]); $siteinfo['enrolments'] = $DB->count_records('role_assignments'); $siteinfo['posts'] = $DB->count_records('forum_posts'); $siteinfo['questions'] = $DB->count_records('question'); @@ -175,6 +178,7 @@ class registration { $siteinfo['badges'] = $DB->count_records_select('badge', 'status <> ' . BADGE_STATUS_ARCHIVED); $siteinfo['issuedbadges'] = $DB->count_records('badge_issued'); $siteinfo['participantnumberaverage'] = average_number_of_participants(); + $siteinfo['activeparticipantnumberaverage'] = average_number_of_participants(true, time() - DAYSECS * 30); $siteinfo['modulenumberaverage'] = average_number_of_courses_modules(); // Version and url. @@ -229,6 +233,7 @@ class registration { 'moodlerelease' => get_string('sitereleasenum', 'hub', $moodlerelease), 'courses' => get_string('coursesnumber', 'hub', $siteinfo['courses']), 'users' => get_string('usersnumber', 'hub', $siteinfo['users']), + 'activeusers' => get_string('activeusersnumber', 'hub', $siteinfo['activeusers']), 'enrolments' => get_string('roleassignmentsnumber', 'hub', $siteinfo['enrolments']), 'posts' => get_string('postsnumber', 'hub', $siteinfo['posts']), 'questions' => get_string('questionsnumber', 'hub', $siteinfo['questions']), @@ -237,6 +242,8 @@ class registration { 'issuedbadges' => get_string('issuedbadgesnumber', 'hub', $siteinfo['issuedbadges']), 'participantnumberaverage' => get_string('participantnumberaverage', 'hub', format_float($siteinfo['participantnumberaverage'], 2)), + 'activeparticipantnumberaverage' => get_string('activeparticipantnumberaverage', 'hub', + format_float($siteinfo['activeparticipantnumberaverage'], 2)), 'modulenumberaverage' => get_string('modulenumberaverage', 'hub', format_float($siteinfo['modulenumberaverage'], 2)), 'mobileservicesenabled' => get_string('mobileservicesenabled', 'hub', $mobileservicesenabled),