MDL-59212 analytics: Core indicators tests

Part of MDL-57791 epic.
This commit is contained in:
David Monllao
2017-07-24 08:37:01 +02:00
parent 206d7aa9ad
commit 2db6e9811b
13 changed files with 327 additions and 30 deletions
+2 -2
View File
@@ -163,10 +163,10 @@ abstract class calculable {
$starttimedt = new \DateTime();
$starttimedt->setTimestamp($starttime);
$starttimedt->setTimezone(\DateTimeZone::UTC);
$starttimedt->setTimezone(new \DateTimeZone('UTC'));
$endtimedt = new \DateTime();
$endtimedt->setTimestamp($endtime);
$endtimedt->setTimezone(\DateTimeZone::UTC);
$endtimedt->setTimezone(new \DateTimeZone('UTC'));
$diff = $endtimedt->getTimestamp() - $starttimedt->getTimestamp();
return $diff / WEEKSECS;
+17 -3
View File
@@ -169,6 +169,15 @@ class course implements \core_analytics\analysable {
return self::$instances[$courseid];
}
/**
* Clears all statically cached instances.
*
* @return void
*/
public static function reset_caches() {
self::$instances = array();
}
/**
* get_id
*
@@ -224,7 +233,9 @@ class course implements \core_analytics\analysable {
return 0;
}
$logstore = \core_analytics\manager::get_analytics_logstore();
if (!$logstore = \core_analytics\manager::get_analytics_logstore()) {
return 0;
}
// We first try to find current course student logs.
$firstlogs = array();
@@ -431,8 +442,11 @@ class course implements \core_analytics\analysable {
if ($this->ntotallogs === null) {
list($filterselect, $filterparams) = $this->course_students_query_filter();
$logstore = \core_analytics\manager::get_analytics_logstore();
$this->ntotallogs = $logstore->get_events_select_count($filterselect, $filterparams);
if (!$logstore = \core_analytics\manager::get_analytics_logstore()) {
$this->ntotallogs = 0;
} else {
$this->ntotallogs = $logstore->get_events_select_count($filterselect, $filterparams);
}
}
return $this->ntotallogs;
@@ -428,7 +428,9 @@ abstract class community_of_inquiry_activity extends linear {
$params = $contextparams + array('starttime' => $starttime, 'endtime' => $endtime);
// Pity that we need to pass through logging readers API when most of the people just uses the standard one.
$logstore = \core_analytics\manager::get_analytics_logstore();
if (!$logstore = \core_analytics\manager::get_analytics_logstore()) {
throw new \coding_exception('No log store available');
}
$events = $logstore->get_events_select_iterator($select, $params, 'timecreated ASC', 0, 0);
// Returs the logs organised by contextid, userid and eventname so it is easier to calculate activities data later.
+7 -2
View File
@@ -305,7 +305,7 @@ class manager {
/**
* Returns the logstore used for analytics.
*
* @return \core\log\sql_reader
* @return \core\log\sql_reader|false False if no log stores are enabled.
*/
public static function get_analytics_logstore() {
$readers = get_log_manager()->get_readers('core\log\sql_reader');
@@ -314,12 +314,17 @@ class manager {
$logstore = reset($readers);
} else if (!empty($readers[$analyticsstore])) {
$logstore = $readers[$analyticsstore];
} else {
} else if (!empty($readers)) {
$logstore = reset($readers);
debugging('The selected log store for analytics is not available anymore. Using "' .
$logstore->get_name() . '"', DEBUG_DEVELOPER);
}
if (empty($logstore)) {
debugging('No system log stores available to use for analytics', DEBUG_DEVELOPER);
return false;
}
if (!$logstore->is_logging()) {
debugging('The selected log store for analytics "' . $logstore->get_name() .
'" is not logging activity logs', DEBUG_DEVELOPER);
+10 -2
View File
@@ -73,7 +73,11 @@ class site implements \core_analytics\analysable {
return $this->start;
}
$logstore = \core_analytics\manager::get_analytics_logstore();
if (!$logstore = \core_analytics\manager::get_analytics_logstore()) {
$this->start = 0;
return $this->start;
}
// Basically a SELECT MIN(timecreated) FROM ...
$events = $logstore->get_events_select("", array(), "timecreated ASC", 0, 1);
if ($events) {
@@ -97,7 +101,11 @@ class site implements \core_analytics\analysable {
return $this->end;
}
$logstore = \core_analytics\manager::get_analytics_logstore();
if (!$logstore = \core_analytics\manager::get_analytics_logstore()) {
$this->end = time();
return $this->end;
}
// Basically a SELECT MAX(timecreated) FROM ...
$events = $logstore->get_events_select("", array(), "timecreated DESC", 0, 1);
if ($events) {
@@ -68,14 +68,17 @@ class any_access_before_start extends \core_analytics\local\indicator\binary {
$user = $this->retrieve('user', $sampleid);
$course = \core_analytics\course::instance($this->retrieve('course', $sampleid));
// Filter by context to use the db table index.
if (!$logstore = \core_analytics\manager::get_analytics_logstore()) {
throw new \coding_exception('No available log stores');
}
// Filter by context to use the logstore_standard_log db table index.
$context = $this->retrieve('context', $sampleid);
$select = "userid = :userid AND contextlevel = :contextlevel AND contextinstanceid = :contextinstanceid AND " .
"timecreated < :start";
$params = array('userid' => $user->id, 'contextlevel' => $context->contextlevel,
'contextinstanceid' => $context->instanceid, 'start' => $course->get_start());
$logstore = \core_analytics\manager::get_analytics_logstore();
$nlogs = $logstore->get_events_select_count($select, $params);
if ($nlogs) {
return self::get_max_value();
@@ -74,13 +74,26 @@ class any_write_action extends \core_analytics\local\indicator\binary {
$params = $params + array('userid' => $user->id);
}
// Filter by context to use the db table index.
if (!$logstore = \core_analytics\manager::get_analytics_logstore()) {
throw new \coding_exception('No available log stores');
}
// Filter by context to use the logstore_standard_log db table index.
$context = $this->retrieve('context', $sampleid);
$select .= "contextlevel = :contextlevel AND contextinstanceid = :contextinstanceid AND " .
"(crud = 'c' OR crud = 'u') AND timecreated > :starttime AND timecreated <= :endtime";
"(crud = 'c' OR crud = 'u')";
$params = $params + array('contextlevel' => $context->contextlevel,
'contextinstanceid' => $context->instanceid, 'starttime' => $starttime, 'endtime' => $endtime);
$logstore = \core_analytics\manager::get_analytics_logstore();
'contextinstanceid' => $context->instanceid);
if ($starttime) {
$select .= " AND timecreated > :starttime";
$params['starttime'] = $starttime;
}
if ($endtime) {
$select .= " AND timecreated <= :endtime";
$params['endtime'] = $endtime;
}
$nlogs = $logstore->get_events_select_count($select, $params);
if ($nlogs) {
return self::get_max_value();
@@ -65,6 +65,10 @@ class read_actions extends \core_analytics\local\indicator\linear {
*/
protected function calculate_sample($sampleid, $sampleorigin, $starttime = false, $endtime = false) {
if (!$starttime || !$endtime) {
return null;
}
$select = '';
$params = array();
@@ -73,15 +77,17 @@ class read_actions extends \core_analytics\local\indicator\linear {
$params = $params + array('userid' => $user->id);
}
// Filter by context to use the db table index.
if (!$logstore = \core_analytics\manager::get_analytics_logstore()) {
throw new \coding_exception('No available log stores');
}
// Filter by context to use the logstore_standard_log db table index.
$context = $this->retrieve('context', $sampleid);
$select .= "contextlevel = :contextlevel AND contextinstanceid = :contextinstanceid AND " .
"crud = 'r' AND timecreated > :starttime AND timecreated <= :endtime";
$params = $params + array('contextlevel' => $context->contextlevel,
'contextinstanceid' => $context->instanceid, 'starttime' => $starttime, 'endtime' => $endtime);
$logstore = \core_analytics\manager::get_analytics_logstore();
$nrecords = $logstore->get_events_select_count($select, $params);
// We define a list of ranges to fit $nrecords into it
// # Done absolutely nothing
// # Not much really, just accessing the course once a week
@@ -160,7 +160,9 @@ class course_dropout extends \core_analytics\local\target\binary {
$select = 'courseid = :courseid AND anonymous = :anonymous AND timecreated > :start AND timecreated < :end ' .
'AND userid ' . $studentssql;
$logstore = \core_analytics\manager::get_analytics_logstore();
if (!$logstore = \core_analytics\manager::get_analytics_logstore()) {
throw new \coding_exception('No available log stores');
}
$nlogs = $logstore->get_events_select_count($select, array_merge($params, $studentparams));
// At least a minimum of students activity.
@@ -213,12 +215,15 @@ class course_dropout extends \core_analytics\local\target\binary {
}
}
if (!$logstore = \core_analytics\manager::get_analytics_logstore()) {
throw new \coding_exception('No available log stores');
}
// No logs during the last quarter of the course.
$courseduration = $course->get_end() - $course->get_start();
$limit = intval($course->get_end() - ($courseduration / 4));
$select = "courseid = :courseid AND userid = :userid AND timecreated > :limit";
$params = array('userid' => $userenrol->userid, 'courseid' => $course->get_id(), 'limit' => $limit);
$logstore = \core_analytics\manager::get_analytics_logstore();
$nlogs = $logstore->get_events_select_count($select, $params);
if ($nlogs == 0) {
return 0;
+1
View File
@@ -220,6 +220,7 @@ class phpunit_util extends testing_util {
if (class_exists('core_media_manager', false)) {
core_media_manager::reset_caches();
}
\core_analytics\course::reset_caches();
// Reset static unit test options.
if (class_exists('\availability_date\condition', false)) {
@@ -17,24 +17,26 @@
/**
* Unit tests for core analysers.
*
* @package core_analytics
* @package core
* @category test
* @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/fixtures/test_target_shortname.php');
require_once(__DIR__ . '/../../analytics/tests/fixtures/test_target_shortname.php');
require_once(__DIR__ . '/../../lib/enrollib.php');
/**
* Unit tests for core analysers.
*
* @package core_analytics
* @package core
* @category test
* @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class analytics_analysers_testcase extends advanced_testcase {
class core_analytics_analysers_testcase extends advanced_testcase {
/**
* test_courses_analyser
+236
View File
@@ -0,0 +1,236 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Unit tests for core indicators.
*
* @package core
* @category phpunit
* @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/../../analytics/tests/fixtures/test_target_shortname.php');
require_once(__DIR__ . '/../../admin/tool/log/store/standard/tests/fixtures/event.php');
require_once(__DIR__ . '/../../lib/enrollib.php');
/**
* Unit tests for core indicators.
*
* @package core
* @category phpunit
* @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class core_analytics_indicators_testcase extends advanced_testcase {
/**
* Test all core indicators.
*
* Single method as it is significantly faster (from 13s to 5s) than having separate
* methods because of preventResetByRollback.
*
* @return void
*/
public function test_core_indicators() {
$this->preventResetByRollback();
$this->resetAfterTest(true);
$this->setAdminuser();
set_config('enabled_stores', 'logstore_standard', 'tool_log');
set_config('buffersize', 0, 'logstore_standard');
$user1 = $this->getDataGenerator()->create_user();
$user2 = $this->getDataGenerator()->create_user();
// Test any access after end.
$params = array(
'startdate' => mktime(0, 0, 0, 10, 24, 2015),
'enddate' => mktime(0, 0, 0, 10, 24, 2016)
);
$course = $this->getDataGenerator()->create_course($params);
$coursecontext = \context_course::instance($course->id);
$this->getDataGenerator()->enrol_user($user1->id, $course->id);
$indicator = new \core\analytics\indicator\any_access_after_end();
$sampleids = array($user1->id => $user1->id, $user2->id => $user2->id);
$data = array($user1->id => array(
'context' => $coursecontext,
'course' => $course,
'user' => $user1
));
$data[$user2->id] = $data[$user1->id];
$data[$user2->id]['user'] = $user2;
$indicator->add_sample_data($data);
$values = $indicator->calculate($sampleids, 'notrelevanthere');
$this->assertEquals($indicator::get_min_value(), $values[$user1->id][0]);
$this->assertEquals($indicator::get_min_value(), $values[$user2->id][0]);
\logstore_standard\event\unittest_executed::create(
array('context' => $coursecontext, 'userid' => $user1->id))->trigger();
$values = $indicator->calculate($sampleids, 'notrelevanthere');
$this->assertEquals($indicator::get_max_value(), $values[$user1->id][0]);
$this->assertEquals($indicator::get_min_value(), $values[$user2->id][0]);
// Test any access before start.
$params = array(
'startdate' => 9999999998,
'enddate' => 9999999999
);
// Resetting $course var.
$course = $this->getDataGenerator()->create_course($params);
$coursecontext = \context_course::instance($course->id);
$this->getDataGenerator()->enrol_user($user1->id, $course->id);
$indicator = new \core\analytics\indicator\any_access_before_start();
$sampleids = array($user1->id => $user1->id, $user2->id => $user2->id);
$data = array($user1->id => array(
'context' => $coursecontext,
'course' => $course,
'user' => $user1
));
$data[$user2->id] = $data[$user1->id];
$data[$user2->id]['user'] = $user2;
$indicator->add_sample_data($data);
$values = $indicator->calculate($sampleids, 'notrelevanthere');
$this->assertEquals($indicator::get_min_value(), $values[$user1->id][0]);
$this->assertEquals($indicator::get_min_value(), $values[$user2->id][0]);
\logstore_standard\event\unittest_executed::create(
array('context' => $coursecontext, 'userid' => $user1->id))->trigger();
$values = $indicator->calculate($sampleids, 'notrelevanthere');
$this->assertEquals($indicator::get_max_value(), $values[$user1->id][0]);
$this->assertEquals($indicator::get_min_value(), $values[$user2->id][0]);
// Test any write action.
$course1 = $this->getDataGenerator()->create_course();
$coursecontext1 = \context_course::instance($course1->id);
$course2 = $this->getDataGenerator()->create_course();
$coursecontext2 = \context_course::instance($course2->id);
$this->getDataGenerator()->enrol_user($user1->id, $course2->id);
$indicator = new \core\analytics\indicator\any_write_action();
$sampleids = array($user1->id => $user1->id, $user2->id => $user2->id);
$data = array($user1->id => array(
'context' => $coursecontext1,
'course' => $course1,
'user' => $user1
));
$data[$user2->id] = $data[$user1->id];
$data[$user2->id]['user'] = $user2;
$indicator->add_sample_data($data);
$values = $indicator->calculate($sampleids, 'user');
$this->assertEquals($indicator::get_min_value(), $values[$user1->id][0]);
$this->assertEquals($indicator::get_min_value(), $values[$user2->id][0]);
$beforecourseeventcreate = time();
sleep(1);
\logstore_standard\event\unittest_executed::create(
array('context' => $coursecontext1, 'userid' => $user1->id))->trigger();
$values = $indicator->calculate($sampleids, 'user');
$this->assertEquals($indicator::get_max_value(), $values[$user1->id][0]);
$this->assertEquals($indicator::get_min_value(), $values[$user2->id][0]);
// Now try with course-level samples where user is not available.
$sampleids = array($course1->id => $course1->id, $course2->id => $course2->id);
$data = array(
$course1->id => array(
'context' => $coursecontext1,
'course' => $course1,
),
$course2->id => array(
'context' => $coursecontext2,
'course' => $course2,
)
);
$indicator->clear_sample_data();
$indicator->add_sample_data($data);
// Limited by time to avoid previous logs interfering as other logs
// have been generated by the system.
$values = $indicator->calculate($sampleids, 'course', $beforecourseeventcreate);
$this->assertEquals($indicator::get_max_value(), $values[$course1->id][0]);
$this->assertEquals($indicator::get_min_value(), $values[$course2->id][0]);
// Test read actions.
$course = $this->getDataGenerator()->create_course();
$coursecontext = \context_course::instance($course->id);
$this->getDataGenerator()->enrol_user($user1->id, $course->id);
$indicator = new \core\analytics\indicator\read_actions();
$sampleids = array($user1->id => $user1->id, $user2->id => $user2->id);
$data = array($user1->id => array(
'context' => $coursecontext,
'course' => $course,
'user' => $user1
));
$data[$user2->id] = $data[$user1->id];
$data[$user2->id]['user'] = $user2;
$indicator->add_sample_data($data);
// More or less 4 weeks duration.
$startdate = time() - (WEEKSECS * 2);
$enddate = time() + (WEEKSECS * 2);
$this->setAdminUser();
$values = $indicator->calculate($sampleids, 'user');
$this->assertNull($values[$user1->id][0]);
$this->assertNull($values[$user1->id][1]);
$this->assertNull($values[$user1->id][0]);
$this->assertNull($values[$user2->id][1]);
// Zero score for 0 accesses.
$values = $indicator->calculate($sampleids, 'user', $startdate, $enddate);
$this->assertEquals($indicator::get_min_value(), $values[$user1->id][0]);
$this->assertEquals($indicator::get_min_value(), $values[$user2->id][0]);
// 1/3 score for more than 0 accesses.
\core\event\course_viewed::create(
array('context' => $coursecontext, 'userid' => $user1->id))->trigger();
$values = $indicator->calculate($sampleids, 'user', $startdate, $enddate);
$this->assertEquals(-0.33, $values[$user1->id][0]);
$this->assertEquals($indicator::get_min_value(), $values[$user2->id][0]);
// 2/3 score for more than 1 access per week.
for ($i = 0; $i < 12; $i++) {
\core\event\course_viewed::create(
array('context' => $coursecontext, 'userid' => $user1->id))->trigger();
}
$values = $indicator->calculate($sampleids, 'user', $startdate, $enddate);
$this->assertEquals(0.33, $values[$user1->id][0]);
$this->assertEquals($indicator::get_min_value(), $values[$user2->id][0]);
// 100% score for tons of accesses during this period (3 logs per access * 4 weeks * 10 accesses).
for ($i = 0; $i < (3 * 10 * 4); $i++) {
\core\event\course_viewed::create(
array('context' => $coursecontext, 'userid' => $user1->id))->trigger();
}
$values = $indicator->calculate($sampleids, 'user', $startdate, $enddate);
$this->assertEquals($indicator::get_max_value(), $values[$user1->id][0]);
$this->assertEquals($indicator::get_min_value(), $values[$user2->id][0]);
}
}
@@ -17,24 +17,26 @@
/**
* Unit tests for core time splitting methods.
*
* @package core_analytics
* @package core
* @category phpunit
* @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/fixtures/test_target_shortname.php');
require_once(__DIR__ . '/../../analytics/tests/fixtures/test_target_shortname.php');
require_once(__DIR__ . '/../../lib/enrollib.php');
/**
* Unit tests for core time splitting methods.
*
* @package core_analytics
* @package core
* @category phpunit
* @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class analytics_time_splittings_testcase extends advanced_testcase {
class core_analytics_time_splittings_testcase extends advanced_testcase {
/**
* setUp
@@ -61,7 +63,7 @@ class analytics_time_splittings_testcase extends advanced_testcase {
*/
public function test_valid_ranges() {
// All core_analytics time splitting methods.
// All core time splitting methods.
$timesplittings = array(
'\core\analytics\time_splitting\deciles',
'\core\analytics\time_splitting\deciles_accum',