diff --git a/analytics/classes/calculable.php b/analytics/classes/calculable.php index ca6c68d9139..72813d7fa3b 100644 --- a/analytics/classes/calculable.php +++ b/analytics/classes/calculable.php @@ -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; diff --git a/analytics/classes/course.php b/analytics/classes/course.php index d99d3af01da..aae57482310 100644 --- a/analytics/classes/course.php +++ b/analytics/classes/course.php @@ -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; diff --git a/analytics/classes/local/indicator/community_of_inquiry_activity.php b/analytics/classes/local/indicator/community_of_inquiry_activity.php index 25e6c1b23f1..f7dbcfdffa2 100644 --- a/analytics/classes/local/indicator/community_of_inquiry_activity.php +++ b/analytics/classes/local/indicator/community_of_inquiry_activity.php @@ -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. diff --git a/analytics/classes/manager.php b/analytics/classes/manager.php index 8029db5bd75..afea72da9c2 100644 --- a/analytics/classes/manager.php +++ b/analytics/classes/manager.php @@ -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); diff --git a/analytics/classes/site.php b/analytics/classes/site.php index e84c00a1043..8e9a634d078 100644 --- a/analytics/classes/site.php +++ b/analytics/classes/site.php @@ -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) { diff --git a/lib/classes/analytics/indicator/any_access_before_start.php b/lib/classes/analytics/indicator/any_access_before_start.php index 8d00aa6e841..e5cada01a85 100644 --- a/lib/classes/analytics/indicator/any_access_before_start.php +++ b/lib/classes/analytics/indicator/any_access_before_start.php @@ -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(); diff --git a/lib/classes/analytics/indicator/any_write_action.php b/lib/classes/analytics/indicator/any_write_action.php index edfd2e1d458..21aab2cccd6 100644 --- a/lib/classes/analytics/indicator/any_write_action.php +++ b/lib/classes/analytics/indicator/any_write_action.php @@ -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(); diff --git a/lib/classes/analytics/indicator/read_actions.php b/lib/classes/analytics/indicator/read_actions.php index 03f731535e2..77641fa7e31 100644 --- a/lib/classes/analytics/indicator/read_actions.php +++ b/lib/classes/analytics/indicator/read_actions.php @@ -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 diff --git a/lib/classes/analytics/target/course_dropout.php b/lib/classes/analytics/target/course_dropout.php index aeb79e038a9..80c88908482 100644 --- a/lib/classes/analytics/target/course_dropout.php +++ b/lib/classes/analytics/target/course_dropout.php @@ -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; diff --git a/lib/phpunit/classes/util.php b/lib/phpunit/classes/util.php index 57c67c06d03..716ab3f629a 100644 --- a/lib/phpunit/classes/util.php +++ b/lib/phpunit/classes/util.php @@ -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)) { diff --git a/analytics/tests/analysers_test.php b/lib/tests/analysers_test.php similarity index 97% rename from analytics/tests/analysers_test.php rename to lib/tests/analysers_test.php index 7533eae2da2..4c28a0685e2 100644 --- a/analytics/tests/analysers_test.php +++ b/lib/tests/analysers_test.php @@ -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 diff --git a/lib/tests/indicators_test.php b/lib/tests/indicators_test.php new file mode 100644 index 00000000000..73f623de796 --- /dev/null +++ b/lib/tests/indicators_test.php @@ -0,0 +1,236 @@ +. + +/** + * 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]); + } +} diff --git a/analytics/tests/time_splittings_test.php b/lib/tests/time_splittings_test.php similarity index 95% rename from analytics/tests/time_splittings_test.php rename to lib/tests/time_splittings_test.php index 5e4c6476cd0..821024590cd 100644 --- a/analytics/tests/time_splittings_test.php +++ b/lib/tests/time_splittings_test.php @@ -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',