first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
+88
View File
@@ -0,0 +1,88 @@
<?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/>.
namespace core_analytics;
/**
* Unit tests for the analysis class.
*
* @package core_analytics
* @copyright 2019 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class analysis_test extends \advanced_testcase {
/**
* Test fill_firstanalyses_cache.
* @return null
*/
public function test_fill_firstanalyses_cache(): void {
require_once(__DIR__ . '/fixtures/test_timesplitting_upcoming_seconds.php');
$this->resetAfterTest();
$modelid = 1;
$params = ['startdate' => (new \DateTimeImmutable('-5 seconds'))->getTimestamp()];
$course1 = $this->getDataGenerator()->create_course($params);
$course2 = $this->getDataGenerator()->create_course($params);
$analysable1 = new \core_analytics\course($course1);
$afewsecsago = time() - 5;
$earliest = $afewsecsago - 1;
$this->insert_used($modelid, $course1->id, 'training', $afewsecsago);
// Course2 processed after course1.
$this->insert_used($modelid, $course2->id, 'training', $afewsecsago + 1);
// After the first process involving course1.
$this->insert_used($modelid, $course1->id, 'prediction', $afewsecsago + 5);
$firstanalyses = \core_analytics\analysis::fill_firstanalyses_cache($modelid);
$this->assertCount(2, $firstanalyses);
$this->assertEquals($afewsecsago, $firstanalyses[$modelid . '_' . $course1->id]);
$this->assertEquals($afewsecsago + 1, $firstanalyses[$modelid . '_' . $course2->id]);
// The cached elements get refreshed.
$this->insert_used($modelid, $course1->id, 'prediction', $earliest);
$firstanalyses = \core_analytics\analysis::fill_firstanalyses_cache($modelid, $course1->id);
$this->assertCount(1, $firstanalyses);
$this->assertEquals($earliest, $firstanalyses[$modelid . '_' . $course1->id]);
// Upcoming periodic time-splitting methods can read and process the cached data.
$seconds = new \test_timesplitting_upcoming_seconds();
$seconds->set_modelid($modelid);
$seconds->set_analysable($analysable1);
// The generated ranges should start from the cached firstanalysis value, which is $earliest.
$ranges = $seconds->get_all_ranges();
$this->assertGreaterThanOrEqual(7, count($ranges));
$firstrange = reset($ranges);
$this->assertEquals($earliest, $firstrange['time']);
}
private function insert_used($modelid, $analysableid, $action, $timestamp) {
global $DB;
$obj = new \stdClass();
$obj->modelid = $modelid;
$obj->action = $action;
$obj->analysableid = $analysableid;
$obj->firstanalysis = $timestamp;
$obj->timeanalysed = $timestamp;
$obj->id = $DB->insert_record('analytics_used_analysables', $obj);
}
}
+163
View File
@@ -0,0 +1,163 @@
@core @core_analytics @javascript
Feature: Manage analytics models
In order to manage analytics models
As a manager
I need to create and use a model
Background:
# Turn off the course welcome message, so we can easily test other messages.
Given the following config values are set as admin:
| onlycli | 0 | analytics |
| sendcoursewelcomemessage | 0 | enrol_manual |
And the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
| manager1 | Manager | 1 | manager1@example.com |
| student0 | Student | 0 | student0@example.com |
| student1 | Student | 1 | student1@example.com |
| student2 | Student | 2 | student2@example.com |
| student3 | Student | 3 | student3@example.com |
| student4 | Student | 4 | student4@example.com |
| student5 | Student | 5 | student5@example.com |
| student6 | Student | 6 | student6@example.com |
And the following "system role assigns" exist:
| user | course | role |
| manager1 | Acceptance test site | manager |
And the following "courses" exist:
| fullname | shortname | category | enddate | startdate | enablecompletion |
| Course 1 | C1 | 0 | ## yesterday ## | ## 2 days ago ## | 1 |
| Course 2 | C2 | 0 | ## yesterday ## | ## 2 days ago ## | 1 |
| Course 3 | C3 | 0 | ## tomorrow ## | ## 2 days ago ## | 1 |
And the following "course enrolments" exist:
| user | course | role | timeend | timestart |
| teacher1 | C1 | editingteacher | ## 1 day ago ## | ## 2 days ago ## |
| student0 | C1 | student | ## 1 day ago ## | ## 2 days ago ## |
| student1 | C1 | student | ## 1 day ago ## | ## 2 days ago ## |
| student2 | C1 | student | ## 1 day ago ## | ## 2 days ago ## |
| teacher1 | C2 | editingteacher | ## 1 day ago ## | ## 2 days ago ## |
| student3 | C2 | student | ## 1 day ago ## | ## 2 days ago ## |
| student4 | C2 | student | ## 1 day ago ## | ## 2 days ago ## |
| teacher1 | C3 | editingteacher | 0 | ## 2 days ago ## |
| manager1 | C3 | manager | 0 | ## 2 days ago ## |
| student5 | C3 | student | 0 | ## 2 days ago ## |
| student6 | C3 | student | 0 | ## 2 days ago ## |
And the following "activities" exist:
| activity | name | intro | course | idnumber | section | completion | completionview |
| assign | assign1 | A1 desc | C1 | assign1 | 0 | 2 | 1 |
| assign | assign2 | A2 desc | C2 | assign2 | 0 | 2 | 1 |
| assign | assign3 | A3 desc | C3 | assign3 | 0 | 2 | 1 |
And the following "analytics model" exist:
| target | indicators | timesplitting | enabled |
| \core_course\analytics\target\course_completion | \core\analytics\indicator\any_write_action,\core\analytics\indicator\read_actions | \core\analytics\time_splitting\single_range | true |
And I log in as "manager1"
And I navigate to "Analytics > Analytics models" in site administration
Scenario: Create a model
When I open the action menu in ".top-nav" "css_element"
And I choose "Create model" in the open action menu
And I set the field "Enabled" to "Enable"
And I select "__core_course__analytics__target__course_completion" from the "target" singleselect
And I set the field "Indicators" to "Read actions amount, Any write action in the course"
And I select "__core__analytics__time_splitting__single_range" from the "timesplitting" singleselect
And I press "Save changes"
Then I should see "No predictions available yet" in the "Students at risk of not meeting the course completion conditions" "table_row"
Scenario: Evaluate a model
Given I am on "Course 1" course homepage
And I navigate to "Course completion" in current page administration
And I expand all fieldsets
And I set the following fields to these values:
| Assignment - assign1 | 1 |
And I click on "Save changes" "button"
And I am on "Course 2" course homepage
And I navigate to "Course completion" in current page administration
And I expand all fieldsets
And I set the following fields to these values:
| Assignment - assign2 | 1 |
And I click on "Save changes" "button"
And I am on "Course 3" course homepage
And I navigate to "Course completion" in current page administration
And I expand all fieldsets
And I set the following fields to these values:
| Assignment - assign3 | 1 |
And I click on "Save changes" "button"
And I am on site homepage
And I navigate to "Analytics > Analytics models" in site administration
And I open the action menu in "Students at risk of not meeting the course completion conditions" "table_row"
And I choose "Evaluate" in the open action menu
And I press "Evaluate"
And I should see "Evaluate model"
And I press "Continue"
# Evaluation log
And I open the action menu in "Students at risk of not meeting the course completion conditions" "table_row"
And I choose "Evaluation log" in the open action menu
And I should see "Configuration"
And I click on "View" "link"
And I should see "Log extra info"
And I click on "Close" "button" in the "Log extra info" "dialogue"
And I navigate to "Analytics > Analytics models" in site administration
# Execute scheduled analysis
And I open the action menu in "Students at risk of not meeting the course completion conditions" "table_row"
And I choose "Execute scheduled analysis" in the open action menu
And I should see "Training results"
And I press "Continue"
# Check notifications
Then I should see "1" in the "#nav-notification-popover-container [data-region='count-container']" "css_element"
And I open the notification popover
And I click on "View full notification" "link" in the ".popover-region-notifications" "css_element"
And I should see "Students at risk in Course 3 course"
When I am on site homepage
And I navigate to "Analytics > Analytics models" in site administration
# View predictions
When I select "C3" from the "contextid" singleselect
And I click on "View prediction details" "icon" in the "Student 6" "table_row"
And I should see "Prediction details"
And I should see "Any write action"
And I should see "Read actions amount"
And I click on "Select Student 6 for bulk action" "checkbox" in the "Student 6" "table_row"
And I click on "Accept" "button"
And I wait until "Confirm" "button" exists
And I click on "Confirm" "button" in the "Accept" "dialogue"
And I click on "View prediction details" "icon" in the "Student 5" "table_row"
And I click on "Select Student 5 for bulk action" "checkbox" in the "Student 5" "table_row"
And I click on "Not applicable" "button"
And I click on "Confirm" "button" in the "Not applicable" "dialogue"
And I should see "No insights reported"
# Clear predictions
When I am on site homepage
And I navigate to "Analytics > Analytics models" in site administration
And I should see "No insights reported" in the "Students at risk of not meeting the course completion conditions" "table_row"
And I open the action menu in "Students at risk of not meeting the course completion conditions" "table_row"
And I choose "Clear predictions" in the open action menu
And I press "Clear predictions"
Then I should see "No predictions available yet" in the "Students at risk of not meeting the course completion conditions" "table_row"
Scenario: Edit a model
When I open the action menu in "Students at risk of not meeting the course completion conditions" "table_row"
And I choose "Edit" in the open action menu
And I click on "Read actions amount" "text" in the ".form-autocomplete-selection" "css_element"
And I press "Save changes"
And I should not see "Read actions amount"
Scenario: Disable a model
When I open the action menu in "Students at risk of not meeting the course completion conditions" "table_row"
And I choose "Disable" in the open action menu
Then I should see "Disabled model" in the "Students at risk of not meeting the course completion conditions" "table_row"
Scenario: Export model
When I open the action menu in "Students at risk of not meeting the course completion conditions" "table_row"
And I choose "Export" in the open action menu
And I click on "Actions" "link" in the "Students at risk of not meeting the course completion conditions" "table_row"
And following "Export" should download a file that:
| Contains file in zip | model-config.json |
Scenario: Check invalid site elements
When I open the action menu in "Students at risk of not meeting the course completion conditions" "table_row"
And I choose "Invalid site elements" in the open action menu
Then I should see "Invalid analysable elements"
Scenario: Delete model
When I open the action menu in "Students at risk of not meeting the course completion conditions" "table_row"
And I choose "Delete" in the open action menu
And I click on "Delete" "button" in the "Delete" "dialogue"
Then I should not see "Students at risk of not meeting the course completion conditions"
+96
View File
@@ -0,0 +1,96 @@
<?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/>.
namespace core_analytics;
/**
* Unit tests for the calculation info cache.
*
* @package core_analytics
* @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class calculation_info_test extends \advanced_testcase {
/**
* test_calculation_info description
*
* @dataProvider provider_test_calculation_info_add_pull
* @param mixed $info1
* @param mixed $info2
* @param mixed $info3
* @param mixed $info4
* @return null
*/
public function test_calculation_info_add_pull($info1, $info2, $info3, $info4): void {
require_once(__DIR__ . '/fixtures/test_indicator_max.php');
require_once(__DIR__ . '/fixtures/test_indicator_min.php');
$this->resetAfterTest();
$atimesplitting = new \core\analytics\time_splitting\quarters();
$indicator1 = new \test_indicator_min();
$indicator2 = new \test_indicator_max();
$calculationinfo = new \core_analytics\calculation_info();
$calculationinfo->add_shared(111, [111 => $info1]);
$calculationinfo->add_shared(222, [222 => 'should-get-overwritten-in-next-line']);
$calculationinfo->add_shared(222, [222 => $info2]);
$calculationinfo->save($indicator1, $atimesplitting, 0);
// We also check that the eheheh does not overwrite the value previously stored in the cache
// during the previous save call.
$calculationinfo->add_shared(222, [222 => 'eheheh']);
$calculationinfo->save($indicator1, $atimesplitting, 0);
// The method save() should clear the internal attrs in \core_analytics\calculation_info
// so it is fine to reuse the same calculation_info instance.
$calculationinfo->add_shared(111, [111 => $info3]);
$calculationinfo->add_shared(333, [333 => $info4]);
$calculationinfo->save($indicator2, $atimesplitting, 0);
// We pull data in rangeindex '0' for samples 111, 222 and 333.
$predictionrecords = [
'111-0' => (object)['sampleid' => '111'],
'222-0' => (object)['sampleid' => '222'],
'333-0' => (object)['sampleid' => '333'],
];
$info = \core_analytics\calculation_info::pull_info($predictionrecords);
$this->assertCount(3, $info);
$this->assertCount(2, $info[111]);
$this->assertCount(1, $info[222]);
$this->assertCount(1, $info[333]);
$this->assertEquals($info1, $info[111]['test_indicator_min:extradata'][111]);
$this->assertEquals($info2, $info[222]['test_indicator_min:extradata'][222]);
$this->assertEquals($info3, $info[111]['test_indicator_max:extradata'][111]);
$this->assertEquals($info4, $info[333]['test_indicator_max:extradata'][333]);
// The calculationinfo cache gets emptied.
$this->assertFalse(\core_analytics\calculation_info::pull_info($predictionrecords));
}
/**
* provider_test_calculation_info_add_pull
*
* @return mixed[]
*/
public function provider_test_calculation_info_add_pull() {
return [
'mixed-types' => ['asd', true, [123, 123, 123], (object)['asd' => 'fgfg']],
];
}
}
@@ -0,0 +1,346 @@
<?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/>.
namespace core_analytics;
use advanced_testcase;
use ReflectionClass;
use ReflectionMethod;
use stdClass;
/**
* Unit tests for activities completed by classification.
*
* @package core_analytics
* @covers \core_analytics\local\indicator\community_of_inquiry_activity
* @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
final class community_of_inquiry_activities_completed_by_test extends advanced_testcase {
/**
* availability_levels
*
* @return array
*/
public static function availability_levels(): array {
return array(
'activity' => array('activity'),
'section' => array('section'),
);
}
/**
* test_get_activities_with_availability
*
* @dataProvider availability_levels
* @param string $availabilitylevel
* @return void
*/
public function test_get_activities_with_availability($availabilitylevel) {
list($course, $stu1) = $this->setup_course();
// Forum1 is ignored as section 0 does not count.
$forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
$modinfo = get_fast_modinfo($course, $stu1->id);
$cm = $modinfo->get_cm($forum->cmid);
if ($availabilitylevel === 'activity') {
$availabilityinfo = new \core_availability\info_module($cm);
} else if ($availabilitylevel === 'section') {
$availabilityinfo = new \core_availability\info_section($cm->get_modinfo()->get_section_info($cm->sectionnum));
} else {
$this->fail('Unsupported availability level');
}
$fromtime = strtotime('2015-10-22 00:00:00 GMT');
$untiltime = strtotime('2015-10-24 00:00:00 GMT');
$structure = (object)array('op' => '|', 'show' => true, 'c' => array(
(object)array('type' => 'date', 'd' => '<', 't' => $untiltime),
(object)array('type' => 'date', 'd' => '>=', 't' => $fromtime)
));
$method = new ReflectionMethod($availabilityinfo, 'set_in_database');
$method->invoke($availabilityinfo, json_encode($structure));
$this->setUser($stu1);
// Reset modinfo we also want coursemodinfo cache definition to be cleared.
get_fast_modinfo($course, $stu1->id, true);
rebuild_course_cache($course->id, true);
$modinfo = get_fast_modinfo($course, $stu1->id);
$cm = $modinfo->get_cm($forum->cmid);
$course = new \core_analytics\course($course);
list($indicator, $method) = $this->instantiate_indicator('mod_forum', $course);
// Condition from after provided end time.
$this->assertCount(0, $method->invoke($indicator, strtotime('2015-10-20 00:00:00 GMT'),
strtotime('2015-10-21 00:00:00 GMT'), $stu1));
// Condition until before provided start time
$this->assertCount(0, $method->invoke($indicator, strtotime('2015-10-25 00:00:00 GMT'),
strtotime('2015-10-26 00:00:00 GMT'), $stu1));
// Condition until after provided end time.
$this->assertCount(0, $method->invoke($indicator, strtotime('2015-10-22 00:00:00 GMT'),
strtotime('2015-10-23 00:00:00 GMT'), $stu1));
// Condition until after provided start time and before provided end time.
$this->assertCount(1, $method->invoke($indicator, strtotime('2015-10-22 00:00:00 GMT'),
strtotime('2015-10-25 00:00:00 GMT'), $stu1));
}
/**
* test_get_activities_with_weeks
*
* @return void
*/
public function test_get_activities_with_weeks() {
$startdate = gmmktime('0', '0', '0', 10, 24, 2015);
$record = array(
'format' => 'weeks',
'numsections' => 4,
'startdate' => $startdate,
);
list($course, $stu1) = $this->setup_course($record);
// Forum1 is ignored as section 0 does not count.
$forum1 = $this->getDataGenerator()->create_module('forum', array('course' => $course->id),
array('section' => 0));
$forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $course->id),
array('section' => 1));
$forum3 = $this->getDataGenerator()->create_module('forum', array('course' => $course->id),
array('section' => 2));
$forum4 = $this->getDataGenerator()->create_module('forum', array('course' => $course->id),
array('section' => 4));
$forum5 = $this->getDataGenerator()->create_module('forum', array('course' => $course->id),
array('section' => 4));
$course = new \core_analytics\course($course);
list($indicator, $method) = $this->instantiate_indicator('mod_forum', $course);
$this->setUser($stu1);
$first = $startdate;
$second = $startdate + WEEKSECS;
$third = $startdate + (WEEKSECS * 2);
$forth = $startdate + (WEEKSECS * 3);
$this->assertCount(2, $method->invoke($indicator, $first, $first + WEEKSECS, $stu1));
$this->assertCount(1, $method->invoke($indicator, $second, $second + WEEKSECS, $stu1));
$this->assertCount(0, $method->invoke($indicator, $third, $third + WEEKSECS, $stu1));
$this->assertCount(2, $method->invoke($indicator, $forth, $forth + WEEKSECS, $stu1));
}
/**
* test_get_activities_by_section
*
* @return void
*/
public function test_get_activities_by_section() {
// This makes debugging easier, sorry WA's +8 :).
$this->setTimezone('UTC');
// 1 year.
$startdate = gmmktime('0', '0', '0', 10, 24, 2015);
$enddate = gmmktime('0', '0', '0', 10, 24, 2016);
$numsections = 12;
$record = array(
'format' => 'topics',
'numsections' => $numsections,
'startdate' => $startdate,
'enddate' => $enddate
);
list($course, $stu1) = $this->setup_course($record);
// Forum1 is ignored as section 0 does not count.
$forum1 = $this->getDataGenerator()->create_module('forum', array('course' => $course->id),
array('section' => 0));
$forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $course->id),
array('section' => 1));
$forum3 = $this->getDataGenerator()->create_module('forum', array('course' => $course->id),
array('section' => 4));
$forum4 = $this->getDataGenerator()->create_module('forum', array('course' => $course->id),
array('section' => 8));
$forum5 = $this->getDataGenerator()->create_module('forum', array('course' => $course->id),
array('section' => 10));
$forum6 = $this->getDataGenerator()->create_module('forum', array('course' => $course->id),
array('section' => 12));
$course = new \core_analytics\course($course);
list($indicator, $method) = $this->instantiate_indicator('mod_forum', $course);
$this->setUser($stu1);
// Split the course in quarters.
$duration = ($enddate - $startdate) / 4;
$first = $startdate;
$second = $startdate + $duration;
$third = $startdate + ($duration * 2);
$forth = $startdate + ($duration * 3);
$this->assertCount(1, $method->invoke($indicator, $first, $first + $duration, $stu1));
$this->assertCount(1, $method->invoke($indicator, $second, $second + $duration, $stu1));
$this->assertCount(1, $method->invoke($indicator, $third, $third + $duration, $stu1));
$this->assertCount(2, $method->invoke($indicator, $forth, $forth + $duration, $stu1));
// Split the course in as many parts as sections.
$duration = ($enddate - $startdate) / $numsections;
for ($i = 1; $i <= $numsections; $i++) {
// The -1 because section 1 start represents the course start.
$timeranges[$i] = $startdate + ($duration * ($i - 1));
}
$this->assertCount(1, $method->invoke($indicator, $timeranges[1], $timeranges[1] + $duration, $stu1));
$this->assertCount(1, $method->invoke($indicator, $timeranges[4], $timeranges[4] + $duration, $stu1));
$this->assertCount(1, $method->invoke($indicator, $timeranges[8], $timeranges[8] + $duration, $stu1));
$this->assertCount(1, $method->invoke($indicator, $timeranges[10], $timeranges[10] + $duration, $stu1));
$this->assertCount(1, $method->invoke($indicator, $timeranges[12], $timeranges[12] + $duration, $stu1));
// Nothing here.
$this->assertCount(0, $method->invoke($indicator, $timeranges[2], $timeranges[2] + $duration, $stu1));
$this->assertCount(0, $method->invoke($indicator, $timeranges[3], $timeranges[3] + $duration, $stu1));
}
/**
* test_get_activities_with_specific_restrictions
*
* @return void
*/
public function test_get_activities_with_specific_restrictions() {
list($course, $stu1) = $this->setup_course();
$end = strtotime('2015-10-24 00:00:00 GMT');
// 1 with time close, one without.
$params = array('course' => $course->id);
$assign1 = $this->getDataGenerator()->create_module('assign', $params);
$params['duedate'] = $end;
$assign2 = $this->getDataGenerator()->create_module('assign', $params);
// 1 with time close, one without.
$params = array('course' => $course->id);
$choice1 = $this->getDataGenerator()->create_module('choice', $params);
$params['timeclose'] = $end;
$choice1 = $this->getDataGenerator()->create_module('choice', $params);
// 1 with time close, one without.
$params = array('course' => $course->id);
$data1 = $this->getDataGenerator()->create_module('data', $params);
$params['timeavailableto'] = $end;
$data1 = $this->getDataGenerator()->create_module('data', $params);
// 1 with time close, one without.
$params = array('course' => $course->id);
$feedback1 = $this->getDataGenerator()->create_module('feedback', $params);
$params['timeclose'] = $end;
$feedback1 = $this->getDataGenerator()->create_module('feedback', $params);
// 1 with time close, one without.
$params = array('course' => $course->id);
$lesson1 = $this->getDataGenerator()->create_module('lesson', $params);
$params['deadline'] = $end;
$lesson1 = $this->getDataGenerator()->create_module('lesson', $params);
// 1 with time close, one without.
$params = array('course' => $course->id);
$quiz1 = $this->getDataGenerator()->create_module('quiz', $params);
$params['timeclose'] = $end;
$quiz1 = $this->getDataGenerator()->create_module('quiz', $params);
// 1 with time close, one without.
$params = array('course' => $course->id);
$scorm1 = $this->getDataGenerator()->create_module('scorm', $params);
$params['timeclose'] = $end;
$scorm1 = $this->getDataGenerator()->create_module('scorm', $params);
// 1 with time close, one without.
$params = array('course' => $course->id);
$workshop1 = $this->getDataGenerator()->create_module('workshop', $params);
$params['submissionend'] = $end;
$workshop1 = $this->getDataGenerator()->create_module('workshop', $params);
$course = new \core_analytics\course($course);
$activitytypes = array('mod_assign', 'mod_choice', 'mod_data', 'mod_feedback', 'mod_lesson',
'mod_quiz', 'mod_scorm', 'mod_workshop');
foreach ($activitytypes as $activitytype) {
list($indicator, $method) = $this->instantiate_indicator($activitytype, $course);
$message = $activitytype . ' activity type returned activities do not match expected size';
$this->assertCount(0, $method->invoke($indicator, strtotime('2015-10-20 00:00:00 GMT'),
strtotime('2015-10-21 00:00:00 GMT'), $stu1), $message);
$this->assertCount(0, $method->invoke($indicator, strtotime('2015-10-25 00:00:00 GMT'),
strtotime('2015-10-26 00:00:00 GMT'), $stu1), $message);
$this->assertCount(1, $method->invoke($indicator, strtotime('2015-10-22 00:00:00 GMT'),
strtotime('2015-10-25 00:00:00 GMT'), $stu1), $message);
}
}
/**
* setup_course
*
* @param stdClass $courserecord
* @return array
*/
protected function setup_course($courserecord = null) {
global $CFG;
$this->resetAfterTest(true);
$this->setAdminUser();
$CFG->enableavailability = true;
$course = $this->getDataGenerator()->create_course($courserecord);
$stu1 = $this->getDataGenerator()->create_user();
$this->getDataGenerator()->enrol_user($stu1->id, $course->id, 'student');
return array($course, $stu1);
}
/**
* Returns the module cognitive depth indicator and the reflection method.
*
* @param string $modulename
* @param \core_analytics\course $course
* @return array
*/
private function instantiate_indicator($modulename, \core_analytics\course $course) {
$classname = '\\' . $modulename . '\analytics\indicator\cognitive_depth';
$indicator = new $classname();
$class = new ReflectionClass($indicator);
$property = $class->getProperty('course');
$property->setValue($indicator, $course);
$method = new ReflectionMethod($indicator, 'get_activities');
return array($indicator, $method);
}
}
+168
View File
@@ -0,0 +1,168 @@
<?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/>.
namespace core_analytics;
/**
* Unit tests for course.
*
* @package core_analytics
* @copyright 2016 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class course_test extends \advanced_testcase {
/** @var \stdClass Course record. */
protected $course;
/** @var \stdClass Student 1 user record. */
protected $stu1;
/** @var \stdClass Student 2 user record. */
protected $stu2;
/** @var \stdClass Student both user record. */
protected $both;
/** @var \stdClass Editing teacher user record. */
protected $editingteacher;
/** @var \stdClass Teacher user record. */
protected $teacher;
/** @var int Student role ID record. */
protected $studentroleid;
/** @var int Editing teacher role ID record. */
protected $editingteacherroleid;
/** @var int Teacher role ID record. */
protected $teacherroleid;
public function setUp(): void {
global $DB;
$this->course = $this->getDataGenerator()->create_course(['startdate' => 0]);
$this->stu1 = $this->getDataGenerator()->create_user();
$this->stu2 = $this->getDataGenerator()->create_user();
$this->both = $this->getDataGenerator()->create_user();
$this->editingteacher = $this->getDataGenerator()->create_user();
$this->teacher = $this->getDataGenerator()->create_user();
$this->studentroleid = $DB->get_field('role', 'id', array('shortname' => 'student'));
$this->editingteacherroleid = $DB->get_field('role', 'id', array('shortname' => 'editingteacher'));
$this->teacherroleid = $DB->get_field('role', 'id', array('shortname' => 'teacher'));
$this->getDataGenerator()->enrol_user($this->stu1->id, $this->course->id, $this->studentroleid);
$this->getDataGenerator()->enrol_user($this->stu2->id, $this->course->id, $this->studentroleid);
$this->getDataGenerator()->enrol_user($this->both->id, $this->course->id, $this->studentroleid);
$this->getDataGenerator()->enrol_user($this->both->id, $this->course->id, $this->editingteacherroleid);
$this->getDataGenerator()->enrol_user($this->editingteacher->id, $this->course->id, $this->editingteacherroleid);
$this->getDataGenerator()->enrol_user($this->teacher->id, $this->course->id, $this->teacherroleid);
}
/**
* Users tests.
*/
public function test_users(): void {
global $DB;
$this->resetAfterTest(true);
$courseman = new \core_analytics\course($this->course->id);
$this->assertCount(3, $courseman->get_user_ids(array($this->studentroleid)));
$this->assertCount(2, $courseman->get_user_ids(array($this->editingteacherroleid)));
$this->assertCount(1, $courseman->get_user_ids(array($this->teacherroleid)));
// Distinct is applied.
$this->assertCount(3, $courseman->get_user_ids(array($this->editingteacherroleid, $this->teacherroleid)));
$this->assertCount(4, $courseman->get_user_ids(array($this->editingteacherroleid, $this->studentroleid)));
}
/**
* Course validation tests.
*
* @return void
*/
public function test_course_validation(): void {
global $DB;
$this->resetAfterTest(true);
$courseman = new \core_analytics\course($this->course->id);
$this->assertFalse($courseman->was_started());
$this->assertFalse($courseman->is_finished());
// Nothing should change when assigning as teacher.
for ($i = 0; $i < 10; $i++) {
$user = $this->getDataGenerator()->create_user();
$this->getDataGenerator()->enrol_user($user->id, $this->course->id, $this->teacherroleid);
}
$courseman = new \core_analytics\course($this->course->id);
$this->assertFalse($courseman->was_started());
$this->assertFalse($courseman->is_finished());
// More students now.
for ($i = 0; $i < 10; $i++) {
$user = $this->getDataGenerator()->create_user();
$this->getDataGenerator()->enrol_user($user->id, $this->course->id, $this->studentroleid);
}
$courseman = new \core_analytics\course($this->course->id);
$this->assertFalse($courseman->was_started());
$this->assertFalse($courseman->is_finished());
// Valid start date unknown end date.
$this->course->startdate = gmmktime('0', '0', '0', 10, 24, 2015);
$DB->update_record('course', $this->course);
$courseman = new \core_analytics\course($this->course->id);
$this->assertTrue($courseman->was_started());
$this->assertFalse($courseman->is_finished());
// Valid start and end date.
$this->course->enddate = gmmktime('0', '0', '0', 8, 27, 2016);
$DB->update_record('course', $this->course);
$courseman = new \core_analytics\course($this->course->id);
$this->assertTrue($courseman->was_started());
$this->assertTrue($courseman->is_finished());
// Valid start and ongoing course.
$this->course->enddate = gmmktime('0', '0', '0', 8, 27, 2286);
$DB->update_record('course', $this->course);
$courseman = new \core_analytics\course($this->course->id);
$this->assertTrue($courseman->was_started());
$this->assertFalse($courseman->is_finished());
}
/**
* Get the minimum time that is considered valid according to guess_end logic.
*
* @param int $time
* @return int
*/
protected function time_greater_than($time) {
return $time - (WEEKSECS * 2);
}
/**
* Get the maximum time that is considered valid according to guess_end logic.
*
* @param int $time
* @return int
*/
protected function time_less_than($time) {
return $time + (WEEKSECS * 2);
}
}
+155
View File
@@ -0,0 +1,155 @@
<?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/>.
namespace core_analytics;
/**
* Unit tests for the dataset manager.
*
* @package core_analytics
* @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class dataset_manager_test extends \advanced_testcase {
/** @var array Store dataset top rows. */
protected array $sharedtoprows = [];
/**
* setUp
*
* @return null
*/
public function setUp(): void {
$this->resetAfterTest(true);
$this->sharedtoprows = array(
array('var1', 'var2'),
array('value1', 'value2'),
array('header1', 'header2')
);
}
/**
* test_create_dataset
*
* @return null
*/
public function test_create_dataset(): void {
$dataset1 = new \core_analytics\dataset_manager(1, 1, 'whatever', \core_analytics\dataset_manager::LABELLED_FILEAREA, false);
$dataset1data = array_merge($this->sharedtoprows, array(array('yeah', 'yeah', 'yeah')));
$f1 = $dataset1->store($dataset1data);
$f1contents = $f1->get_content();
$this->assertStringContainsString('yeah', $f1contents);
$this->assertStringContainsString('var1', $f1contents);
$this->assertStringContainsString('value1', $f1contents);
$this->assertStringContainsString('header1', $f1contents);
}
/**
* test_merge_datasets
*
* @return null
*/
public function test_merge_datasets(): void {
$dataset1 = new \core_analytics\dataset_manager(1, 1, 'whatever', \core_analytics\dataset_manager::LABELLED_FILEAREA, false);
$dataset1data = array_merge($this->sharedtoprows, array(array('yeah', 'yeah', 'yeah')));
$f1 = $dataset1->store($dataset1data);
$dataset2 = new \core_analytics\dataset_manager(1, 2, 'whatever', \core_analytics\dataset_manager::LABELLED_FILEAREA, false);
$dataset2data = array_merge($this->sharedtoprows, array(array('no', 'no', 'no')));
$f2 = $dataset2->store($dataset2data);
$files = array($f1, $f2);
$merged = \core_analytics\dataset_manager::merge_datasets($files, 1, 'whatever',
\core_analytics\dataset_manager::LABELLED_FILEAREA);
$mergedfilecontents = $merged->get_content();
$this->assertStringContainsString('yeah', $mergedfilecontents);
$this->assertStringContainsString('no', $mergedfilecontents);
$this->assertStringContainsString('var1', $mergedfilecontents);
$this->assertStringContainsString('value1', $mergedfilecontents);
$this->assertStringContainsString('header1', $mergedfilecontents);
}
/**
* test_get_pending_files
*
* @return null
*/
public function test_get_pending_files(): void {
global $DB;
$this->resetAfterTest();
$fakemodelid = 123;
$timesplittingids = array(
'\core\analytics\time_splitting\quarters',
'\core\analytics\time_splitting\quarters_accum',
);
// No files.
$this->assertEmpty(\core_analytics\dataset_manager::get_pending_files($fakemodelid, true, $timesplittingids));
$this->assertEmpty(\core_analytics\dataset_manager::get_pending_files($fakemodelid, false, $timesplittingids));
// We will reuse this analysable file to create training and prediction datasets (analysable level files are
// merged into training and prediction files).
$analysabledataset = new \core_analytics\dataset_manager($fakemodelid, 1, 'whatever',
\core_analytics\dataset_manager::LABELLED_FILEAREA, false);
$analysabledatasetdata = array_merge($this->sharedtoprows, array(array('yeah', 'yeah', 'yeah')));
$file = $analysabledataset->store($analysabledatasetdata);
// Evaluation files ignored.
$evaluationdataset = \core_analytics\dataset_manager::merge_datasets(array($file), $fakemodelid,
'\core\analytics\time_splitting\quarters', \core_analytics\dataset_manager::LABELLED_FILEAREA, true);
$this->assertEmpty(\core_analytics\dataset_manager::get_pending_files($fakemodelid, true, $timesplittingids));
$this->assertEmpty(\core_analytics\dataset_manager::get_pending_files($fakemodelid, false, $timesplittingids));
// Training and prediction files are not mixed up.
$trainingfile1 = \core_analytics\dataset_manager::merge_datasets(array($file), $fakemodelid,
'\core\analytics\time_splitting\quarters', \core_analytics\dataset_manager::LABELLED_FILEAREA, false);
$this->waitForSecond();
$trainingfile2 = \core_analytics\dataset_manager::merge_datasets(array($file), $fakemodelid,
'\core\analytics\time_splitting\quarters', \core_analytics\dataset_manager::LABELLED_FILEAREA, false);
$bytimesplitting = \core_analytics\dataset_manager::get_pending_files($fakemodelid, true, $timesplittingids);
$this->assertFalse(isset($bytimesplitting['\core\analytics\time_splitting\quarters_accum']));
$this->assertCount(2, $bytimesplitting['\core\analytics\time_splitting\quarters']);
$this->assertEmpty(\core_analytics\dataset_manager::get_pending_files($fakemodelid, false, $timesplittingids));
$predictionfile = \core_analytics\dataset_manager::merge_datasets(array($file), $fakemodelid,
'\core\analytics\time_splitting\quarters', \core_analytics\dataset_manager::UNLABELLED_FILEAREA, false);
$bytimesplitting = \core_analytics\dataset_manager::get_pending_files($fakemodelid, false, $timesplittingids);
$this->assertFalse(isset($bytimesplitting['\core\analytics\time_splitting\quarters_accum']));
$this->assertCount(1, $bytimesplitting['\core\analytics\time_splitting\quarters']);
// Already used for training and prediction are discarded.
$usedfile = (object)['modelid' => $fakemodelid, 'fileid' => $trainingfile1->get_id(), 'action' => 'trained',
'time' => time()];
$DB->insert_record('analytics_used_files', $usedfile);
$bytimesplitting = \core_analytics\dataset_manager::get_pending_files($fakemodelid, true, $timesplittingids);
$this->assertCount(1, $bytimesplitting['\core\analytics\time_splitting\quarters']);
$usedfile->fileid = $predictionfile->get_id();
$usedfile->action = 'predicted';
$DB->insert_record('analytics_used_files', $usedfile);
$this->assertEmpty(\core_analytics\dataset_manager::get_pending_files($fakemodelid, false, $timesplittingids));
}
}
@@ -0,0 +1,37 @@
<?php
// This file is part of Moodle - https://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/>.
/**
* Example of an invalid db/analytics.php file content used for unit tests.
*
* @package core_analytics
* @category test
* @copyright 2019 David Mudrák <david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$models = [
[
'target' => 'test_target_course_level_shortname',
'indicators' => [
'\core_course\analytics\indicator\no_teacher',
],
// Cannot be enabled without timesplitting defined.
'enabled' => true,
],
];
@@ -0,0 +1,36 @@
<?php
// This file is part of Moodle - https://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/>.
/**
* Example of an invalid db/analytics.php file content used for unit tests.
*
* @package core_analytics
* @category test
* @copyright 2019 David Mudrák <david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$models = [
[
'target' => 'test_target_course_level_shortname',
'indicators' => [
'\core_course\analytics\indicator\no_teacher',
'\non\existing\class\name',
],
],
];
@@ -0,0 +1,36 @@
<?php
// This file is part of Moodle - https://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/>.
/**
* Example of an invalid db/analytics.php file content used for unit tests.
*
* @package core_analytics
* @category test
* @copyright 2019 David Mudrák <david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$models = [
[
'target' => 'there_should_be_a_valid_fully_qualified_classname_here',
'indicators' => [
'\core_course\analytics\indicator\no_teacher',
'\core_course\analytics\indicator\no_student',
],
],
];
@@ -0,0 +1,36 @@
<?php
// This file is part of Moodle - https://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/>.
/**
* Example of an invalid db/analytics.php file content used for unit tests.
*
* @package core_analytics
* @category test
* @copyright 2019 David Mudrák <david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$models = [
[
'target' => 'test_target_course_level_shortname',
'indicators' => [
'\core_course\analytics\indicator\no_teacher',
],
'timesplitting' => '\non\existing\class\name',
],
];
@@ -0,0 +1,36 @@
<?php
// This file is part of Moodle - https://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/>.
/**
* Example of an invalid db/analytics.php file content used for unit tests.
*
* @package core_analytics
* @category test
* @copyright 2019 David Mudrák <david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$models = [
[
'target' => 'test_target_course_level_shortname',
'indicators' => [
'\core_course\analytics\indicator\no_teacher',
],
'timesplitting' => 'local_customplugin_non_fully_qualified_class_name',
],
];
@@ -0,0 +1,32 @@
<?php
// This file is part of Moodle - https://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/>.
/**
* Example of an invalid db/analytics.php file content used for unit tests.
*
* @package core_analytics
* @category test
* @copyright 2019 David Mudrák <david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$models = [
[
'target' => 'test_target_course_level_shortname',
],
];
@@ -0,0 +1,35 @@
<?php
// This file is part of Moodle - https://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/>.
/**
* Example of an invalid db/analytics.php file content used for unit tests.
*
* @package core_analytics
* @category test
* @copyright 2019 David Mudrák <david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$models = [
[
'indicators' => [
'\core_course\analytics\indicator\no_teacher',
'\core_course\analytics\indicator\no_student',
],
],
];
@@ -0,0 +1,38 @@
<?php
// This file is part of Moodle - https://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/>.
/**
* Example of a valid db/analytics.php file content used for unit tests.
*
* @package core_analytics
* @category test
* @copyright 2019 David Mudrák <david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$models = [
[
'target' => '\core_course\analytics\target\no_teaching',
'indicators' => [
'\core_course\analytics\indicator\no_teacher',
'\core_course\analytics\indicator\no_student',
],
'timesplitting' => '\core\analytics\time_splitting\single_range',
'enabled' => true,
],
];
+47
View File
@@ -0,0 +1,47 @@
<?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/>.
/**
* Test analyser.
*
* @package core_analytics
* @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();
/**
* Test analyser.
*
* @package core_analytics
* @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class test_analysis extends \core_analytics\analysis {
/**
* Overwritten to add a delay.
*
* @param \core_analytics\analysable $analysable
* @return array
*/
public function process_analysable(\core_analytics\analysable $analysable): array {
// Half a second.
sleep(1);
return parent::process_analysable($analysable);
}
}
+90
View File
@@ -0,0 +1,90 @@
<?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/>.
/**
* Test indicator.
*
* @package core_analytics
* @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Test indicator.
*
* @package core_analytics
* @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class test_indicator_discrete extends \core_analytics\local\indicator\discrete {
/**
* Returns the name.
*
* If there is a corresponding '_help' string this will be shown as well.
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
// Using a string that exists and contains a corresponding '_help' string.
return new \lang_string('allowstealthmodules');
}
/**
* The different classes this discrete indicator provides.
* @return [type] [description]
*/
protected static function get_classes() {
return [0, 1, 2, 3, 4];
}
/**
* Just for testing.
*
* @param float $value
* @param string $subtype
* @return string
*/
public function get_calculation_outcome($value, $subtype = false) {
return self::OUTCOME_OK;
}
/**
* Custom indicator calculated value display as otherwise we would display meaningless numbers to users.
*
* @param float $value
* @param string $subtype
* @return string
*/
public function get_display_value($value, $subtype = false) {
return $value;
}
/**
* calculate_sample
*
* @param int $sampleid
* @param string $sampleorigin
* @param int $starttime
* @param int $endtime
* @return float
*/
protected function calculate_sample($sampleid, $sampleorigin, $starttime = false, $endtime = false) {
return 4;
}
}
+92
View File
@@ -0,0 +1,92 @@
<?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/>.
/**
* Test indicator.
*
* @package core_analytics
* @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();
/**
* Test indicator.
*
* @package core_analytics
* @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class test_indicator_fullname extends \core_analytics\local\indicator\linear {
/**
* Returns a lang_string object representing the name for the indicator.
*
* Used as column identificator.
*
* If there is a corresponding '_help' string this will be shown as well.
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
// Using a string that exists and contains a corresponding '_help' string.
return new \lang_string('allowstealthmodules');
}
/**
* include_averages
*
* @return bool
*/
protected static function include_averages() {
return false;
}
/**
* required_sample_data
*
* @return string[]
*/
public static function required_sample_data() {
return array('course');
}
/**
* calculate_sample
*
* @param int $sampleid
* @param string $samplesorigin
* @param int $starttime
* @param int $endtime
* @return float
*/
protected function calculate_sample($sampleid, $samplesorigin, $starttime, $endtime) {
$course = $this->retrieve('course', $sampleid);
$firstchar = substr($course->fullname, 0, 1);
if ($firstchar === 'a') {
return self::MIN_VALUE;
} else if ($firstchar === 'b') {
return -0.2;
} else if ($firstchar === 'c') {
return 0.2;
} else {
return self::MAX_VALUE;
}
}
}
+62
View File
@@ -0,0 +1,62 @@
<?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/>.
/**
* Test indicator.
*
* @package core_analytics
* @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();
/**
* Test indicator.
*
* @package core_analytics
* @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class test_indicator_max extends \core_analytics\local\indicator\binary {
/**
* Returns a lang_string object representing the name for the indicator.
*
* Used as column identificator.
*
* If there is a corresponding '_help' string this will be shown as well.
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
// Using a string that exists and contains a corresponding '_help' string.
return new \lang_string('allowstealthmodules');
}
/**
* calculate_sample
*
* @param int $sampleid
* @param string $samplesorigin
* @param int $starttime
* @param int $endtime
* @return float
*/
protected function calculate_sample($sampleid, $samplesorigin, $starttime, $endtime) {
return self::MAX_VALUE;
}
}
+62
View File
@@ -0,0 +1,62 @@
<?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/>.
/**
* Test indicator.
*
* @package core_analytics
* @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();
/**
* Test indicator.
*
* @package core_analytics
* @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class test_indicator_min extends \core_analytics\local\indicator\binary {
/**
* Returns a lang_string object representing the name for the indicator.
*
* Used as column identificator.
*
* If there is a corresponding '_help' string this will be shown as well.
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
// Using a string that exists and contains a corresponding '_help' string.
return new \lang_string('allowstealthmodules');
}
/**
* calculate_sample
*
* @param int $sampleid
* @param string $samplesorigin
* @param int $starttime
* @param int $endtime
* @return float
*/
protected function calculate_sample($sampleid, $samplesorigin, $starttime, $endtime) {
return self::MIN_VALUE;
}
}
+92
View File
@@ -0,0 +1,92 @@
<?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/>.
/**
* Multiclass test indicator.
*
* @package core_analytics
* @copyright 2019 Vlad Apetrei
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Multiclass test indicator.
*
* @package core_analytics
* @copyright 2019 Vlad Apetrei
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class test_indicator_multiclass extends \core_analytics\local\indicator\linear {
/**
* Returns a lang_string object representing the name for the indicator.
*
* Used as column identificator.
*
* If there is a corresponding '_help' string this will be shown as well.
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
// Using a string that exists and contains a corresponding '_help' string.
return new \lang_string('allowstealthmodules');
}
/**
* include_averages
*
* @return bool
*/
protected static function include_averages() {
return false;
}
/**
* required_sample_data
*
* @return string[]
*/
public static function required_sample_data() {
return array('course');
}
/**
* calculate_sample
*
* @param int $sampleid
* @param string $samplesorigin
* @param int $starttime
* @param int $endtime
* @return float
*/
protected function calculate_sample($sampleid, $samplesorigin, $starttime, $endtime) {
$course = $this->retrieve('course', $sampleid);
$firstchar = substr($course->fullname, 0, 1);
if ($firstchar === 'a') {
return 1;
} else if ($firstchar === 'b') {
return -1;
} else if ($firstchar === 'c') {
return 1;
} else {
return self::MAX_VALUE;
}
}
}
+62
View File
@@ -0,0 +1,62 @@
<?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/>.
/**
* Test indicator. Always null.
*
* @package core_analytics
* @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();
/**
* Test indicator. Always null.
*
* @package core_analytics
* @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class test_indicator_null extends \core_analytics\local\indicator\binary {
/**
* Returns a lang_string object representing the name for the indicator.
*
* Used as column identificator.
*
* If there is a corresponding '_help' string this will be shown as well.
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
// Using a string that exists and contains a corresponding '_help' string.
return new \lang_string('allowstealthmodules');
}
/**
* calculate_sample
*
* @param int $sampleid
* @param string $samplesorigin
* @param int $starttime
* @param int $endtime
* @return float
*/
protected function calculate_sample($sampleid, $samplesorigin, $starttime, $endtime) {
return null;
}
}
+62
View File
@@ -0,0 +1,62 @@
<?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/>.
/**
* Test indicator.
*
* @package core_analytics
* @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();
/**
* Test indicator.
*
* @package core_analytics
* @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class test_indicator_random extends \core_analytics\local\indicator\linear {
/**
* Returns a lang_string object representing the name for the indicator.
*
* Used as column identificator.
*
* If there is a corresponding '_help' string this will be shown as well.
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
// Using a string that exists and contains a corresponding '_help' string.
return new \lang_string('allowstealthmodules');
}
/**
* calculate_sample
*
* @param int $sampleid
* @param string $samplesorigin
* @param int $starttime
* @param int $endtime
* @return float
*/
protected function calculate_sample($sampleid, $samplesorigin, $starttime, $endtime) {
return mt_rand(-1, 1);
}
}
+147
View File
@@ -0,0 +1,147 @@
<?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/>.
/**
* Test analyser
*
* @package core
* @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Test analyser
*
* @package core
* @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class test_site_users_analyser extends \core_analytics\local\analyser\sitewide {
/**
* Samples origin is course table.
*
* @return string
*/
public function get_samples_origin() {
return 'user';
}
/**
* Returns the sample analysable
*
* @param int $sampleid
* @return \core_analytics\analysable
*/
public function get_sample_analysable($sampleid) {
return new \core_analytics\site();
}
/**
* Data this analyer samples provide.
*
* @return string[]
*/
protected function provided_sample_data() {
return array('user');
}
/**
* Returns the sample context.
*
* @param int $sampleid
* @return \context
*/
public function sample_access_context($sampleid) {
return \context_system::instance();
}
/**
* Returns all site courses.
*
* @param \core_analytics\analysable $site
* @return array
*/
public function get_all_samples(\core_analytics\analysable $site) {
global $DB;
$users = $DB->get_records('user');
$userids = array_keys($users);
$sampleids = array_combine($userids, $userids);
$users = array_map(function($user) {
return array('user' => $user);
}, $users);
return array($sampleids, $users);
}
/**
* Return all complete samples data from sample ids.
*
* @param int[] $sampleids
* @return array
*/
public function get_samples($sampleids) {
global $DB;
list($userssql, $params) = $DB->get_in_or_equal($sampleids, SQL_PARAMS_NAMED);
$users = $DB->get_records_select('user', "id {$userssql}", $params);
$userids = array_keys($users);
$sampleids = array_combine($userids, $userids);
$users = array_map(function($user) {
return array('user' => $user);
}, $users);
return array($sampleids, $users);
}
/**
* Returns the description of a sample.
*
* @param int $sampleid
* @param int $contextid
* @param array $sampledata
* @return array array(string, \renderable)
*/
public function sample_description($sampleid, $contextid, $sampledata) {
$description = fullname($sampledata['user']);
$userimage = new \pix_icon('i/user', get_string('user'));
return array($description, $userimage);
}
/**
* We need to delete associated data if a user requests his data to be deleted.
*
* @return bool
*/
public function processes_user_data() {
return true;
}
/**
* Join the samples origin table with the user id table.
*
* @param string $sampletablealias
* @return string
*/
public function join_sample_user($sampletablealias) {
return "JOIN {user} u ON u.id = {$sampletablealias}.sampleid";
}
}
@@ -0,0 +1,82 @@
<?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/>.
/**
* Test static target.
*
* @package core_analytics
* @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__ . '/test_target_shortname.php');
/**
* Test static target.
*
* Testing target extension to make it static and to use a different analyser
* just to try a different one. Method calculate_sample is exactly the same.
*
* @package core_analytics
* @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class test_static_target_shortname extends test_target_shortname {
/**
* Returns a lang_string object representing the name for the indicator.
*
* Used as column identificator.
*
* If there is a corresponding '_help' string this will be shown as well.
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
// Using a string that exists and contains a corresponding '_help' string.
return new \lang_string('allowstealthmodules');
}
/**
* based_on_assumptions
*
* @return bool
*/
public static function based_on_assumptions() {
return true;
}
/**
* Everything yep, this is just for testing.
*
* @param \core_analytics\local\time_splitting\base $timesplitting
* @return bool
*/
public function can_use_timesplitting(\core_analytics\local\time_splitting\base $timesplitting): bool {
return true;
}
/**
* Different analyser just to test a different one.
*
* @return string
*/
public function get_analyser_class() {
return '\core\analytics\analyser\courses';
}
}
@@ -0,0 +1,46 @@
<?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/>.
/**
* Test target.
*
* @package core_analytics
* @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__ . '/test_target_shortname.php');
/**
* Test target.
*
* @package core_analytics
* @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class test_target_course_level_shortname extends test_target_shortname {
/**
* get_analyser_class
*
* @return string
*/
public function get_analyser_class() {
return '\core\analytics\analyser\courses';
}
}
+60
View File
@@ -0,0 +1,60 @@
<?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/>.
/**
* Test target.
*
* @package core_analytics
* @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__ . '/test_target_site_users.php');
/**
* Test target.
*
* @package core_analytics
* @copyright 2018 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class test_target_course_users extends test_target_site_users {
/**
* get_analyser_class
*
* @return string
*/
public function get_analyser_class() {
return '\core\analytics\analyser\student_enrolments';
}
/**
* Returns a lang_string object representing the name for the indicator.
*
* Used as column identificator.
*
* If there is a corresponding '_help' string this will be shown as well.
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
// Using a string that exists and contains a corresponding '_help' string.
return new \lang_string('adminhelpedituser');
}
}
+159
View File
@@ -0,0 +1,159 @@
<?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/>.
/**
* Test target.
*
* @package core_analytics
* @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();
/**
* Test target.
*
* @package core_analytics
* @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class test_target_shortname extends \core_analytics\local\target\binary {
/**
* Returns a lang_string object representing the name for the indicator.
*
* Used as column identificator.
*
* If there is a corresponding '_help' string this will be shown as well.
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
// Using a string that exists and contains a corresponding '_help' string.
return new \lang_string('allowstealthmodules');
}
/**
* predictions
*
* @var array
*/
protected $predictions = array();
/**
* get_analyser_class
*
* @return string
*/
public function get_analyser_class() {
return '\core\analytics\analyser\site_courses';
}
/**
* classes_description
*
* @return string[]
*/
public static function classes_description() {
return array(
'Course fullname first char is A',
'Course fullname first char is not A'
);
}
/**
* We don't want to discard results.
* @return float
*/
protected function min_prediction_score() {
return null;
}
/**
* We don't want to discard results.
* @return array
*/
public function ignored_predicted_classes() {
return array();
}
/**
* Only past stuff.
*
* @param \core_analytics\local\time_splitting\base $timesplitting
* @return bool
*/
public function can_use_timesplitting(\core_analytics\local\time_splitting\base $timesplitting): bool {
return ($timesplitting instanceof \core_analytics\local\time_splitting\before_now);
}
/**
* is_valid_analysable
*
* @param \core_analytics\analysable $analysable
* @param bool $fortraining
* @return bool
*/
public function is_valid_analysable(\core_analytics\analysable $analysable, $fortraining = true) {
// This is testing, let's make things easy.
return true;
}
/**
* is_valid_sample
*
* @param int $sampleid
* @param \core_analytics\analysable $analysable
* @param bool $fortraining
* @return bool
*/
public function is_valid_sample($sampleid, \core_analytics\analysable $analysable, $fortraining = true) {
// We skip not-visible courses during training as a way to emulate the training data / prediction data difference.
// In normal circumstances is_valid_sample will return false when they receive a sample that can not be
// processed.
if (!$fortraining) {
return true;
}
$sample = $this->retrieve('course', $sampleid);
if ($sample->visible == 0) {
return false;
}
return true;
}
/**
* calculate_sample
*
* @param int $sampleid
* @param \core_analytics\analysable $analysable
* @param int $starttime
* @param int $endtime
* @return float
*/
protected function calculate_sample($sampleid, \core_analytics\analysable $analysable, $starttime = false, $endtime = false) {
$sample = $this->retrieve('course', $sampleid);
$firstchar = substr($sample->shortname, 0, 1);
if ($firstchar === 'a') {
return 1;
} else {
return 0;
}
}
}
@@ -0,0 +1,211 @@
<?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/>.
/**
* Multi-class classifier target.
*
* @package core_analytics
* @copyright 2019 Apetrei Vlad
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Multi-class classifier target.
*
* @package core_analytics
* @copyright 2019 Apetrei Vlad
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class test_target_shortname_multiclass extends \core_analytics\local\target\discrete {
/**
* Returns a lang_string object representing the name for the indicator.
*
* Used as column identificator.
*
* If there is a corresponding '_help' string this will be shown as well.
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
// Using a string that exists and contains a corresponding '_help' string.
return new \lang_string('allowstealthmodules');
}
/**
* predictions
*
* @var array
*/
protected $predictions = array();
/**
* is_linear
*
* @return bool
*/
public function is_linear() {
return false;
}
/**
* Returns the target discrete values.
*
* Only useful for targets using discrete values, must be overwriten if it is the case.
*
* @return array
*/
final public static function get_classes() {
return array(0, 1, 2);
}
/**
* Is the calculated value a positive outcome of this target?
*
* @param string $value
* @param string $ignoredsubtype
* @return int
*/
public function get_calculation_outcome($value, $ignoredsubtype = false) {
if (!self::is_a_class($value)) {
throw new \moodle_exception('errorpredictionformat', 'analytics');
}
if (in_array($value, $this->ignored_predicted_classes(), false)) {
// Just in case, if it is ignored the prediction should not even be recorded but if it would, it is ignored now,
// which should mean that is it nothing serious.
return self::OUTCOME_VERY_POSITIVE;
}
// By default binaries are danger when prediction = 1.
if ($value) {
return self::OUTCOME_VERY_NEGATIVE;
}
return self::OUTCOME_VERY_POSITIVE;
}
/**
* get_analyser_class
*
* @return string
*/
public function get_analyser_class() {
return '\core\analytics\analyser\site_courses';
}
/**
* We don't want to discard results.
* @return float
*/
protected function min_prediction_score() {
return null;
}
/**
* We don't want to discard results.
* @return array
*/
public function ignored_predicted_classes() {
return array();
}
/**
* is_valid_analysable
*
* @param \core_analytics\analysable $analysable
* @param bool $fortraining
* @return bool
*/
public function is_valid_analysable(\core_analytics\analysable $analysable, $fortraining = true) {
// This is testing, let's make things easy.
return true;
}
/**
* is_valid_sample
*
* @param int $sampleid
* @param \core_analytics\analysable $analysable
* @param bool $fortraining
* @return bool
*/
public function is_valid_sample($sampleid, \core_analytics\analysable $analysable, $fortraining = true) {
// We skip not-visible courses during training as a way to emulate the training data / prediction data difference.
// In normal circumstances is_valid_sample will return false when they receive a sample that can not be
// processed.
if (!$fortraining) {
return true;
}
$sample = $this->retrieve('course', $sampleid);
if ($sample->visible == 0) {
return false;
}
return true;
}
/**
* classes_description
*
* @return string[]
*/
protected static function classes_description() {
return array(
get_string('first class'),
get_string('second class'),
get_string('third class')
);
}
/**
* calculate_sample
*
* @param int $sampleid
* @param \core_analytics\analysable $analysable
* @param int $starttime
* @param int $endtime
* @return float
*/
protected function calculate_sample($sampleid, \core_analytics\analysable $analysable, $starttime = false, $endtime = false) {
$sample = $this->retrieve('course', $sampleid);
$firstchar = substr($sample->shortname, 0, 1);
switch ($firstchar) {
case 'a':
return 0;
case 'b':
return 1;
case 'c':
return 2;
}
}
/**
* Can the provided time-splitting method be used on this target?.
*
* Time-splitting methods not matching the target requirements will not be selectable by models based on this target.
*
* @param \core_analytics\local\time_splitting\base $timesplitting
* @return bool
*/
public function can_use_timesplitting(\core_analytics\local\time_splitting\base $timesplitting): bool {
return true;
}
}
+161
View File
@@ -0,0 +1,161 @@
<?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/>.
/**
* Test target.
*
* @package core_analytics
* @copyright 2018 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__ . '/test_site_users_analyser.php');
/**
* Test target.
*
* @package core_analytics
* @copyright 2018 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class test_target_site_users extends \core_analytics\local\target\binary {
/**
* Returns a lang_string object representing the name for the indicator.
*
* Used as column identificator.
*
* If there is a corresponding '_help' string this will be shown as well.
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
// Using a string that exists and contains a corresponding '_help' string.
return new \lang_string('adminhelplogs');
}
/**
* predictions
*
* @var array
*/
protected $predictions = array();
/**
* get_analyser_class
*
* @return string
*/
public function get_analyser_class() {
return 'test_site_users_analyser';
}
/**
* Everything yep, this is just for testing.
*
* @param \core_analytics\local\time_splitting\base $timesplitting
* @return bool
*/
public function can_use_timesplitting(\core_analytics\local\time_splitting\base $timesplitting): bool {
return true;
}
/**
* classes_description
*
* @return string[]
*/
public static function classes_description() {
return array(
'firstname first char is A',
'firstname first char is not A'
);
}
/**
* We don't want to discard results.
* @return float
*/
protected function min_prediction_score() {
return null;
}
/**
* We don't want to discard results.
* @return array
*/
public function ignored_predicted_classes() {
return array();
}
/**
* is_valid_analysable
*
* @param \core_analytics\analysable $analysable
* @param bool $fortraining
* @return bool
*/
public function is_valid_analysable(\core_analytics\analysable $analysable, $fortraining = true) {
// This is testing, let's make things easy.
return true;
}
/**
* is_valid_sample
*
* @param int $sampleid
* @param \core_analytics\analysable $analysable
* @param bool $fortraining
* @return bool
*/
public function is_valid_sample($sampleid, \core_analytics\analysable $analysable, $fortraining = true) {
// We skip not-visible courses during training as a way to emulate the training data / prediction data difference.
// In normal circumstances is_valid_sample will return false when they receive a sample that can not be
// processed.
if (!$fortraining) {
return true;
}
$sample = $this->retrieve('user', $sampleid);
if ($sample->lastname == 'b') {
return false;
}
return true;
}
/**
* calculate_sample
*
* @param int $sampleid
* @param \core_analytics\analysable $analysable
* @param int $starttime
* @param int $endtime
* @return float
*/
protected function calculate_sample($sampleid, \core_analytics\analysable $analysable, $starttime = false, $endtime = false) {
$sample = $this->retrieve('user', $sampleid);
$firstchar = substr($sample->firstname, 0, 1);
if ($firstchar === 'a') {
return 1;
} else {
return 0;
}
}
}
+52
View File
@@ -0,0 +1,52 @@
<?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/>.
/**
* Test time splitting.
*
* @package core_analytics
* @copyright 2019 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Test time splitting.
*
* @package core_analytics
* @copyright 2019 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class test_timesplitting_seconds extends \core_analytics\local\time_splitting\past_periodic {
/**
* Every second.
* @return \DateInterval
*/
public function periodicity() {
return new \DateInterval('PT1S');
}
/**
* Just to comply with the interface.
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
return new \lang_string('error');
}
}
@@ -0,0 +1,52 @@
<?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/>.
/**
* Test time splitting.
*
* @package core_analytics
* @copyright 2019 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Test time splitting.
*
* @package core_analytics
* @copyright 2019 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class test_timesplitting_upcoming_seconds extends \core_analytics\local\time_splitting\upcoming_periodic {
/**
* Every second.
* @return \DateInterval
*/
public function periodicity() {
return new \DateInterval('PT1S');
}
/**
* Just to comply with the interface.
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
return new \lang_string('error');
}
}
+93
View File
@@ -0,0 +1,93 @@
<?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/>.
namespace core_analytics;
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/fixtures/test_indicator_max.php');
require_once(__DIR__ . '/fixtures/test_indicator_discrete.php');
require_once(__DIR__ . '/fixtures/test_indicator_min.php');
/**
* Unit tests for the model.
*
* @package core_analytics
* @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class indicator_test extends \advanced_testcase {
/**
* test_validate_calculated_value
*
* @param string $indicatorclass
* @param array $returnedvalue
* @dataProvider validate_calculated_value
* @return null
*/
public function test_validate_calculated_value($indicatorclass, $returnedvalue): void {
$indicator = new $indicatorclass();
list($values, $unused) = $indicator->calculate([1], 'notrelevanthere');
$this->assertEquals($returnedvalue, $values[0]);
}
/**
* Data provider for test_validate_calculated_value
*
* @return array
*/
public function validate_calculated_value() {
return [
'max' => ['test_indicator_max', [1]],
'min' => ['test_indicator_min', [-1]],
'discrete' => ['test_indicator_discrete', [0, 0, 0, 0, 1]],
];
}
/**
* test_validate_calculated_value_exceptions
*
* @param string $indicatorclass
* @param string $willreturn
* @dataProvider validate_calculated_value_exceptions
* @return null
*/
public function test_validate_calculated_value_exceptions($indicatorclass, $willreturn): void {
$indicator = new $indicatorclass();
$indicatormock = $this->getMockBuilder(get_class($indicator))
->onlyMethods(['calculate_sample'])
->getMock();
$indicatormock->method('calculate_sample')->willReturn($willreturn);
$this->expectException(\coding_exception::class);
list($values, $unused) = $indicatormock->calculate([1], 'notrelevanthere');
}
/**
* Data provider for test_validate_calculated_value_exceptions
*
* @return array
*/
public function validate_calculated_value_exceptions() {
return [
'max' => ['test_indicator_max', 2],
'min' => ['test_indicator_min', -2],
'discrete' => ['test_indicator_discrete', 7],
];
}
}
+554
View File
@@ -0,0 +1,554 @@
<?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/>.
namespace core_analytics;
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/fixtures/test_indicator_max.php');
require_once(__DIR__ . '/fixtures/test_indicator_min.php');
require_once(__DIR__ . '/fixtures/test_indicator_fullname.php');
require_once(__DIR__ . '/fixtures/test_target_course_level_shortname.php');
/**
* Unit tests for the core_analytics manager.
*
* @package core_analytics
* @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \core_analytics\manager
*/
class manager_test extends \advanced_testcase {
/**
* test_deleted_context
*/
public function test_deleted_context(): void {
global $DB;
$this->resetAfterTest(true);
$this->setAdminuser();
set_config('enabled_stores', 'logstore_standard', 'tool_log');
$target = \core_analytics\manager::get_target('test_target_course_level_shortname');
$indicators = array('test_indicator_max', 'test_indicator_min', 'test_indicator_fullname');
foreach ($indicators as $key => $indicator) {
$indicators[$key] = \core_analytics\manager::get_indicator($indicator);
}
$model = \core_analytics\model::create($target, $indicators);
$modelobj = $model->get_model_obj();
$coursepredict1 = $this->getDataGenerator()->create_course(array('visible' => 0));
$coursepredict2 = $this->getDataGenerator()->create_course(array('visible' => 0));
$coursetrain1 = $this->getDataGenerator()->create_course(array('visible' => 1));
$coursetrain2 = $this->getDataGenerator()->create_course(array('visible' => 1));
$model->enable('\core\analytics\time_splitting\no_splitting');
$model->train();
$model->predict();
// Generate a prediction action to confirm that it is deleted when there is an important update.
$predictions = $DB->get_records('analytics_predictions');
$prediction = reset($predictions);
$prediction = new \core_analytics\prediction($prediction, array('whatever' => 'not used'));
$prediction->action_executed(\core_analytics\prediction::ACTION_USEFUL, $model->get_target());
$predictioncontextid = $prediction->get_prediction_data()->contextid;
$npredictions = $DB->count_records('analytics_predictions', array('contextid' => $predictioncontextid));
$npredictionactions = $DB->count_records('analytics_prediction_actions',
array('predictionid' => $prediction->get_prediction_data()->id));
$nindicatorcalc = $DB->count_records('analytics_indicator_calc', array('contextid' => $predictioncontextid));
\core_analytics\manager::cleanup();
// Nothing is incorrectly deleted.
$this->assertEquals($npredictions, $DB->count_records('analytics_predictions',
array('contextid' => $predictioncontextid)));
$this->assertEquals($npredictionactions, $DB->count_records('analytics_prediction_actions',
array('predictionid' => $prediction->get_prediction_data()->id)));
$this->assertEquals($nindicatorcalc, $DB->count_records('analytics_indicator_calc',
array('contextid' => $predictioncontextid)));
// Now we delete a context, the course predictions and prediction actions should be deleted.
$deletedcontext = \context::instance_by_id($predictioncontextid);
delete_course($deletedcontext->instanceid, false);
\core_analytics\manager::cleanup();
$this->assertEmpty($DB->count_records('analytics_predictions', array('contextid' => $predictioncontextid)));
$this->assertEmpty($DB->count_records('analytics_prediction_actions',
array('predictionid' => $prediction->get_prediction_data()->id)));
$this->assertEmpty($DB->count_records('analytics_indicator_calc', array('contextid' => $predictioncontextid)));
set_config('enabled_stores', '', 'tool_log');
get_log_manager(true);
}
/**
* test_deleted_analysable
*/
public function test_deleted_analysable(): void {
global $DB;
$this->resetAfterTest(true);
$this->setAdminuser();
set_config('enabled_stores', 'logstore_standard', 'tool_log');
$target = \core_analytics\manager::get_target('test_target_course_level_shortname');
$indicators = array('test_indicator_max', 'test_indicator_min', 'test_indicator_fullname');
foreach ($indicators as $key => $indicator) {
$indicators[$key] = \core_analytics\manager::get_indicator($indicator);
}
$model = \core_analytics\model::create($target, $indicators);
$modelobj = $model->get_model_obj();
$coursepredict1 = $this->getDataGenerator()->create_course(array('visible' => 0));
$coursepredict2 = $this->getDataGenerator()->create_course(array('visible' => 0));
$coursetrain1 = $this->getDataGenerator()->create_course(array('visible' => 1));
$coursetrain2 = $this->getDataGenerator()->create_course(array('visible' => 1));
$model->enable('\core\analytics\time_splitting\no_splitting');
$model->train();
$model->predict();
$this->assertNotEmpty($DB->count_records('analytics_predict_samples'));
$this->assertNotEmpty($DB->count_records('analytics_train_samples'));
$this->assertNotEmpty($DB->count_records('analytics_used_analysables'));
// Now we delete an analysable, stored predict and training samples should be deleted.
$deletedcontext = \context_course::instance($coursepredict1->id);
delete_course($coursepredict1, false);
\core_analytics\manager::cleanup();
$this->assertEmpty($DB->count_records('analytics_predict_samples', array('analysableid' => $coursepredict1->id)));
$this->assertEmpty($DB->count_records('analytics_train_samples', array('analysableid' => $coursepredict1->id)));
$this->assertEmpty($DB->count_records('analytics_used_analysables', array('analysableid' => $coursepredict1->id)));
set_config('enabled_stores', '', 'tool_log');
get_log_manager(true);
}
/**
* Tests for the {@link \core_analytics\manager::load_default_models_for_component()} implementation.
*/
public function test_load_default_models_for_component(): void {
$this->resetAfterTest();
// Attempting to load builtin models should always work without throwing exception.
\core_analytics\manager::load_default_models_for_component('core');
// Attempting to load from a core subsystem without its own subsystem directory.
$this->assertSame([], \core_analytics\manager::load_default_models_for_component('core_access'));
// Attempting to load from a non-existing subsystem.
$this->assertSame([], \core_analytics\manager::load_default_models_for_component('core_nonexistingsubsystem'));
// Attempting to load from a non-existing plugin of a known plugin type.
$this->assertSame([], \core_analytics\manager::load_default_models_for_component('mod_foobarbazquaz12240996776'));
// Attempting to load from a non-existing plugin type.
$this->assertSame([], \core_analytics\manager::load_default_models_for_component('foo_bar2776327736558'));
}
/**
* Tests for the {@link \core_analytics\manager::load_default_models_for_all_components()} implementation.
*/
public function test_load_default_models_for_all_components(): void {
$this->resetAfterTest();
$models = \core_analytics\manager::load_default_models_for_all_components();
$this->assertTrue(is_array($models['core']));
$this->assertNotEmpty($models['core']);
$this->assertNotEmpty($models['core'][0]['target']);
$this->assertNotEmpty($models['core'][0]['indicators']);
}
/**
* Tests for the successful execution of the {@link \core_analytics\manager::validate_models_declaration()}.
*/
public function test_validate_models_declaration(): void {
$this->resetAfterTest();
// This is expected to run without an exception.
$models = $this->load_models_from_fixture_file('no_teaching');
\core_analytics\manager::validate_models_declaration($models);
}
/**
* Tests for the exceptions thrown by {@link \core_analytics\manager::validate_models_declaration()}.
*
* @dataProvider validate_models_declaration_exceptions_provider
* @param array $models Models declaration.
* @param string $exception Expected coding exception message.
*/
public function test_validate_models_declaration_exceptions(array $models, string $exception): void {
$this->resetAfterTest();
$this->expectException(\coding_exception::class);
$this->expectExceptionMessage($exception);
\core_analytics\manager::validate_models_declaration($models);
}
/**
* Data provider for the {@link self::test_validate_models_declaration_exceptions()}.
*
* @return array of (string)testcase => [(array)models, (string)expected exception message]
*/
public function validate_models_declaration_exceptions_provider() {
return [
'missing_target' => [
$this->load_models_from_fixture_file('missing_target'),
'Missing target declaration',
],
'invalid_target' => [
$this->load_models_from_fixture_file('invalid_target'),
'Invalid target classname',
],
'missing_indicators' => [
$this->load_models_from_fixture_file('missing_indicators'),
'Missing indicators declaration',
],
'invalid_indicators' => [
$this->load_models_from_fixture_file('invalid_indicators'),
'Invalid indicator classname',
],
'invalid_time_splitting' => [
$this->load_models_from_fixture_file('invalid_time_splitting'),
'Invalid time splitting classname',
],
'invalid_time_splitting_fq' => [
$this->load_models_from_fixture_file('invalid_time_splitting_fq'),
'Expecting fully qualified time splitting classname',
],
'invalid_enabled' => [
$this->load_models_from_fixture_file('invalid_enabled'),
'Cannot enable a model without time splitting method specified',
],
];
}
/**
* Loads models as declared in the given fixture file.
*
* @param string $filename
* @return array
*/
protected function load_models_from_fixture_file(string $filename) {
global $CFG;
$models = null;
require($CFG->dirroot.'/analytics/tests/fixtures/db_analytics_php/'.$filename.'.php');
return $models;
}
/**
* Test the implementation of the {@link \core_analytics\manager::create_declared_model()}.
*/
public function test_create_declared_model(): void {
global $DB;
$this->resetAfterTest();
$this->setAdminuser();
$declaration = [
'target' => 'test_target_course_level_shortname',
'indicators' => [
'test_indicator_max',
'test_indicator_min',
'test_indicator_fullname',
],
];
$declarationwithtimesplitting = array_merge($declaration, [
'timesplitting' => '\core\analytics\time_splitting\no_splitting',
]);
$declarationwithtimesplittingenabled = array_merge($declarationwithtimesplitting, [
'enabled' => true,
]);
// Check that no such model exists yet.
$target = \core_analytics\manager::get_target('test_target_course_level_shortname');
$this->assertEquals(0, $DB->count_records('analytics_models', ['target' => $target->get_id()]));
$this->assertFalse(\core_analytics\model::exists($target));
// Check that the model is created.
$created = \core_analytics\manager::create_declared_model($declaration);
$this->assertTrue($created instanceof \core_analytics\model);
$this->assertTrue(\core_analytics\model::exists($target));
$this->assertEquals(1, $DB->count_records('analytics_models', ['target' => $target->get_id()]));
$modelid = $created->get_id();
// Check that created models are disabled by default.
$existing = new \core_analytics\model($modelid);
$this->assertEquals(0, $existing->get_model_obj()->enabled);
$this->assertEquals(0, $DB->get_field('analytics_models', 'enabled', ['target' => $target->get_id()], MUST_EXIST));
// Let the admin enable the model.
$existing->enable('\core\analytics\time_splitting\no_splitting');
$this->assertEquals(1, $DB->get_field('analytics_models', 'enabled', ['target' => $target->get_id()], MUST_EXIST));
// Check that further calls create a new model.
$repeated = \core_analytics\manager::create_declared_model($declaration);
$this->assertTrue($repeated instanceof \core_analytics\model);
$this->assertEquals(2, $DB->count_records('analytics_models', ['target' => $target->get_id()]));
// Delete the models.
$existing->delete();
$repeated->delete();
$this->assertEquals(0, $DB->count_records('analytics_models', ['target' => $target->get_id()]));
$this->assertFalse(\core_analytics\model::exists($target));
// Create it again, this time with time splitting method specified.
$created = \core_analytics\manager::create_declared_model($declarationwithtimesplitting);
$this->assertTrue($created instanceof \core_analytics\model);
$this->assertTrue(\core_analytics\model::exists($target));
$this->assertEquals(1, $DB->count_records('analytics_models', ['target' => $target->get_id()]));
$modelid = $created->get_id();
// Even if the time splitting method was specified, the model is still not enabled automatically.
$existing = new \core_analytics\model($modelid);
$this->assertEquals(0, $existing->get_model_obj()->enabled);
$this->assertEquals(0, $DB->get_field('analytics_models', 'enabled', ['target' => $target->get_id()], MUST_EXIST));
$existing->delete();
// Let's define the model so that it is enabled by default.
$enabled = \core_analytics\manager::create_declared_model($declarationwithtimesplittingenabled);
$this->assertTrue($enabled instanceof \core_analytics\model);
$this->assertTrue(\core_analytics\model::exists($target));
$this->assertEquals(1, $DB->count_records('analytics_models', ['target' => $target->get_id()]));
$modelid = $enabled->get_id();
$existing = new \core_analytics\model($modelid);
$this->assertEquals(1, $existing->get_model_obj()->enabled);
$this->assertEquals(1, $DB->get_field('analytics_models', 'enabled', ['target' => $target->get_id()], MUST_EXIST));
// Let the admin disable the model.
$existing->update(0, false, false);
$this->assertEquals(0, $DB->get_field('analytics_models', 'enabled', ['target' => $target->get_id()], MUST_EXIST));
}
/**
* Test the implementation of the {@link \core_analytics\manager::update_default_models_for_component()}.
*/
public function test_update_default_models_for_component(): void {
$this->resetAfterTest();
$this->setAdminuser();
$noteaching = \core_analytics\manager::get_target('\core_course\analytics\target\no_teaching');
$dropout = \core_analytics\manager::get_target('\core_course\analytics\target\course_dropout');
$upcomingactivities = \core_analytics\manager::get_target('\core_user\analytics\target\upcoming_activities_due');
$norecentaccesses = \core_analytics\manager::get_target('\core_course\analytics\target\no_recent_accesses');
$noaccesssincestart = \core_analytics\manager::get_target('\core_course\analytics\target\no_access_since_course_start');
$this->assertTrue(\core_analytics\model::exists($noteaching));
$this->assertTrue(\core_analytics\model::exists($dropout));
$this->assertTrue(\core_analytics\model::exists($upcomingactivities));
$this->assertTrue(\core_analytics\model::exists($norecentaccesses));
$this->assertTrue(\core_analytics\model::exists($noaccesssincestart));
foreach (\core_analytics\manager::get_all_models() as $model) {
$model->delete();
}
$this->assertFalse(\core_analytics\model::exists($noteaching));
$this->assertFalse(\core_analytics\model::exists($dropout));
$this->assertFalse(\core_analytics\model::exists($upcomingactivities));
$this->assertFalse(\core_analytics\model::exists($norecentaccesses));
$this->assertFalse(\core_analytics\model::exists($noaccesssincestart));
$updated = \core_analytics\manager::update_default_models_for_component('moodle');
$this->assertEquals(5, count($updated));
$this->assertTrue(array_pop($updated) instanceof \core_analytics\model);
$this->assertTrue(array_pop($updated) instanceof \core_analytics\model);
$this->assertTrue(array_pop($updated) instanceof \core_analytics\model);
$this->assertTrue(array_pop($updated) instanceof \core_analytics\model);
$this->assertTrue(array_pop($updated) instanceof \core_analytics\model);
$this->assertTrue(\core_analytics\model::exists($noteaching));
$this->assertTrue(\core_analytics\model::exists($dropout));
$this->assertTrue(\core_analytics\model::exists($upcomingactivities));
$this->assertTrue(\core_analytics\model::exists($norecentaccesses));
$this->assertTrue(\core_analytics\model::exists($noaccesssincestart));
$repeated = \core_analytics\manager::update_default_models_for_component('moodle');
$this->assertSame([], $repeated);
}
/**
* test_get_time_splitting_methods description
* @return null
*/
public function test_get_time_splitting_methods(): void {
$this->resetAfterTest(true);
$all = \core_analytics\manager::get_all_time_splittings();
$this->assertArrayHasKey('\core\analytics\time_splitting\upcoming_week', $all);
$this->assertArrayHasKey('\core\analytics\time_splitting\quarters', $all);
$allforevaluation = \core_analytics\manager::get_time_splitting_methods_for_evaluation(true);
$this->assertArrayNotHasKey('\core\analytics\time_splitting\upcoming_week', $allforevaluation);
$this->assertArrayHasKey('\core\analytics\time_splitting\quarters', $allforevaluation);
$defaultforevaluation = \core_analytics\manager::get_time_splitting_methods_for_evaluation(false);
$this->assertArrayNotHasKey('\core\analytics\time_splitting\upcoming_week', $defaultforevaluation);
$this->assertArrayHasKey('\core\analytics\time_splitting\quarters', $defaultforevaluation);
$sometimesplittings = '\core\analytics\time_splitting\single_range,' .
'\core\analytics\time_splitting\tenths';
set_config('defaulttimesplittingsevaluation', $sometimesplittings, 'analytics');
$defaultforevaluation = \core_analytics\manager::get_time_splitting_methods_for_evaluation(false);
$this->assertArrayNotHasKey('\core\analytics\time_splitting\quarters', $defaultforevaluation);
}
/**
* Test the implementation of the {@link \core_analytics\manager::model_declaration_identifier()}.
*/
public function test_model_declaration_identifier(): void {
$noteaching1 = $this->load_models_from_fixture_file('no_teaching');
$noteaching2 = $this->load_models_from_fixture_file('no_teaching');
$noteaching3 = $this->load_models_from_fixture_file('no_teaching');
// Same model declaration should always lead to same identifier.
$this->assertEquals(
\core_analytics\manager::model_declaration_identifier(reset($noteaching1)),
\core_analytics\manager::model_declaration_identifier(reset($noteaching2))
);
// If something is changed, the identifier should change, too.
$noteaching2[0]['target'] .= '_';
$this->assertNotEquals(
\core_analytics\manager::model_declaration_identifier(reset($noteaching1)),
\core_analytics\manager::model_declaration_identifier(reset($noteaching2))
);
$noteaching3[0]['indicators'][] = '\core_analytics\local\indicator\binary';
$this->assertNotEquals(
\core_analytics\manager::model_declaration_identifier(reset($noteaching1)),
\core_analytics\manager::model_declaration_identifier(reset($noteaching3))
);
// The identifier is supposed to contain PARAM_ALPHANUM only.
$this->assertEquals(
\core_analytics\manager::model_declaration_identifier(reset($noteaching1)),
clean_param(\core_analytics\manager::model_declaration_identifier(reset($noteaching1)), PARAM_ALPHANUM)
);
$this->assertEquals(
\core_analytics\manager::model_declaration_identifier(reset($noteaching2)),
clean_param(\core_analytics\manager::model_declaration_identifier(reset($noteaching2)), PARAM_ALPHANUM)
);
$this->assertEquals(
\core_analytics\manager::model_declaration_identifier(reset($noteaching3)),
clean_param(\core_analytics\manager::model_declaration_identifier(reset($noteaching3)), PARAM_ALPHANUM)
);
}
/**
* Tests for the {@link \core_analytics\manager::get_declared_target_and_indicators_instances()}.
*/
public function test_get_declared_target_and_indicators_instances(): void {
$this->resetAfterTest();
$definition = $this->load_models_from_fixture_file('no_teaching');
list($target, $indicators) = \core_analytics\manager::get_declared_target_and_indicators_instances($definition[0]);
$this->assertTrue($target instanceof \core_analytics\local\target\base);
$this->assertNotEmpty($indicators);
$this->assertContainsOnlyInstancesOf(\core_analytics\local\indicator\base::class, $indicators);
}
/**
* test_get_potential_context_restrictions description
*/
public function test_get_potential_context_restrictions(): void {
$this->resetAfterTest();
// No potential context restrictions.
$this->assertFalse(\core_analytics\manager::get_potential_context_restrictions([]));
$defaultcategory = \core_course_category::get_default();
$defaultcategorycontext = $defaultcategory->get_context();
// Include the all context levels so the misc. category get included.
$this->assertEquals([
$defaultcategorycontext->id => "Category: {$defaultcategory->name}",
], manager::get_potential_context_restrictions());
$category = $this->getDataGenerator()->create_category(['name' => 'My category']);
$categorycontext = $category->get_context();
$courseone = $this->getDataGenerator()->create_course(['fullname' => 'Course one', 'shortname' => 'CS1']);
$courseonecontext = \context_course::instance($courseone->id);
$coursetwo = $this->getDataGenerator()->create_course(['fullname' => 'Course two', 'shortname' => 'CS2']);
$coursetwocontext = \context_course::instance($coursetwo->id);
// All context levels.
$this->assertEqualsCanonicalizing([
$defaultcategorycontext->id => "Category: {$defaultcategory->name}",
$categorycontext->id => "Category: {$category->name}",
$courseonecontext->id => "Course: {$courseone->shortname}",
$coursetwocontext->id => "Course: {$coursetwo->shortname}",
], manager::get_potential_context_restrictions());
// All category/course context levels.
$this->assertEqualsCanonicalizing([
$defaultcategorycontext->id => "Category: {$defaultcategory->name}",
$categorycontext->id => "Category: {$category->name}",
$courseonecontext->id => "Course: {$courseone->shortname}",
$coursetwocontext->id => "Course: {$coursetwo->shortname}",
], manager::get_potential_context_restrictions([CONTEXT_COURSECAT, CONTEXT_COURSE]));
// All category context levels.
$this->assertEqualsCanonicalizing([
$defaultcategorycontext->id => "Category: {$defaultcategory->name}",
$categorycontext->id => "Category: {$category->name}",
], manager::get_potential_context_restrictions([CONTEXT_COURSECAT]));
// Filtered category context levels.
$this->assertEquals([
$categorycontext->id => "Category: {$category->name}",
], manager::get_potential_context_restrictions([CONTEXT_COURSECAT], 'My cat'));
$this->assertEmpty(manager::get_potential_context_restrictions([CONTEXT_COURSECAT], 'nothing'));
// All course context levels.
$this->assertEqualsCanonicalizing([
$courseonecontext->id => "Course: {$courseone->shortname}",
$coursetwocontext->id => "Course: {$coursetwo->shortname}",
], manager::get_potential_context_restrictions([CONTEXT_COURSE]));
// Filtered course context levels.
$this->assertEquals([
$courseonecontext->id => "Course: {$courseone->shortname}",
], manager::get_potential_context_restrictions([CONTEXT_COURSE], 'one'));
$this->assertEmpty(manager::get_potential_context_restrictions([CONTEXT_COURSE], 'nothing'));
}
}
+608
View File
@@ -0,0 +1,608 @@
<?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 the model.
*
* @package core_analytics
* @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_analytics;
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/fixtures/test_indicator_max.php');
require_once(__DIR__ . '/fixtures/test_indicator_min.php');
require_once(__DIR__ . '/fixtures/test_indicator_fullname.php');
require_once(__DIR__ . '/fixtures/test_target_shortname.php');
require_once(__DIR__ . '/fixtures/test_static_target_shortname.php');
require_once(__DIR__ . '/fixtures/test_target_course_level_shortname.php');
require_once(__DIR__ . '/fixtures/test_analysis.php');
/**
* Unit tests for the model.
*
* @package core_analytics
* @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class model_test extends \advanced_testcase {
/** @var model Store Model. */
protected $model;
/** @var \stdClass Store model object. */
protected $modelobj;
public function setUp(): void {
$this->setAdminUser();
$target = \core_analytics\manager::get_target('test_target_shortname');
$indicators = array('test_indicator_max', 'test_indicator_min', 'test_indicator_fullname');
foreach ($indicators as $key => $indicator) {
$indicators[$key] = \core_analytics\manager::get_indicator($indicator);
}
$this->model = testable_model::create($target, $indicators);
$this->modelobj = $this->model->get_model_obj();
}
public function test_enable(): void {
$this->resetAfterTest(true);
$this->assertEquals(0, $this->model->get_model_obj()->enabled);
$this->assertEquals(0, $this->model->get_model_obj()->trained);
$this->assertEquals('', $this->model->get_model_obj()->timesplitting);
$this->model->enable('\core\analytics\time_splitting\quarters');
$this->assertEquals(1, $this->model->get_model_obj()->enabled);
$this->assertEquals(0, $this->model->get_model_obj()->trained);
$this->assertEquals('\core\analytics\time_splitting\quarters', $this->model->get_model_obj()->timesplitting);
}
public function test_create(): void {
$this->resetAfterTest(true);
$target = \core_analytics\manager::get_target('\core_course\analytics\target\course_dropout');
$indicators = array(
\core_analytics\manager::get_indicator('\core\analytics\indicator\any_write_action'),
\core_analytics\manager::get_indicator('\core\analytics\indicator\read_actions')
);
$model = \core_analytics\model::create($target, $indicators);
$this->assertInstanceOf('\core_analytics\model', $model);
}
/**
* test_delete
*/
public function test_delete(): void {
global $DB;
$this->resetAfterTest(true);
set_config('enabled_stores', 'logstore_standard', 'tool_log');
$coursepredict1 = $this->getDataGenerator()->create_course(array('visible' => 0));
$coursepredict2 = $this->getDataGenerator()->create_course(array('visible' => 0));
$coursetrain1 = $this->getDataGenerator()->create_course(array('visible' => 1));
$coursetrain2 = $this->getDataGenerator()->create_course(array('visible' => 1));
$this->model->enable('\core\analytics\time_splitting\single_range');
$this->model->train();
$this->model->predict();
// Fake evaluation results record to check that it is actually deleted.
$this->add_fake_log();
$modeloutputdir = $this->model->get_output_dir(array(), true);
$this->assertTrue(is_dir($modeloutputdir));
// Generate a prediction action to confirm that it is deleted when there is an important update.
$predictions = $DB->get_records('analytics_predictions');
$prediction = reset($predictions);
$prediction = new \core_analytics\prediction($prediction, array('whatever' => 'not used'));
$prediction->action_executed(\core_analytics\prediction::ACTION_FIXED, $this->model->get_target());
$this->model->delete();
$this->assertEmpty($DB->count_records('analytics_models', array('id' => $this->modelobj->id)));
$this->assertEmpty($DB->count_records('analytics_models_log', array('modelid' => $this->modelobj->id)));
$this->assertEmpty($DB->count_records('analytics_predictions'));
$this->assertEmpty($DB->count_records('analytics_prediction_actions'));
$this->assertEmpty($DB->count_records('analytics_train_samples'));
$this->assertEmpty($DB->count_records('analytics_predict_samples'));
$this->assertEmpty($DB->count_records('analytics_used_files'));
$this->assertFalse(is_dir($modeloutputdir));
set_config('enabled_stores', '', 'tool_log');
get_log_manager(true);
}
/**
* test_clear
*/
public function test_clear(): void {
global $DB;
$this->resetAfterTest(true);
set_config('enabled_stores', 'logstore_standard', 'tool_log');
$coursepredict1 = $this->getDataGenerator()->create_course(array('visible' => 0));
$coursepredict2 = $this->getDataGenerator()->create_course(array('visible' => 0));
$coursetrain1 = $this->getDataGenerator()->create_course(array('visible' => 1));
$coursetrain2 = $this->getDataGenerator()->create_course(array('visible' => 1));
$this->model->enable('\core\analytics\time_splitting\single_range');
$this->model->train();
$this->model->predict();
// Fake evaluation results record to check that it is actually deleted.
$this->add_fake_log();
// Generate a prediction action to confirm that it is deleted when there is an important update.
$predictions = $DB->get_records('analytics_predictions');
$prediction = reset($predictions);
$prediction = new \core_analytics\prediction($prediction, array('whatever' => 'not used'));
$prediction->action_executed(\core_analytics\prediction::ACTION_FIXED, $this->model->get_target());
$modelversionoutputdir = $this->model->get_output_dir();
$this->assertTrue(is_dir($modelversionoutputdir));
// Update to an empty time splitting method to force model::clear execution.
$this->model->clear();
$this->assertFalse(is_dir($modelversionoutputdir));
// Check that most of the stuff got deleted.
$this->assertEquals(1, $DB->count_records('analytics_models', array('id' => $this->modelobj->id)));
$this->assertEquals(1, $DB->count_records('analytics_models_log', array('modelid' => $this->modelobj->id)));
$this->assertEmpty($DB->count_records('analytics_predictions'));
$this->assertEmpty($DB->count_records('analytics_prediction_actions'));
$this->assertEmpty($DB->count_records('analytics_train_samples'));
$this->assertEmpty($DB->count_records('analytics_predict_samples'));
$this->assertEmpty($DB->count_records('analytics_used_files'));
// Check that the model is marked as not trained after clearing (as it is not a static one).
$this->assertEquals(0, $DB->get_field('analytics_models', 'trained', array('id' => $this->modelobj->id)));
set_config('enabled_stores', '', 'tool_log');
get_log_manager(true);
}
/**
* Test behaviour of {\core_analytics\model::clear()} for static models.
*/
public function test_clear_static(): void {
global $DB;
$this->resetAfterTest();
$statictarget = new \test_static_target_shortname();
$indicators['test_indicator_max'] = \core_analytics\manager::get_indicator('test_indicator_max');
$model = \core_analytics\model::create($statictarget, $indicators, '\core\analytics\time_splitting\quarters');
$modelobj = $model->get_model_obj();
// Static models are always considered trained.
$this->assertEquals(1, $DB->get_field('analytics_models', 'trained', array('id' => $modelobj->id)));
$model->clear();
// Check that the model is still marked as trained even after clearing.
$this->assertEquals(1, $DB->get_field('analytics_models', 'trained', array('id' => $modelobj->id)));
}
public function test_model_manager(): void {
$this->resetAfterTest(true);
$this->assertCount(3, $this->model->get_indicators());
$this->assertInstanceOf('\core_analytics\local\target\binary', $this->model->get_target());
// Using evaluation as the model is not yet enabled.
$this->model->init_analyser(array('evaluation' => true));
$this->assertInstanceOf('\core_analytics\local\analyser\base', $this->model->get_analyser());
$this->model->enable('\core\analytics\time_splitting\quarters');
$this->assertInstanceOf('\core\analytics\analyser\site_courses', $this->model->get_analyser());
}
public function test_output_dir(): void {
$this->resetAfterTest(true);
$dir = make_request_directory();
set_config('modeloutputdir', $dir, 'analytics');
$modeldir = $dir . DIRECTORY_SEPARATOR . $this->modelobj->id . DIRECTORY_SEPARATOR . $this->modelobj->version;
$this->assertEquals($modeldir, $this->model->get_output_dir());
$this->assertEquals($modeldir . DIRECTORY_SEPARATOR . 'testing', $this->model->get_output_dir(array('testing')));
}
public function test_unique_id(): void {
global $DB;
$this->resetAfterTest(true);
$originaluniqueid = $this->model->get_unique_id();
// Same id across instances.
$this->model = new testable_model($this->modelobj);
$this->assertEquals($originaluniqueid, $this->model->get_unique_id());
// We will restore it later.
$originalversion = $this->modelobj->version;
// Generates a different id if timemodified changes.
$this->modelobj->version = $this->modelobj->version + 10;
$DB->update_record('analytics_models', $this->modelobj);
$this->model = new testable_model($this->modelobj);
$this->assertNotEquals($originaluniqueid, $this->model->get_unique_id());
// Restore original timemodified to continue testing.
$this->modelobj->version = $originalversion;
$DB->update_record('analytics_models', $this->modelobj);
// Same when updating through an action that changes the model.
$this->model = new testable_model($this->modelobj);
$this->model->mark_as_trained();
$this->assertEquals($originaluniqueid, $this->model->get_unique_id());
// Wait for the current timestamp to change.
$this->waitForSecond();
$this->model->enable('\core\analytics\time_splitting\deciles');
$this->assertNotEquals($originaluniqueid, $this->model->get_unique_id());
$uniqueid = $this->model->get_unique_id();
// Wait for the current timestamp to change.
$this->waitForSecond();
$this->model->enable('\core\analytics\time_splitting\quarters');
$this->assertNotEquals($originaluniqueid, $this->model->get_unique_id());
$this->assertNotEquals($uniqueid, $this->model->get_unique_id());
}
/**
* test_exists
*
* @return void
*/
public function test_exists(): void {
$this->resetAfterTest(true);
$target = \core_analytics\manager::get_target('\core_course\analytics\target\no_teaching');
$this->assertTrue(\core_analytics\model::exists($target));
foreach (\core_analytics\manager::get_all_models() as $model) {
$model->delete();
}
$this->assertFalse(\core_analytics\model::exists($target));
}
/**
* test_model_timelimit
*
* @return null
*/
public function test_model_timelimit(): void {
global $DB;
$this->resetAfterTest(true);
set_config('modeltimelimit', 2, 'analytics');
$courses = array();
for ($i = 0; $i < 5; $i++) {
$course = $this->getDataGenerator()->create_course();
$analysable = new \core_analytics\course($course);
$courses[$analysable->get_id()] = $course;
}
$target = new \test_target_course_level_shortname();
$analyser = new \core\analytics\analyser\courses(1, $target, [], [], []);
$result = new \core_analytics\local\analysis\result_array(1, false, []);
$analysis = new \test_analysis($analyser, false, $result);
// Each analysable element takes 0.5 secs minimum (test_analysis), so the max (and likely) number of analysable
// elements that will be processed is 2.
$analysis->run();
$params = array('modelid' => 1, 'action' => 'prediction');
$this->assertLessThanOrEqual(2, $DB->count_records('analytics_used_analysables', $params));
$analysis->run();
$this->assertLessThanOrEqual(4, $DB->count_records('analytics_used_analysables', $params));
// Check that analysable elements have been processed following the analyser order
// (course->sortorder here). We can not check this nicely after next get_unlabelled_data round
// because the first analysed element will be analysed again.
$analysedelems = $DB->get_records('analytics_used_analysables', $params, 'timeanalysed ASC');
// Just a default for the first checked element.
$last = (object)['sortorder' => PHP_INT_MAX];
foreach ($analysedelems as $analysed) {
if ($courses[$analysed->analysableid]->sortorder > $last->sortorder) {
$this->fail('Analysable elements have not been analysed sorted by course sortorder.');
}
$last = $courses[$analysed->analysableid];
}
// No time limit now to process the rest.
set_config('modeltimelimit', 1000, 'analytics');
$analysis->run();
$this->assertEquals(5, $DB->count_records('analytics_used_analysables', $params));
// New analysable elements are immediately pulled.
$this->getDataGenerator()->create_course();
$analysis->run();
$this->assertEquals(6, $DB->count_records('analytics_used_analysables', $params));
// Training and prediction data do not get mixed.
$result = new \core_analytics\local\analysis\result_array(1, false, []);
$analysis = new \test_analysis($analyser, false, $result);
$analysis->run();
$params = array('modelid' => 1, 'action' => 'training');
$this->assertLessThanOrEqual(2, $DB->count_records('analytics_used_analysables', $params));
}
/**
* Test model_config::get_class_component.
*/
public function test_model_config_get_class_component(): void {
$this->resetAfterTest(true);
$this->assertEquals('core',
\core_analytics\model_config::get_class_component('\\core\\analytics\\indicator\\read_actions'));
$this->assertEquals('core',
\core_analytics\model_config::get_class_component('core\\analytics\\indicator\\read_actions'));
$this->assertEquals('core',
\core_analytics\model_config::get_class_component('\\core_course\\analytics\\indicator\\completion_enabled'));
$this->assertEquals('mod_forum',
\core_analytics\model_config::get_class_component('\\mod_forum\\analytics\\indicator\\cognitive_depth'));
$this->assertEquals('core', \core_analytics\model_config::get_class_component('\\core_class'));
}
/**
* Test that import_model import models' configurations.
*/
public function test_import_model_config(): void {
$this->resetAfterTest(true);
$this->model->enable('\\core\\analytics\\time_splitting\\quarters');
$zipfilepath = $this->model->export_model('yeah-config.zip');
$this->modelobj = $this->model->get_model_obj();
$importedmodelobj = \core_analytics\model::import_model($zipfilepath)->get_model_obj();
$this->assertSame($this->modelobj->target, $importedmodelobj->target);
$this->assertSame($this->modelobj->indicators, $importedmodelobj->indicators);
$this->assertSame($this->modelobj->timesplitting, $importedmodelobj->timesplitting);
$predictionsprocessor = $this->model->get_predictions_processor();
$this->assertSame('\\' . get_class($predictionsprocessor), $importedmodelobj->predictionsprocessor);
}
/**
* Test can export configuration
*/
public function test_can_export_configuration(): void {
$this->resetAfterTest(true);
// No time splitting method.
$this->assertFalse($this->model->can_export_configuration());
$this->model->enable('\\core\\analytics\\time_splitting\\quarters');
$this->assertTrue($this->model->can_export_configuration());
$this->model->update(true, [], false);
$this->assertFalse($this->model->can_export_configuration());
$statictarget = new \test_static_target_shortname();
$indicators['test_indicator_max'] = \core_analytics\manager::get_indicator('test_indicator_max');
$model = \core_analytics\model::create($statictarget, $indicators, '\\core\\analytics\\time_splitting\\quarters');
$this->assertFalse($model->can_export_configuration());
}
/**
* Test export_config
*/
public function test_export_config(): void {
$this->resetAfterTest(true);
$this->model->enable('\\core\\analytics\\time_splitting\\quarters');
$modelconfig = new \core_analytics\model_config($this->model);
$method = new \ReflectionMethod('\\core_analytics\\model_config', 'export_model_data');
$modeldata = $method->invoke($modelconfig);
$this->assertArrayHasKey('core', $modeldata->dependencies);
$this->assertIsFloat($modeldata->dependencies['core']);
$this->assertNotEmpty($modeldata->target);
$this->assertNotEmpty($modeldata->timesplitting);
$this->assertCount(3, $modeldata->indicators);
$indicators['test_indicator_max'] = \core_analytics\manager::get_indicator('test_indicator_max');
$this->model->update(true, $indicators, false);
$modeldata = $method->invoke($modelconfig);
$this->assertCount(1, $modeldata->indicators);
}
/**
* Test the implementation of {@link \core_analytics\model::inplace_editable_name()}.
*/
public function test_inplace_editable_name(): void {
global $PAGE;
$this->resetAfterTest();
$output = new \core_renderer($PAGE, RENDERER_TARGET_GENERAL);
// Check as a user with permission to edit the name.
$this->setAdminUser();
$ie = $this->model->inplace_editable_name();
$this->assertInstanceOf(\core\output\inplace_editable::class, $ie);
$data = $ie->export_for_template($output);
$this->assertEquals('core_analytics', $data['component']);
$this->assertEquals('modelname', $data['itemtype']);
// Check as a user without permission to edit the name.
$this->setGuestUser();
$ie = $this->model->inplace_editable_name();
$this->assertInstanceOf(\core\output\inplace_editable::class, $ie);
$data = $ie->export_for_template($output);
$this->assertArrayHasKey('displayvalue', $data);
}
/**
* Test how the models present themselves in the UI and that they can be renamed.
*/
public function test_get_name_and_rename(): void {
global $PAGE;
$this->resetAfterTest();
$output = new \core_renderer($PAGE, RENDERER_TARGET_GENERAL);
// By default, the model exported for template uses its target's name in the name inplace editable element.
$this->assertEquals($this->model->get_name(), $this->model->get_target()->get_name());
$data = $this->model->export($output);
$this->assertEquals($data->name['displayvalue'], $this->model->get_target()->get_name());
$this->assertEquals($data->name['value'], '');
// Rename the model.
$this->model->rename('Nějaký pokusný model');
$this->assertEquals($this->model->get_name(), 'Nějaký pokusný model');
$data = $this->model->export($output);
$this->assertEquals($data->name['displayvalue'], 'Nějaký pokusný model');
$this->assertEquals($data->name['value'], 'Nějaký pokusný model');
// Undo the renaming.
$this->model->rename('');
$this->assertEquals($this->model->get_name(), $this->model->get_target()->get_name());
$data = $this->model->export($output);
$this->assertEquals($data->name['displayvalue'], $this->model->get_target()->get_name());
$this->assertEquals($data->name['value'], '');
}
/**
* Tests model::get_potential_timesplittings()
*/
public function test_potential_timesplittings(): void {
$this->resetAfterTest();
$this->assertArrayNotHasKey('\core\analytics\time_splitting\no_splitting', $this->model->get_potential_timesplittings());
$this->assertArrayHasKey('\core\analytics\time_splitting\single_range', $this->model->get_potential_timesplittings());
$this->assertArrayHasKey('\core\analytics\time_splitting\quarters', $this->model->get_potential_timesplittings());
}
/**
* Tests model::get_samples()
*
* @return null
*/
public function test_get_samples(): void {
$this->resetAfterTest();
if (!PHPUNIT_LONGTEST) {
$this->markTestSkipped('PHPUNIT_LONGTEST is not defined');
}
// 10000 should be enough to make oracle and mssql fail, if we want pgsql to fail we need around 70000
// users, that is a few minutes just to create the users.
$nusers = 10000;
$userids = [];
for ($i = 0; $i < $nusers; $i++) {
$user = $this->getDataGenerator()->create_user();
$userids[] = $user->id;
}
$upcomingactivities = null;
foreach (\core_analytics\manager::get_all_models() as $model) {
if (get_class($model->get_target()) === 'core_user\\analytics\\target\\upcoming_activities_due') {
$upcomingactivities = $model;
}
}
list($sampleids, $samplesdata) = $upcomingactivities->get_samples($userids);
$this->assertCount($nusers, $sampleids);
$this->assertCount($nusers, $samplesdata);
$subset = array_slice($userids, 0, 100);
list($sampleids, $samplesdata) = $upcomingactivities->get_samples($subset);
$this->assertCount(100, $sampleids);
$this->assertCount(100, $samplesdata);
$subset = array_slice($userids, 0, 2);
list($sampleids, $samplesdata) = $upcomingactivities->get_samples($subset);
$this->assertCount(2, $sampleids);
$this->assertCount(2, $samplesdata);
$subset = array_slice($userids, 0, 1);
list($sampleids, $samplesdata) = $upcomingactivities->get_samples($subset);
$this->assertCount(1, $sampleids);
$this->assertCount(1, $samplesdata);
// Unexisting, so nothing returned, but still 2 arrays.
list($sampleids, $samplesdata) = $upcomingactivities->get_samples([1231231231231231]);
$this->assertEmpty($sampleids);
$this->assertEmpty($samplesdata);
}
/**
* Generates a model log record.
*/
private function add_fake_log() {
global $DB, $USER;
$log = new \stdClass();
$log->modelid = $this->modelobj->id;
$log->version = $this->modelobj->version;
$log->target = $this->modelobj->target;
$log->indicators = $this->modelobj->indicators;
$log->score = 1;
$log->info = json_encode([]);
$log->dir = 'not important';
$log->timecreated = time();
$log->usermodified = $USER->id;
$DB->insert_record('analytics_models_log', $log);
}
}
/**
* Testable version to change methods' visibility.
*
* @package core_analytics
* @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class testable_model extends \core_analytics\model {
/**
* init_analyser
*
* @param array $options
* @return void
*/
public function init_analyser($options = array()) {
parent::init_analyser($options);
}
}
+265
View File
@@ -0,0 +1,265 @@
<?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/>.
namespace core_analytics;
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/fixtures/test_indicator_max.php');
require_once(__DIR__ . '/fixtures/test_target_shortname.php');
/**
* Unit tests for prediction actions.
*
* @package core_analytics
* @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class prediction_actions_test extends \advanced_testcase {
/** @var model Store Model. */
protected $model;
/** @var \stdClass Store model object. */
protected $modelobj;
/** @var \stdClass Course 1 record. */
protected $course1;
/** @var \stdClass Course 2 record. */
protected $course2;
/** @var \context_course Store Model. */
protected $context;
/** @var \stdClass Teacher 1 user record. */
protected $teacher1;
/** @var \stdClass Teacher 2 user record. */
protected $teacher2;
/** @var \stdClass Teacher 3 user record. */
protected $teacher3;
/**
* Common startup tasks
*/
public function setUp(): void {
global $DB;
$this->setAdminUser();
$target = \core_analytics\manager::get_target('test_target_shortname');
$indicators = array('test_indicator_max');
foreach ($indicators as $key => $indicator) {
$indicators[$key] = \core_analytics\manager::get_indicator($indicator);
}
$this->model = \core_analytics\model::create($target, $indicators);
$this->modelobj = $this->model->get_model_obj();
$this->model->enable('\core\analytics\time_splitting\single_range');
$this->resetAfterTest(true);
$this->course1 = $this->getDataGenerator()->create_course();
$this->course2 = $this->getDataGenerator()->create_course();
$this->context = \context_course::instance($this->course1->id);
$this->teacher1 = $this->getDataGenerator()->create_user();
$this->teacher2 = $this->getDataGenerator()->create_user();
$this->teacher3 = $this->getDataGenerator()->create_user();
$this->getDataGenerator()->enrol_user($this->teacher1->id, $this->course1->id, 'editingteacher');
$this->getDataGenerator()->enrol_user($this->teacher2->id, $this->course1->id, 'editingteacher');
$this->getDataGenerator()->enrol_user($this->teacher3->id, $this->course1->id, 'editingteacher');
// The only relevant fields are modelid, contextid and sampleid. I'm cheating and setting
// contextid as the course context so teachers can access these predictions.
$pred = new \stdClass();
$pred->modelid = $this->model->get_id();
$pred->contextid = $this->context->id;
$pred->sampleid = $this->course1->id;
$pred->rangeindex = 1;
$pred->prediction = 1;
$pred->predictionscore = 1;
$pred->calculations = json_encode(array('test_indicator_max' => 1));
$pred->timecreated = time();
$DB->insert_record('analytics_predictions', $pred);
$pred->sampleid = $this->course2->id;
$DB->insert_record('analytics_predictions', $pred);
}
/**
* test_get_predictions
*/
public function test_action_executed(): void {
global $DB;
$this->assertEquals(0, $DB->count_records('analytics_prediction_actions'));
// Teacher 2 flags a prediction (it doesn't matter which one) as fixed.
$this->setUser($this->teacher2);
list($ignored, $predictions) = $this->model->get_predictions($this->context, true);
$prediction = reset($predictions);
$prediction->action_executed(\core_analytics\prediction::ACTION_FIXED, $this->model->get_target());
$recordset = $this->model->get_prediction_actions($this->context);
$this->assertCount(1, $recordset);
$recordset->close();
$this->assertEquals(1, $DB->count_records('analytics_prediction_actions'));
$action = $DB->get_record('analytics_prediction_actions', array('userid' => $this->teacher2->id));
$this->assertEquals(\core_analytics\prediction::ACTION_FIXED, $action->actionname);
$prediction->action_executed(\core_analytics\prediction::ACTION_INCORRECTLY_FLAGGED, $this->model->get_target());
$recordset = $this->model->get_prediction_actions($this->context);
$this->assertCount(2, $recordset);
$recordset->close();
$this->assertEquals(2, $DB->count_records('analytics_prediction_actions'));
}
/**
* Data provider for test_get_executed_actions.
*
* @return array
*/
public function execute_actions_provider(): array {
return [
'Empty actions with no filter' => [
[],
[],
0
],
'Empty actions with filter' => [
[],
[\core_analytics\prediction::ACTION_FIXED],
0
],
'Multiple actions with no filter' => [
[
\core_analytics\prediction::ACTION_FIXED,
\core_analytics\prediction::ACTION_FIXED,
\core_analytics\prediction::ACTION_INCORRECTLY_FLAGGED
],
[],
3
],
'Multiple actions applying filter' => [
[
\core_analytics\prediction::ACTION_FIXED,
\core_analytics\prediction::ACTION_FIXED,
\core_analytics\prediction::ACTION_INCORRECTLY_FLAGGED
],
[\core_analytics\prediction::ACTION_FIXED],
2
],
'Multiple actions not applying filter' => [
[
\core_analytics\prediction::ACTION_FIXED,
\core_analytics\prediction::ACTION_FIXED,
\core_analytics\prediction::ACTION_INCORRECTLY_FLAGGED
],
[\core_analytics\prediction::ACTION_NOT_APPLICABLE],
0
],
'Multiple actions with multiple filter' => [
[
\core_analytics\prediction::ACTION_FIXED,
\core_analytics\prediction::ACTION_FIXED,
\core_analytics\prediction::ACTION_INCORRECTLY_FLAGGED
],
[\core_analytics\prediction::ACTION_FIXED, \core_analytics\prediction::ACTION_INCORRECTLY_FLAGGED],
3
],
];
}
/**
* Tests for get_executed_actions() function.
*
* @dataProvider execute_actions_provider
* @param array $actionstoexecute An array of actions to execute
* @param array $actionnamefilter Actions to filter
* @param int $returned Number of actions returned
*
* @covers \core_analytics\prediction::get_executed_actions
*/
public function test_get_executed_actions(array $actionstoexecute, array $actionnamefilter, int $returned): void {
$this->setUser($this->teacher2);
list($ignored, $predictions) = $this->model->get_predictions($this->context, true);
$prediction = reset($predictions);
$target = $this->model->get_target();
foreach($actionstoexecute as $action) {
$prediction->action_executed($action, $target);
}
$filteredactions = $prediction->get_executed_actions($actionnamefilter);
$this->assertCount($returned, $filteredactions);
}
/**
* test_get_predictions
*/
public function test_get_predictions(): void {
global $DB;
// Already logged in as admin.
list($ignored, $predictions) = $this->model->get_predictions($this->context, true);
$this->assertCount(2, $predictions);
$this->setUser($this->teacher1);
list($ignored, $predictions) = $this->model->get_predictions($this->context, true);
$this->assertCount(2, $predictions);
$this->setUser($this->teacher2);
list($ignored, $predictions) = $this->model->get_predictions($this->context, false);
$this->assertCount(2, $predictions);
// Teacher 2 flags a prediction (it doesn't matter which one).
$prediction = reset($predictions);
$prediction->action_executed(\core_analytics\prediction::ACTION_FIXED, $this->model->get_target());
$prediction->action_executed(\core_analytics\prediction::ACTION_NOT_APPLICABLE, $this->model->get_target());
$prediction->action_executed(\core_analytics\prediction::ACTION_INCORRECTLY_FLAGGED, $this->model->get_target());
$recordset = $this->model->get_prediction_actions($this->context);
$this->assertCount(3, $recordset);
$recordset->close();
list($ignored, $predictions) = $this->model->get_predictions($this->context, true);
$this->assertCount(1, $predictions);
list($ignored, $predictions) = $this->model->get_predictions($this->context, false);
$this->assertCount(2, $predictions);
// Teacher 1 can still see both predictions.
$this->setUser($this->teacher1);
list($ignored, $predictions) = $this->model->get_predictions($this->context, true);
$this->assertCount(2, $predictions);
list($ignored, $predictions) = $this->model->get_predictions($this->context, false);
$this->assertCount(2, $predictions);
$recordset = $this->model->get_prediction_actions($this->context);
$this->assertCount(3, $recordset);
$recordset->close();
// Trying with a deleted course.
$DB->delete_records('course', ['id' => $this->course2->id]);
$this->setUser($this->teacher3);
list($ignored, $predictions) = $this->model->get_predictions($this->context);
$this->assertCount(1, $predictions);
reset($predictions)->action_executed(\core_analytics\prediction::ACTION_FIXED, $this->model->get_target());
$this->assertEmpty($this->model->get_predictions($this->context));
}
}
+990
View File
@@ -0,0 +1,990 @@
<?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/>.
namespace core_analytics;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once(__DIR__ . '/fixtures/test_indicator_max.php');
require_once(__DIR__ . '/fixtures/test_indicator_min.php');
require_once(__DIR__ . '/fixtures/test_indicator_null.php');
require_once(__DIR__ . '/fixtures/test_indicator_fullname.php');
require_once(__DIR__ . '/fixtures/test_indicator_random.php');
require_once(__DIR__ . '/fixtures/test_indicator_multiclass.php');
require_once(__DIR__ . '/fixtures/test_target_shortname.php');
require_once(__DIR__ . '/fixtures/test_target_shortname_multiclass.php');
require_once(__DIR__ . '/fixtures/test_static_target_shortname.php');
require_once(__DIR__ . '/../../course/lib.php');
/**
* Unit tests for evaluation, training and prediction.
*
* NOTE: in order to execute this test using a separate server for the
* python ML backend you need to define these variables in your config.php file:
*
* define('TEST_MLBACKEND_PYTHON_HOST', '127.0.0.1');
* define('TEST_MLBACKEND_PYTHON_PORT', 5000);
* define('TEST_MLBACKEND_PYTHON_USERNAME', 'default');
* define('TEST_MLBACKEND_PYTHON_PASSWORD', 'sshhhh');
*
* @package core_analytics
* @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class prediction_test extends \advanced_testcase {
/**
* Purge all the mlbackend outputs.
*
* This is done automatically for mlbackends using the web server dataroot but
* other mlbackends may store files elsewhere and these files need to be removed.
*
* @return null
*/
public function tearDown(): void {
$this->setAdminUser();
$models = \core_analytics\manager::get_all_models();
foreach ($models as $model) {
$model->delete();
}
}
/**
* test_static_prediction
*
* @return void
*/
public function test_static_prediction(): void {
global $DB;
$this->resetAfterTest(true);
$this->setAdminuser();
$model = $this->add_perfect_model('test_static_target_shortname');
$model->enable('\core\analytics\time_splitting\no_splitting');
$this->assertEquals(1, $model->is_enabled());
$this->assertEquals(1, $model->is_trained());
// No training for static models.
$results = $model->train();
$trainedsamples = $DB->get_records('analytics_train_samples', array('modelid' => $model->get_id()));
$this->assertEmpty($trainedsamples);
$this->assertEmpty($DB->count_records('analytics_used_files',
array('modelid' => $model->get_id(), 'action' => 'trained')));
// Now we create 2 hidden courses (only hidden courses are getting predictions).
$courseparams = array('shortname' => 'aaaaaa', 'fullname' => 'aaaaaa', 'visible' => 0);
$course1 = $this->getDataGenerator()->create_course($courseparams);
$courseparams = array('shortname' => 'bbbbbb', 'fullname' => 'bbbbbb', 'visible' => 0);
$course2 = $this->getDataGenerator()->create_course($courseparams);
$result = $model->predict();
// Var $course1 predictions should be 1 == 'a', $course2 predictions should be 0 == 'b'.
$correct = array($course1->id => 1, $course2->id => 0);
foreach ($result->predictions as $uniquesampleid => $predictiondata) {
list($sampleid, $rangeindex) = $model->get_time_splitting()->infer_sample_info($uniquesampleid);
// The range index is not important here, both ranges prediction will be the same.
$this->assertEquals($correct[$sampleid], $predictiondata->prediction);
}
// 1 range for each analysable.
$predictedranges = $DB->get_records('analytics_predict_samples', array('modelid' => $model->get_id()));
$this->assertCount(2, $predictedranges);
// 2 predictions for each range.
$this->assertEquals(2, $DB->count_records('analytics_predictions',
array('modelid' => $model->get_id())));
// No new generated records as there are no new courses available.
$model->predict();
$predictedranges = $DB->get_records('analytics_predict_samples', array('modelid' => $model->get_id()));
$this->assertCount(2, $predictedranges);
$this->assertEquals(2, $DB->count_records('analytics_predictions',
array('modelid' => $model->get_id())));
}
/**
* test_model_contexts
*/
public function test_model_contexts(): void {
global $DB;
$this->resetAfterTest(true);
$this->setAdminuser();
$misc = $DB->get_record('course_categories', ['name' => get_string('defaultcategoryname')]);
$miscctx = \context_coursecat::instance($misc->id);
$category = $this->getDataGenerator()->create_category();
$categoryctx = \context_coursecat::instance($category->id);
// One course per category.
$courseparams = array('shortname' => 'aaaaaa', 'fullname' => 'aaaaaa', 'visible' => 0,
'category' => $category->id);
$course1 = $this->getDataGenerator()->create_course($courseparams);
$course1ctx = \context_course::instance($course1->id);
$courseparams = array('shortname' => 'bbbbbb', 'fullname' => 'bbbbbb', 'visible' => 0,
'category' => $misc->id);
$course2 = $this->getDataGenerator()->create_course($courseparams);
$model = $this->add_perfect_model('test_static_target_shortname');
// Just 1 category.
$model->update(true, false, '\core\analytics\time_splitting\no_splitting', false, [$categoryctx->id]);
$this->assertCount(1, $model->predict()->predictions);
// Now with 2 categories.
$model->update(true, false, false, false, [$categoryctx->id, $miscctx->id]);
// The courses in the new category are processed.
$this->assertCount(1, $model->predict()->predictions);
// Clear the predictions generated by the model and predict() again.
$model->clear();
$this->assertCount(2, $model->predict()->predictions);
// Course context restriction.
$model->update(true, false, '\core\analytics\time_splitting\no_splitting', false, [$course1ctx->id]);
// Nothing new as the course was already analysed.
$result = $model->predict();
$this->assertTrue(empty($result->predictions));
$model->clear();
$this->assertCount(1, $model->predict()->predictions);
}
/**
* test_ml_training_and_prediction
*
* @dataProvider provider_ml_training_and_prediction
* @param string $timesplittingid
* @param int $predictedrangeindex
* @param int $nranges
* @param string $predictionsprocessorclass
* @param array $forcedconfig
* @return void
*/
public function test_ml_training_and_prediction($timesplittingid, $predictedrangeindex, $nranges, $predictionsprocessorclass,
$forcedconfig): void {
global $DB;
$this->resetAfterTest(true);
$this->set_forced_config($forcedconfig);
$predictionsprocessor = $this->is_predictions_processor_ready($predictionsprocessorclass);
$this->setAdminuser();
set_config('enabled_stores', 'logstore_standard', 'tool_log');
// Generate training data.
$ncourses = 10;
$this->generate_courses($ncourses);
$model = $this->add_perfect_model();
$model->update(true, false, $timesplittingid, get_class($predictionsprocessor));
// No samples trained yet.
$this->assertEquals(0, $DB->count_records('analytics_train_samples', array('modelid' => $model->get_id())));
$results = $model->train();
$this->assertEquals(1, $model->is_enabled());
$this->assertEquals(1, $model->is_trained());
// 20 courses * the 3 model indicators * the number of time ranges of this time splitting method.
$indicatorcalc = 20 * 3 * $nranges;
$this->assertEquals($indicatorcalc, $DB->count_records('analytics_indicator_calc'));
// 1 training file was created.
$trainedsamples = $DB->get_records('analytics_train_samples', array('modelid' => $model->get_id()));
$this->assertCount(1, $trainedsamples);
$samples = json_decode(reset($trainedsamples)->sampleids, true);
$this->assertCount($ncourses * 2, $samples);
$this->assertEquals(1, $DB->count_records('analytics_used_files',
array('modelid' => $model->get_id(), 'action' => 'trained')));
// Check that analysable files for training are stored under labelled filearea.
$fs = get_file_storage();
$this->assertCount(1, $fs->get_directory_files(\context_system::instance()->id, 'analytics',
\core_analytics\dataset_manager::LABELLED_FILEAREA, $model->get_id(), '/analysable/', true, false));
$this->assertEmpty($fs->get_directory_files(\context_system::instance()->id, 'analytics',
\core_analytics\dataset_manager::UNLABELLED_FILEAREA, $model->get_id(), '/analysable/', true, false));
$params = [
'startdate' => mktime(0, 0, 0, 10, 24, 2015),
'enddate' => mktime(0, 0, 0, 2, 24, 2016),
];
$courseparams = $params + array('shortname' => 'aaaaaa', 'fullname' => 'aaaaaa', 'visible' => 0);
$course1 = $this->getDataGenerator()->create_course($courseparams);
$courseparams = $params + array('shortname' => 'bbbbbb', 'fullname' => 'bbbbbb', 'visible' => 0);
$course2 = $this->getDataGenerator()->create_course($courseparams);
// They will not be skipped for prediction though.
$result = $model->predict();
// Var $course1 predictions should be 1 == 'a', $course2 predictions should be 0 == 'b'.
$correct = array($course1->id => 1, $course2->id => 0);
foreach ($result->predictions as $uniquesampleid => $predictiondata) {
list($sampleid, $rangeindex) = $model->get_time_splitting()->infer_sample_info($uniquesampleid);
// The range index is not important here, both ranges prediction will be the same.
$this->assertEquals($correct[$sampleid], $predictiondata->prediction);
}
// 1 range will be predicted.
$predictedranges = $DB->get_records('analytics_predict_samples', array('modelid' => $model->get_id()));
$this->assertCount(1, $predictedranges);
foreach ($predictedranges as $predictedrange) {
$this->assertEquals($predictedrangeindex, $predictedrange->rangeindex);
$sampleids = json_decode($predictedrange->sampleids, true);
$this->assertCount(2, $sampleids);
$this->assertContainsEquals($course1->id, $sampleids);
$this->assertContainsEquals($course2->id, $sampleids);
}
$this->assertEquals(1, $DB->count_records('analytics_used_files',
array('modelid' => $model->get_id(), 'action' => 'predicted')));
// 2 predictions.
$this->assertEquals(2, $DB->count_records('analytics_predictions',
array('modelid' => $model->get_id())));
// Check that analysable files to get predictions are stored under unlabelled filearea.
$this->assertCount(1, $fs->get_directory_files(\context_system::instance()->id, 'analytics',
\core_analytics\dataset_manager::LABELLED_FILEAREA, $model->get_id(), '/analysable/', true, false));
$this->assertCount(1, $fs->get_directory_files(\context_system::instance()->id, 'analytics',
\core_analytics\dataset_manager::UNLABELLED_FILEAREA, $model->get_id(), '/analysable/', true, false));
// No new generated files nor records as there are no new courses available.
$model->predict();
$predictedranges = $DB->get_records('analytics_predict_samples', array('modelid' => $model->get_id()));
$this->assertCount(1, $predictedranges);
foreach ($predictedranges as $predictedrange) {
$this->assertEquals($predictedrangeindex, $predictedrange->rangeindex);
}
$this->assertEquals(1, $DB->count_records('analytics_used_files',
array('modelid' => $model->get_id(), 'action' => 'predicted')));
$this->assertEquals(2, $DB->count_records('analytics_predictions',
array('modelid' => $model->get_id())));
// New samples that can be used for prediction.
$courseparams = $params + array('shortname' => 'cccccc', 'fullname' => 'cccccc', 'visible' => 0);
$course3 = $this->getDataGenerator()->create_course($courseparams);
$courseparams = $params + array('shortname' => 'dddddd', 'fullname' => 'dddddd', 'visible' => 0);
$course4 = $this->getDataGenerator()->create_course($courseparams);
$result = $model->predict();
$predictedranges = $DB->get_records('analytics_predict_samples', array('modelid' => $model->get_id()));
$this->assertCount(1, $predictedranges);
foreach ($predictedranges as $predictedrange) {
$this->assertEquals($predictedrangeindex, $predictedrange->rangeindex);
$sampleids = json_decode($predictedrange->sampleids, true);
$this->assertCount(4, $sampleids);
$this->assertContainsEquals($course1->id, $sampleids);
$this->assertContainsEquals($course2->id, $sampleids);
$this->assertContainsEquals($course3->id, $sampleids);
$this->assertContainsEquals($course4->id, $sampleids);
}
$this->assertEquals(2, $DB->count_records('analytics_used_files',
array('modelid' => $model->get_id(), 'action' => 'predicted')));
$this->assertEquals(4, $DB->count_records('analytics_predictions',
array('modelid' => $model->get_id())));
$this->assertCount(1, $fs->get_directory_files(\context_system::instance()->id, 'analytics',
\core_analytics\dataset_manager::LABELLED_FILEAREA, $model->get_id(), '/analysable/', true, false));
$this->assertCount(2, $fs->get_directory_files(\context_system::instance()->id, 'analytics',
\core_analytics\dataset_manager::UNLABELLED_FILEAREA, $model->get_id(), '/analysable/', true, false));
// New visible course (for training).
$course5 = $this->getDataGenerator()->create_course(array('shortname' => 'aaa', 'fullname' => 'aa'));
$course6 = $this->getDataGenerator()->create_course();
$result = $model->train();
$this->assertEquals(2, $DB->count_records('analytics_used_files',
array('modelid' => $model->get_id(), 'action' => 'trained')));
$this->assertCount(2, $fs->get_directory_files(\context_system::instance()->id, 'analytics',
\core_analytics\dataset_manager::LABELLED_FILEAREA, $model->get_id(), '/analysable/', true, false));
$this->assertCount(2, $fs->get_directory_files(\context_system::instance()->id, 'analytics',
\core_analytics\dataset_manager::UNLABELLED_FILEAREA, $model->get_id(), '/analysable/', true, false));
// Confirm that the files associated to the model are deleted on clear and on delete. The ML backend deletion
// processes will be triggered by these actions and any exception there would result in a failed test.
$model->clear();
$this->assertEquals(0, $DB->count_records('analytics_used_files',
array('modelid' => $model->get_id(), 'action' => 'trained')));
$this->assertCount(0, $fs->get_directory_files(\context_system::instance()->id, 'analytics',
\core_analytics\dataset_manager::LABELLED_FILEAREA, $model->get_id(), '/analysable/', true, false));
$this->assertCount(0, $fs->get_directory_files(\context_system::instance()->id, 'analytics',
\core_analytics\dataset_manager::UNLABELLED_FILEAREA, $model->get_id(), '/analysable/', true, false));
$model->delete();
set_config('enabled_stores', '', 'tool_log');
get_log_manager(true);
}
/**
* provider_ml_training_and_prediction
*
* @return array
*/
public function provider_ml_training_and_prediction() {
$cases = array(
'no_splitting' => array('\core\analytics\time_splitting\no_splitting', 0, 1),
'quarters' => array('\core\analytics\time_splitting\quarters', 3, 4)
);
// We need to test all system prediction processors.
return $this->add_prediction_processors($cases);
}
/**
* test_ml_export_import
*
* @param string $predictionsprocessorclass The class name
* @param array $forcedconfig
* @dataProvider provider_ml_processors
*/
public function test_ml_export_import($predictionsprocessorclass, $forcedconfig): void {
$this->resetAfterTest(true);
$this->set_forced_config($forcedconfig);
$predictionsprocessor = $this->is_predictions_processor_ready($predictionsprocessorclass);
$this->setAdminuser();
set_config('enabled_stores', 'logstore_standard', 'tool_log');
// Generate training data.
$ncourses = 10;
$this->generate_courses($ncourses);
$model = $this->add_perfect_model();
$model->update(true, false, '\core\analytics\time_splitting\quarters', get_class($predictionsprocessor));
$model->train();
$this->assertTrue($model->trained_locally());
$this->generate_courses(10, ['visible' => 0]);
$originalresults = $model->predict();
$zipfilename = 'model-zip-' . microtime() . '.zip';
$zipfilepath = $model->export_model($zipfilename);
$modelconfig = new \core_analytics\model_config();
list($modelconfig, $mlbackend) = $modelconfig->extract_import_contents($zipfilepath);
$this->assertNotFalse($mlbackend);
$importmodel = \core_analytics\model::import_model($zipfilepath);
$importmodel->enable();
// Now predict using the imported model without prior training.
$importedmodelresults = $importmodel->predict();
foreach ($originalresults->predictions as $sampleid => $prediction) {
$this->assertEquals($importedmodelresults->predictions[$sampleid]->prediction, $prediction->prediction);
}
$this->assertFalse($importmodel->trained_locally());
$zipfilename = 'model-zip-' . microtime() . '.zip';
$zipfilepath = $model->export_model($zipfilename, false);
$modelconfig = new \core_analytics\model_config();
list($modelconfig, $mlbackend) = $modelconfig->extract_import_contents($zipfilepath);
$this->assertFalse($mlbackend);
set_config('enabled_stores', '', 'tool_log');
get_log_manager(true);
}
/**
* provider_ml_processors
*
* @return array
*/
public function provider_ml_processors() {
$cases = [
'case' => [],
];
// We need to test all system prediction processors.
return $this->add_prediction_processors($cases);
}
/**
* Test the system classifiers returns.
*
* This test checks that all mlbackend plugins in the system are able to return proper status codes
* even under weird situations.
*
* @dataProvider provider_ml_classifiers_return
* @param int $success
* @param int $nsamples
* @param int $classes
* @param string $predictionsprocessorclass
* @param array $forcedconfig
* @return void
*/
public function test_ml_classifiers_return($success, $nsamples, $classes, $predictionsprocessorclass, $forcedconfig): void {
$this->resetAfterTest();
$this->set_forced_config($forcedconfig);
$predictionsprocessor = $this->is_predictions_processor_ready($predictionsprocessorclass);
if ($nsamples % count($classes) != 0) {
throw new \coding_exception('The number of samples should be divisible by the number of classes');
}
$samplesperclass = $nsamples / count($classes);
// Metadata (we pass 2 classes even if $classes only provides 1 class samples as we want to test
// what the backend does in this case.
$dataset = "nfeatures,targetclasses,targettype" . PHP_EOL;
$dataset .= "3,\"[0,1]\",\"discrete\"" . PHP_EOL;
// Headers.
$dataset .= "feature1,feature2,feature3,target" . PHP_EOL;
foreach ($classes as $class) {
for ($i = 0; $i < $samplesperclass; $i++) {
$dataset .= "1,0,1,$class" . PHP_EOL;
}
}
$trainingfile = array(
'contextid' => \context_system::instance()->id,
'component' => 'analytics',
'filearea' => 'labelled',
'itemid' => 123,
'filepath' => '/',
'filename' => 'whocares.csv'
);
$fs = get_file_storage();
$dataset = $fs->create_file_from_string($trainingfile, $dataset);
// Training should work correctly if at least 1 sample of each class is included.
$dir = make_request_directory();
$modeluniqueid = 'whatever' . microtime();
$result = $predictionsprocessor->train_classification($modeluniqueid, $dataset, $dir);
switch ($success) {
case 'yes':
$this->assertEquals(\core_analytics\model::OK, $result->status);
break;
case 'no':
$this->assertNotEquals(\core_analytics\model::OK, $result->status);
break;
case 'maybe':
default:
// We just check that an object is returned so we don't have an empty check,
// what we really want to check is that an exception was not thrown.
$this->assertInstanceOf(\stdClass::class, $result);
}
// Purge the directory used in this test (useful in case the mlbackend is storing files
// somewhere out of the default moodledata/models dir.
$predictionsprocessor->delete_output_dir($dir, $modeluniqueid);
}
/**
* test_ml_classifiers_return provider
*
* We can not be very specific here as test_ml_classifiers_return only checks that
* mlbackend plugins behave and expected and control properly backend errors even
* under weird situations.
*
* @return array
*/
public function provider_ml_classifiers_return() {
// Using verbose options as the first argument for readability.
$cases = array(
'1-samples' => array('maybe', 1, [0]),
'2-samples-same-class' => array('maybe', 2, [0]),
'2-samples-different-classes' => array('yes', 2, [0, 1]),
'4-samples-different-classes' => array('yes', 4, [0, 1])
);
// We need to test all system prediction processors.
return $this->add_prediction_processors($cases);
}
/**
* Tests correct multi-classification.
*
* @dataProvider provider_test_multi_classifier
* @param string $timesplittingid
* @param string $predictionsprocessorclass
* @param array|null $forcedconfig
* @throws coding_exception
* @throws moodle_exception
*/
public function test_ml_multi_classifier($timesplittingid, $predictionsprocessorclass, $forcedconfig): void {
global $DB;
$this->resetAfterTest(true);
$this->setAdminuser();
set_config('enabled_stores', 'logstore_standard', 'tool_log');
$this->set_forced_config($forcedconfig);
$predictionsprocessor = \core_analytics\manager::get_predictions_processor($predictionsprocessorclass, false);
if ($predictionsprocessor->is_ready() !== true) {
$this->markTestSkipped('Skipping ' . $predictionsprocessorclass . ' as the predictor is not ready.');
}
// Generate training courses.
$ncourses = 5;
$this->generate_courses_multiclass($ncourses);
$model = $this->add_multiclass_model();
$model->update(true, false, $timesplittingid, get_class($predictionsprocessor));
$results = $model->train();
$params = [
'startdate' => mktime(0, 0, 0, 10, 24, 2015),
'enddate' => mktime(0, 0, 0, 2, 24, 2016),
];
$courseparams = $params + array('shortname' => 'aaaaaa', 'fullname' => 'aaaaaa', 'visible' => 0);
$course1 = $this->getDataGenerator()->create_course($courseparams);
$courseparams = $params + array('shortname' => 'bbbbbb', 'fullname' => 'bbbbbb', 'visible' => 0);
$course2 = $this->getDataGenerator()->create_course($courseparams);
$courseparams = $params + array('shortname' => 'cccccc', 'fullname' => 'cccccc', 'visible' => 0);
$course3 = $this->getDataGenerator()->create_course($courseparams);
// They will not be skipped for prediction though.
$result = $model->predict();
// The $course1 predictions should be 0 == 'a', $course2 should be 1 == 'b' and $course3 should be 2 == 'c'.
$correct = array($course1->id => 0, $course2->id => 1, $course3->id => 2);
foreach ($result->predictions as $uniquesampleid => $predictiondata) {
list($sampleid, $rangeindex) = $model->get_time_splitting()->infer_sample_info($uniquesampleid);
// The range index is not important here, both ranges prediction will be the same.
$this->assertEquals($correct[$sampleid], $predictiondata->prediction);
}
set_config('enabled_stores', '', 'tool_log');
get_log_manager(true);
}
/**
* Provider for the multi_classification test.
*
* @return array
*/
public function provider_test_multi_classifier() {
$cases = array(
'notimesplitting' => array('\core\analytics\time_splitting\no_splitting'),
);
// Add all system prediction processors.
return $this->add_prediction_processors($cases);
}
/**
* Basic test to check that prediction processors work as expected.
*
* @coversNothing
* @dataProvider provider_ml_test_evaluation_configuration
* @param string $modelquality
* @param int $ncourses
* @param array $expected
* @param string $predictionsprocessorclass
* @param array $forcedconfig
* @return void
*/
public function test_ml_evaluation_configuration($modelquality, $ncourses, $expected, $predictionsprocessorclass,
$forcedconfig): void {
$this->resetAfterTest(true);
$this->set_forced_config($forcedconfig);
$predictionsprocessor = $this->is_predictions_processor_ready($predictionsprocessorclass);
$this->setAdminuser();
set_config('enabled_stores', 'logstore_standard', 'tool_log');
$sometimesplittings = '\core\analytics\time_splitting\single_range,' .
'\core\analytics\time_splitting\quarters';
set_config('defaulttimesplittingsevaluation', $sometimesplittings, 'analytics');
if ($modelquality === 'perfect') {
$model = $this->add_perfect_model();
} else if ($modelquality === 'random') {
$model = $this->add_random_model();
} else {
throw new \coding_exception('Only perfect and random accepted as $modelquality values');
}
// Generate training data.
$this->generate_courses($ncourses);
$model->update(false, false, false, get_class($predictionsprocessor));
$results = $model->evaluate();
// We check that the returned status includes at least $expectedcode code.
foreach ($results as $timesplitting => $result) {
$message = 'The returned status code ' . $result->status . ' should include ' . $expected[$timesplitting];
$filtered = $result->status & $expected[$timesplitting];
$this->assertEquals($expected[$timesplitting], $filtered, $message);
$options = ['evaluation' => true, 'reuseprevanalysed' => true];
$result = new \core_analytics\local\analysis\result_file($model->get_id(), true, $options);
$timesplittingobj = \core_analytics\manager::get_time_splitting($timesplitting);
$analysable = new \core_analytics\site();
$cachedanalysis = $result->retrieve_cached_result($timesplittingobj, $analysable);
$this->assertInstanceOf(\stored_file::class, $cachedanalysis);
}
set_config('enabled_stores', '', 'tool_log');
get_log_manager(true);
}
/**
* Tests the evaluation of already trained models.
*
* @coversNothing
* @dataProvider provider_ml_processors
* @param string $predictionsprocessorclass
* @param array $forcedconfig
* @return null
*/
public function test_ml_evaluation_trained_model($predictionsprocessorclass, $forcedconfig): void {
$this->resetAfterTest(true);
$this->set_forced_config($forcedconfig);
$predictionsprocessor = $this->is_predictions_processor_ready($predictionsprocessorclass);
$this->setAdminuser();
set_config('enabled_stores', 'logstore_standard', 'tool_log');
$model = $this->add_perfect_model();
// Generate training data.
$this->generate_courses(50);
$model->update(true, false, '\\core\\analytics\\time_splitting\\quarters', get_class($predictionsprocessor));
$model->train();
$zipfilename = 'model-zip-' . microtime() . '.zip';
$zipfilepath = $model->export_model($zipfilename);
$importmodel = \core_analytics\model::import_model($zipfilepath);
$results = $importmodel->evaluate(['mode' => 'trainedmodel']);
$this->assertEquals(0, $results['\\core\\analytics\\time_splitting\\quarters']->status);
$this->assertEquals(1, $results['\\core\\analytics\\time_splitting\\quarters']->score);
set_config('enabled_stores', '', 'tool_log');
get_log_manager(true);
}
/**
* test_read_indicator_calculations
*
* @return void
*/
public function test_read_indicator_calculations(): void {
global $DB;
$this->resetAfterTest(true);
$starttime = 123;
$endtime = 321;
$sampleorigin = 'whatever';
$indicator = $this->getMockBuilder('test_indicator_max')->onlyMethods(['calculate_sample'])->getMock();
$indicator->expects($this->never())->method('calculate_sample');
$existingcalcs = array(111 => 1, 222 => -1);
$sampleids = array(111 => 111, 222 => 222);
list($values, $unused) = $indicator->calculate($sampleids, $sampleorigin, $starttime, $endtime, $existingcalcs);
}
/**
* test_not_null_samples
*/
public function test_not_null_samples(): void {
$this->resetAfterTest(true);
$timesplitting = \core_analytics\manager::get_time_splitting('\core\analytics\time_splitting\quarters');
$timesplitting->set_analysable(new \core_analytics\site());
$ranges = array(
array('start' => 111, 'end' => 222, 'time' => 222),
array('start' => 222, 'end' => 333, 'time' => 333)
);
$samples = array(123 => 123, 321 => 321);
$target = \core_analytics\manager::get_target('test_target_shortname');
$indicators = array('test_indicator_null', 'test_indicator_min');
foreach ($indicators as $key => $indicator) {
$indicators[$key] = \core_analytics\manager::get_indicator($indicator);
}
$model = \core_analytics\model::create($target, $indicators, '\core\analytics\time_splitting\no_splitting');
$analyser = $model->get_analyser();
$result = new \core_analytics\local\analysis\result_array($model->get_id(), false, $analyser->get_options());
$analysis = new \core_analytics\analysis($analyser, false, $result);
// Samples with at least 1 not null value are returned.
$params = array(
$timesplitting,
$samples,
$ranges
);
$dataset = \phpunit_util::call_internal_method($analysis, 'calculate_indicators', $params,
'\core_analytics\analysis');
$this->assertArrayHasKey('123-0', $dataset);
$this->assertArrayHasKey('123-1', $dataset);
$this->assertArrayHasKey('321-0', $dataset);
$this->assertArrayHasKey('321-1', $dataset);
$indicators = array('test_indicator_null');
foreach ($indicators as $key => $indicator) {
$indicators[$key] = \core_analytics\manager::get_indicator($indicator);
}
$model = \core_analytics\model::create($target, $indicators, '\core\analytics\time_splitting\no_splitting');
$analyser = $model->get_analyser();
$result = new \core_analytics\local\analysis\result_array($model->get_id(), false, $analyser->get_options());
$analysis = new \core_analytics\analysis($analyser, false, $result);
// Samples with only null values are not returned.
$params = array(
$timesplitting,
$samples,
$ranges
);
$dataset = \phpunit_util::call_internal_method($analysis, 'calculate_indicators', $params,
'\core_analytics\analysis');
$this->assertArrayNotHasKey('123-0', $dataset);
$this->assertArrayNotHasKey('123-1', $dataset);
$this->assertArrayNotHasKey('321-0', $dataset);
$this->assertArrayNotHasKey('321-1', $dataset);
}
/**
* provider_ml_test_evaluation_configuration
*
* @return array
*/
public function provider_ml_test_evaluation_configuration() {
$cases = array(
'bad' => array(
'modelquality' => 'random',
'ncourses' => 50,
'expectedresults' => array(
'\core\analytics\time_splitting\single_range' => \core_analytics\model::LOW_SCORE,
'\core\analytics\time_splitting\quarters' => \core_analytics\model::LOW_SCORE,
)
),
'good' => array(
'modelquality' => 'perfect',
'ncourses' => 50,
'expectedresults' => array(
'\core\analytics\time_splitting\single_range' => \core_analytics\model::OK,
'\core\analytics\time_splitting\quarters' => \core_analytics\model::OK,
)
)
);
return $this->add_prediction_processors($cases);
}
/**
* add_random_model
*
* @return \core_analytics\model
*/
protected function add_random_model() {
$target = \core_analytics\manager::get_target('test_target_shortname');
$indicators = array('test_indicator_max', 'test_indicator_min', 'test_indicator_random');
foreach ($indicators as $key => $indicator) {
$indicators[$key] = \core_analytics\manager::get_indicator($indicator);
}
$model = \core_analytics\model::create($target, $indicators);
// To load db defaults as well.
return new \core_analytics\model($model->get_id());
}
/**
* add_perfect_model
*
* @param string $targetclass
* @return \core_analytics\model
*/
protected function add_perfect_model($targetclass = 'test_target_shortname') {
$target = \core_analytics\manager::get_target($targetclass);
$indicators = array('test_indicator_max', 'test_indicator_min', 'test_indicator_fullname');
foreach ($indicators as $key => $indicator) {
$indicators[$key] = \core_analytics\manager::get_indicator($indicator);
}
$model = \core_analytics\model::create($target, $indicators);
// To load db defaults as well.
return new \core_analytics\model($model->get_id());
}
/**
* Generates model for multi-classification
*
* @param string $targetclass
* @return \core_analytics\model
* @throws coding_exception
* @throws moodle_exception
*/
public function add_multiclass_model($targetclass = 'test_target_shortname_multiclass') {
$target = \core_analytics\manager::get_target($targetclass);
$indicators = array('test_indicator_fullname', 'test_indicator_multiclass');
foreach ($indicators as $key => $indicator) {
$indicators[$key] = \core_analytics\manager::get_indicator($indicator);
}
$model = \core_analytics\model::create($target, $indicators);
return new \core_analytics\model($model->get_id());
}
/**
* Generates $ncourses courses
*
* @param int $ncourses The number of courses to be generated.
* @param array $params Course params
* @return null
*/
protected function generate_courses($ncourses, array $params = []) {
$params = $params + [
'startdate' => mktime(0, 0, 0, 10, 24, 2015),
'enddate' => mktime(0, 0, 0, 2, 24, 2016),
];
for ($i = 0; $i < $ncourses; $i++) {
$name = 'a' . random_string(10);
$courseparams = array('shortname' => $name, 'fullname' => $name) + $params;
$this->getDataGenerator()->create_course($courseparams);
}
for ($i = 0; $i < $ncourses; $i++) {
$name = 'b' . random_string(10);
$courseparams = array('shortname' => $name, 'fullname' => $name) + $params;
$this->getDataGenerator()->create_course($courseparams);
}
}
/**
* Generates ncourses for multi-classification
*
* @param int $ncourses The number of courses to be generated.
* @param array $params Course params
* @return null
*/
protected function generate_courses_multiclass($ncourses, array $params = []) {
$params = $params + [
'startdate' => mktime(0, 0, 0, 10, 24, 2015),
'enddate' => mktime(0, 0, 0, 2, 24, 2016),
];
for ($i = 0; $i < $ncourses; $i++) {
$name = 'a' . random_string(10);
$courseparams = array('shortname' => $name, 'fullname' => $name) + $params;
$this->getDataGenerator()->create_course($courseparams);
}
for ($i = 0; $i < $ncourses; $i++) {
$name = 'b' . random_string(10);
$courseparams = array('shortname' => $name, 'fullname' => $name) + $params;
$this->getDataGenerator()->create_course($courseparams);
}
for ($i = 0; $i < $ncourses; $i++) {
$name = 'c' . random_string(10);
$courseparams = array('shortname' => $name, 'fullname' => $name) + $params;
$this->getDataGenerator()->create_course($courseparams);
}
}
/**
* Forces some configuration values.
*
* @param array $forcedconfig
*/
protected function set_forced_config($forcedconfig) {
\core_analytics\manager::reset_prediction_processors();
if (empty($forcedconfig)) {
return;
}
foreach ($forcedconfig as $pluginname => $pluginconfig) {
foreach ($pluginconfig as $name => $value) {
set_config($name, $value, $pluginname);
}
}
}
/**
* Is the provided processor ready using the current configuration in the site?
*
* @param string $predictionsprocessorclass
* @return \core_analytics\predictor
*/
protected function is_predictions_processor_ready(string $predictionsprocessorclass) {
// We repeat the test for all prediction processors.
$predictionsprocessor = \core_analytics\manager::get_predictions_processor($predictionsprocessorclass, false);
$ready = $predictionsprocessor->is_ready();
if ($ready !== true) {
$this->markTestSkipped('Skipping ' . $predictionsprocessorclass . ' as the predictor is not ready: ' . $ready);
}
return $predictionsprocessor;
}
/**
* add_prediction_processors
*
* @param array $cases
* @return array
*/
protected function add_prediction_processors($cases) {
$return = array();
if (defined('TEST_MLBACKEND_PYTHON_HOST') && defined('TEST_MLBACKEND_PYTHON_PORT')
&& defined('TEST_MLBACKEND_PYTHON_USERNAME') && defined('TEST_MLBACKEND_PYTHON_USERNAME')) {
$testpythonserver = true;
}
// We need to test all prediction processors in the system.
$predictionprocessors = \core_analytics\manager::get_all_prediction_processors();
foreach ($predictionprocessors as $classfullname => $predictionsprocessor) {
foreach ($cases as $key => $case) {
if (!$predictionsprocessor instanceof \mlbackend_python\processor || empty($testpythonserver)) {
$extraparams = ['predictionsprocessor' => $classfullname, 'forcedconfig' => null];
$return[$key . '-' . $classfullname] = $case + $extraparams;
} else {
// We want the configuration to be forced during the test as things like importing models create new
// instances of ML backend processors during the process.
$forcedconfig = ['mlbackend_python' => ['useserver' => true, 'host' => TEST_MLBACKEND_PYTHON_HOST,
'port' => TEST_MLBACKEND_PYTHON_PORT, 'secure' => false, 'username' => TEST_MLBACKEND_PYTHON_USERNAME,
'password' => TEST_MLBACKEND_PYTHON_PASSWORD]];
$casekey = $key . '-' . $classfullname . '-server';
$return[$casekey] = $case + ['predictionsprocessor' => $classfullname, 'forcedconfig' => $forcedconfig];
}
}
}
return $return;
}
}
+460
View File
@@ -0,0 +1,460 @@
<?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 privacy.
*
* @package core_analytics
* @copyright 2018 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_analytics\privacy;
use core_analytics\privacy\provider;
use core_privacy\local\request\transform;
use core_privacy\local\request\writer;
use core_privacy\local\request\approved_contextlist;
use core_privacy\local\request\approved_userlist;
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/../fixtures/test_indicator_max.php');
require_once(__DIR__ . '/../fixtures/test_indicator_min.php');
require_once(__DIR__ . '/../fixtures/test_target_site_users.php');
require_once(__DIR__ . '/../fixtures/test_target_course_users.php');
/**
* Unit tests for privacy.
*
* @package core_analytics
* @copyright 2018 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider_test extends \core_privacy\tests\provider_testcase {
/** @var \core_analytics\model Store Model 1. */
protected $model1;
/** @var \core_analytics\model Store Model 2. */
protected $model2;
/** @var \stdClass $modelobj1 Store Model 1 object. */
protected $modelobj1;
/** @var \stdClass $modelobj2 Store Model 2 object. */
protected $modelobj2;
/** @var \stdClass $u1 User 1 record. */
protected $u1;
/** @var \stdClass $u2 User 2 record. */
protected $u2;
/** @var \stdClass $u3 User 3 record. */
protected $u3;
/** @var \stdClass $u4 User 4 record. */
protected $u4;
/** @var \stdClass $u5 User 5 record. */
protected $u5;
/** @var \stdClass $u6 User 6 record. */
protected $u6;
/** @var \stdClass $u7 User 7 record. */
protected $u7;
/** @var \stdClass $u8 User 8 record. */
protected $u8;
/** @var \stdClass $c1 Course 1 record. */
protected $c1;
/** @var \stdClass $c2 Course 2 record. */
protected $c2;
public function setUp(): void {
$this->resetAfterTest(true);
$this->setAdminUser();
$timesplittingid = '\core\analytics\time_splitting\single_range';
$target = \core_analytics\manager::get_target('test_target_site_users');
$indicators = array('test_indicator_max');
foreach ($indicators as $key => $indicator) {
$indicators[$key] = \core_analytics\manager::get_indicator($indicator);
}
$this->model1 = \core_analytics\model::create($target, $indicators, $timesplittingid);
$this->modelobj1 = $this->model1->get_model_obj();
$target = \core_analytics\manager::get_target('test_target_course_users');
$indicators = array('test_indicator_min');
foreach ($indicators as $key => $indicator) {
$indicators[$key] = \core_analytics\manager::get_indicator($indicator);
}
$this->model2 = \core_analytics\model::create($target, $indicators, $timesplittingid);
$this->modelobj2 = $this->model1->get_model_obj();
$this->u1 = $this->getDataGenerator()->create_user(['firstname' => 'a111111111111', 'lastname' => 'a']);
$this->u2 = $this->getDataGenerator()->create_user(['firstname' => 'a222222222222', 'lastname' => 'a']);
$this->u3 = $this->getDataGenerator()->create_user(['firstname' => 'b333333333333', 'lastname' => 'b']);
$this->u4 = $this->getDataGenerator()->create_user(['firstname' => 'b444444444444', 'lastname' => 'b']);
$this->u5 = $this->getdatagenerator()->create_user(['firstname' => 'a555555555555', 'lastname' => 'a']);
$this->u6 = $this->getdatagenerator()->create_user(['firstname' => 'a666666666666', 'lastname' => 'a']);
$this->u7 = $this->getdatagenerator()->create_user(['firstname' => 'b777777777777', 'lastname' => 'b']);
$this->u8 = $this->getDataGenerator()->create_user(['firstname' => 'b888888888888', 'lastname' => 'b']);
$this->c1 = $this->getDataGenerator()->create_course(['visible' => false]);
$this->c2 = $this->getDataGenerator()->create_course();
$this->getDataGenerator()->enrol_user($this->u1->id, $this->c1->id, 'student');
$this->getDataGenerator()->enrol_user($this->u2->id, $this->c1->id, 'student');
$this->getDataGenerator()->enrol_user($this->u3->id, $this->c1->id, 'student');
$this->getDataGenerator()->enrol_user($this->u4->id, $this->c1->id, 'student');
$this->getDataGenerator()->enrol_user($this->u5->id, $this->c1->id, 'student');
$this->getDataGenerator()->enrol_user($this->u6->id, $this->c1->id, 'student');
$this->getDataGenerator()->enrol_user($this->u7->id, $this->c1->id, 'student');
$this->getDataGenerator()->enrol_user($this->u8->id, $this->c1->id, 'student');
$this->getDataGenerator()->enrol_user($this->u1->id, $this->c2->id, 'student');
$this->getDataGenerator()->enrol_user($this->u2->id, $this->c2->id, 'student');
$this->getDataGenerator()->enrol_user($this->u3->id, $this->c2->id, 'student');
$this->getDataGenerator()->enrol_user($this->u4->id, $this->c2->id, 'student');
$this->getDataGenerator()->enrol_user($this->u5->id, $this->c2->id, 'student');
$this->getDataGenerator()->enrol_user($this->u6->id, $this->c2->id, 'student');
$this->getDataGenerator()->enrol_user($this->u7->id, $this->c2->id, 'student');
$this->getDataGenerator()->enrol_user($this->u8->id, $this->c2->id, 'student');
$this->setAdminUser();
$this->model1->enable();
$this->model1->train();
$this->model1->predict();
$this->model2->enable();
$this->model2->train();
$this->model2->predict();
list($total, $predictions) = $this->model2->get_predictions(\context_course::instance($this->c1->id));
$this->setUser($this->u3);
$prediction = reset($predictions);
$prediction->action_executed(\core_analytics\prediction::ACTION_INCORRECTLY_FLAGGED, $this->model2->get_target());
$this->setAdminUser();
}
/**
* Test fetching users within a context.
*/
public function test_get_users_in_context(): void {
global $CFG;
$component = 'core_analytics';
$course1context = \context_course::instance($this->c1->id);
$course2context = \context_course::instance($this->c2->id);
$systemcontext = \context_system::instance();
$expected = [$this->u1->id, $this->u2->id, $this->u3->id, $this->u4->id, $this->u5->id, $this->u6->id,
$this->u7->id, $this->u8->id];
// Check users exist in the relevant contexts.
$userlist = new \core_privacy\local\request\userlist($course1context, $component);
provider::get_users_in_context($userlist);
$actual = $userlist->get_userids();
sort($actual);
$this->assertEquals($expected, $actual);
$userlist = new \core_privacy\local\request\userlist($course2context, $component);
provider::get_users_in_context($userlist);
$actual = $userlist->get_userids();
sort($actual);
$this->assertEquals($expected, $actual);
// System context will also find guest and admin user, add to expected before testing.
$expected = array_merge($expected, [$CFG->siteguest, get_admin()->id]);
sort($expected);
$userlist = new \core_privacy\local\request\userlist($systemcontext, $component);
provider::get_users_in_context($userlist);
$actual = $userlist->get_userids();
sort($actual);
$this->assertEquals($expected, $actual);
}
/**
* Test delete a context.
*
* @return null
*/
public function test_delete_context_data(): void {
global $DB;
// We have 4 predictions for model1 and 8 predictions for model2.
$this->assertEquals(12, $DB->count_records('analytics_predictions'));
$this->assertEquals(26, $DB->count_records('analytics_indicator_calc'));
// We have 1 prediction action.
$this->assertEquals(1, $DB->count_records('analytics_prediction_actions'));
$coursecontext = \context_course::instance($this->c1->id);
// Delete the course that was used for prediction.
provider::delete_data_for_all_users_in_context($coursecontext);
// The course1 predictions are deleted.
$this->assertEquals(8, $DB->count_records('analytics_predictions'));
// Calculations related to that context are deleted.
$this->assertEmpty($DB->count_records('analytics_indicator_calc', ['contextid' => $coursecontext->id]));
// The deleted context prediction actions are deleted as well.
$this->assertEquals(0, $DB->count_records('analytics_prediction_actions'));
}
/**
* Test delete a user.
*
* @return null
*/
public function test_delete_user_data(): void {
global $DB;
$usercontexts = provider::get_contexts_for_userid($this->u3->id);
$contextlist = new \core_privacy\local\request\approved_contextlist($this->u3, 'core_analytics',
$usercontexts->get_contextids());
provider::delete_data_for_user($contextlist);
// The site level prediction for u3 was deleted.
$this->assertEquals(9, $DB->count_records('analytics_predictions'));
$this->assertEquals(0, $DB->count_records('analytics_prediction_actions'));
$usercontexts = provider::get_contexts_for_userid($this->u1->id);
$contextlist = new \core_privacy\local\request\approved_contextlist($this->u1, 'core_analytics',
$usercontexts->get_contextids());
provider::delete_data_for_user($contextlist);
// We have nothing for u1.
$this->assertEquals(9, $DB->count_records('analytics_predictions'));
$usercontexts = provider::get_contexts_for_userid($this->u4->id);
$contextlist = new \core_privacy\local\request\approved_contextlist($this->u4, 'core_analytics',
$usercontexts->get_contextids());
provider::delete_data_for_user($contextlist);
$this->assertEquals(6, $DB->count_records('analytics_predictions'));
}
/**
* Test deleting multiple users in a context.
*/
public function test_delete_data_for_users(): void {
global $DB;
$component = 'core_analytics';
$course1context = \context_course::instance($this->c1->id);
$course2context = \context_course::instance($this->c2->id);
$systemcontext = \context_system::instance();
// Ensure all records exist in expected contexts.
$expectedcontexts = [$course1context->id, $course2context->id, $systemcontext->id];
sort($expectedcontexts);
$actualcontexts = [
$this->u1->id => provider::get_contexts_for_userid($this->u1->id)->get_contextids(),
$this->u2->id => provider::get_contexts_for_userid($this->u2->id)->get_contextids(),
$this->u3->id => provider::get_contexts_for_userid($this->u3->id)->get_contextids(),
$this->u4->id => provider::get_contexts_for_userid($this->u4->id)->get_contextids(),
$this->u5->id => provider::get_contexts_for_userid($this->u5->id)->get_contextids(),
$this->u6->id => provider::get_contexts_for_userid($this->u6->id)->get_contextids(),
$this->u7->id => provider::get_contexts_for_userid($this->u7->id)->get_contextids(),
$this->u8->id => provider::get_contexts_for_userid($this->u8->id)->get_contextids(),
];
foreach ($actualcontexts as $userid => $unused) {
sort($actualcontexts[$userid]);
$this->assertEquals($expectedcontexts, $actualcontexts[$userid]);
}
// Test initial record counts are as expected.
$this->assertEquals(12, $DB->count_records('analytics_predictions'));
$this->assertEquals(1, $DB->count_records('analytics_prediction_actions'));
$this->assertEquals(26, $DB->count_records('analytics_indicator_calc'));
// Delete u1 and u3 from system context.
$approveduserids = [$this->u1->id, $this->u3->id];
$approvedlist = new approved_userlist($systemcontext, $component, $approveduserids);
provider::delete_data_for_users($approvedlist);
// Ensure u1 and u3 system context data deleted only.
$expectedcontexts = [
$this->u1->id => [$course1context->id, $course2context->id],
$this->u2->id => [$systemcontext->id, $course1context->id, $course2context->id],
$this->u3->id => [$course1context->id, $course2context->id],
$this->u4->id => [$systemcontext->id, $course1context->id, $course2context->id],
$this->u5->id => [$systemcontext->id, $course1context->id, $course2context->id],
$this->u6->id => [$systemcontext->id, $course1context->id, $course2context->id],
$this->u7->id => [$systemcontext->id, $course1context->id, $course2context->id],
$this->u8->id => [$systemcontext->id, $course1context->id, $course2context->id],
];
$actualcontexts = [
$this->u1->id => provider::get_contexts_for_userid($this->u1->id)->get_contextids(),
$this->u2->id => provider::get_contexts_for_userid($this->u2->id)->get_contextids(),
$this->u3->id => provider::get_contexts_for_userid($this->u3->id)->get_contextids(),
$this->u4->id => provider::get_contexts_for_userid($this->u4->id)->get_contextids(),
$this->u5->id => provider::get_contexts_for_userid($this->u5->id)->get_contextids(),
$this->u6->id => provider::get_contexts_for_userid($this->u6->id)->get_contextids(),
$this->u7->id => provider::get_contexts_for_userid($this->u7->id)->get_contextids(),
$this->u8->id => provider::get_contexts_for_userid($this->u8->id)->get_contextids(),
];
foreach ($actualcontexts as $userid => $unused) {
sort($expectedcontexts[$userid]);
sort($actualcontexts[$userid]);
$this->assertEquals($expectedcontexts[$userid], $actualcontexts[$userid]);
}
// Test expected number of records have been deleted.
$this->assertEquals(11, $DB->count_records('analytics_predictions'));
$this->assertEquals(1, $DB->count_records('analytics_prediction_actions'));
$this->assertEquals(24, $DB->count_records('analytics_indicator_calc'));
// Delete for all 8 users in course 2 context.
$approveduserids = [$this->u1->id, $this->u2->id, $this->u3->id, $this->u4->id, $this->u5->id, $this->u6->id,
$this->u7->id, $this->u8->id];
$approvedlist = new approved_userlist($course2context, $component, $approveduserids);
provider::delete_data_for_users($approvedlist);
// Ensure all course 2 context data deleted for all 4 users.
$expectedcontexts = [
$this->u1->id => [$course1context->id],
$this->u2->id => [$systemcontext->id, $course1context->id],
$this->u3->id => [$course1context->id],
$this->u4->id => [$systemcontext->id, $course1context->id],
$this->u5->id => [$systemcontext->id, $course1context->id],
$this->u6->id => [$systemcontext->id, $course1context->id],
$this->u7->id => [$systemcontext->id, $course1context->id],
$this->u8->id => [$systemcontext->id, $course1context->id],
];
$actualcontexts = [
$this->u1->id => provider::get_contexts_for_userid($this->u1->id)->get_contextids(),
$this->u2->id => provider::get_contexts_for_userid($this->u2->id)->get_contextids(),
$this->u3->id => provider::get_contexts_for_userid($this->u3->id)->get_contextids(),
$this->u4->id => provider::get_contexts_for_userid($this->u4->id)->get_contextids(),
$this->u5->id => provider::get_contexts_for_userid($this->u5->id)->get_contextids(),
$this->u6->id => provider::get_contexts_for_userid($this->u6->id)->get_contextids(),
$this->u7->id => provider::get_contexts_for_userid($this->u7->id)->get_contextids(),
$this->u8->id => provider::get_contexts_for_userid($this->u8->id)->get_contextids(),
];
foreach ($actualcontexts as $userid => $unused) {
sort($actualcontexts[$userid]);
sort($expectedcontexts[$userid]);
$this->assertEquals($expectedcontexts[$userid], $actualcontexts[$userid]);
}
// Test expected number of records have been deleted.
$this->assertEquals(7, $DB->count_records('analytics_predictions'));
$this->assertEquals(1, $DB->count_records('analytics_prediction_actions'));
$this->assertEquals(16, $DB->count_records('analytics_indicator_calc'));
$approveduserids = [$this->u3->id];
$approvedlist = new approved_userlist($course1context, $component, $approveduserids);
provider::delete_data_for_users($approvedlist);
// Ensure all course 1 context data deleted for u3.
$expectedcontexts = [
$this->u1->id => [$course1context->id],
$this->u2->id => [$systemcontext->id, $course1context->id],
$this->u3->id => [],
$this->u4->id => [$systemcontext->id, $course1context->id],
$this->u5->id => [$systemcontext->id, $course1context->id],
$this->u6->id => [$systemcontext->id, $course1context->id],
$this->u7->id => [$systemcontext->id, $course1context->id],
$this->u8->id => [$systemcontext->id, $course1context->id],
];
$actualcontexts = [
$this->u1->id => provider::get_contexts_for_userid($this->u1->id)->get_contextids(),
$this->u2->id => provider::get_contexts_for_userid($this->u2->id)->get_contextids(),
$this->u3->id => provider::get_contexts_for_userid($this->u3->id)->get_contextids(),
$this->u4->id => provider::get_contexts_for_userid($this->u4->id)->get_contextids(),
$this->u5->id => provider::get_contexts_for_userid($this->u5->id)->get_contextids(),
$this->u6->id => provider::get_contexts_for_userid($this->u6->id)->get_contextids(),
$this->u7->id => provider::get_contexts_for_userid($this->u7->id)->get_contextids(),
$this->u8->id => provider::get_contexts_for_userid($this->u8->id)->get_contextids(),
];
foreach ($actualcontexts as $userid => $unused) {
sort($actualcontexts[$userid]);
sort($expectedcontexts[$userid]);
$this->assertEquals($expectedcontexts[$userid], $actualcontexts[$userid]);
}
// Test expected number of records have been deleted.
$this->assertEquals(6, $DB->count_records('analytics_predictions'));
$this->assertEquals(0, $DB->count_records('analytics_prediction_actions'));
$this->assertEquals(15, $DB->count_records('analytics_indicator_calc'));
}
/**
* Test export user data.
*
* @return null
*/
public function test_export_data(): void {
global $DB;
$system = \context_system::instance();
list($total, $predictions) = $this->model1->get_predictions($system);
foreach ($predictions as $key => $prediction) {
if ($prediction->get_prediction_data()->sampleid !== $this->u3->id) {
$otheruserprediction = $prediction;
break;
}
}
$this->setUser($this->u3);
$otheruserprediction->action_executed(\core_analytics\prediction::ACTION_INCORRECTLY_FLAGGED, $this->model1->get_target());
$this->setAdminUser();
$this->export_context_data_for_user($this->u3->id, $system, 'core_analytics');
$writer = \core_privacy\local\request\writer::with_context($system);
$this->assertTrue($writer->has_any_data());
$u3prediction = $DB->get_record('analytics_predictions', ['contextid' => $system->id, 'sampleid' => $this->u3->id]);
$data = $writer->get_data([get_string('analytics', 'analytics'),
get_string('privacy:metadata:analytics:predictions', 'analytics'), $u3prediction->id]);
$this->assertEquals(get_string('adminhelplogs'), $data->target);
$this->assertEquals(get_string('coresystem'), $data->context);
$this->assertEquals('firstname first char is not A', $data->prediction);
$u3calculation = $DB->get_record('analytics_indicator_calc', ['contextid' => $system->id, 'sampleid' => $this->u3->id]);
$data = $writer->get_data([get_string('analytics', 'analytics'),
get_string('privacy:metadata:analytics:indicatorcalc', 'analytics'), $u3calculation->id]);
$this->assertEquals('Allow stealth activities', $data->indicator);
$this->assertEquals(get_string('coresystem'), $data->context);
$this->assertEquals(get_string('yes'), $data->calculation);
$sql = "SELECT apa.id FROM {analytics_prediction_actions} apa
JOIN {analytics_predictions} ap ON ap.id = apa.predictionid
WHERE ap.contextid = :contextid AND apa.userid = :userid AND ap.modelid = :modelid";
$params = ['contextid' => $system->id, 'userid' => $this->u3->id, 'modelid' => $this->model1->get_id()];
$u3action = $DB->get_record_sql($sql, $params);
$data = $writer->get_data([get_string('analytics', 'analytics'),
get_string('privacy:metadata:analytics:predictionactions', 'analytics'), $u3action->id]);
$this->assertEquals(get_string('adminhelplogs'), $data->target);
$this->assertEquals(get_string('coresystem'), $data->context);
$this->assertEquals(\core_analytics\prediction::ACTION_INCORRECTLY_FLAGGED, $data->action);
}
}
+158
View File
@@ -0,0 +1,158 @@
<?php
// This file is part of Moodle - https://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/>.
namespace core_analytics;
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/fixtures/test_indicator_fullname.php');
require_once(__DIR__ . '/fixtures/test_target_shortname.php');
/**
* Unit tests for the analytics stats.
*
* @package core_analytics
* @category test
* @copyright 2019 David Mudrák <david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class stats_test extends \advanced_testcase {
/**
* Set up the test environment.
*/
public function setUp(): void {
$this->setAdminUser();
}
/**
* Test the {@link \core_analytics\stats::enabled_models()} implementation.
*/
public function test_enabled_models(): void {
$this->resetAfterTest(true);
// By default, sites have {@link \core_course\analytics\target\no_teaching} and
// {@link \core_user\analytics\target\upcoming_activities_due} enabled.
$this->assertEquals(4, \core_analytics\stats::enabled_models());
$model = \core_analytics\model::create(
\core_analytics\manager::get_target('\core_course\analytics\target\course_dropout'),
[
\core_analytics\manager::get_indicator('\core\analytics\indicator\any_write_action'),
]
);
// Purely adding a new model does not make it included in the stats.
$this->assertEquals(4, \core_analytics\stats::enabled_models());
// New models must be enabled to have them counted.
$model->enable('\core\analytics\time_splitting\quarters');
$this->assertEquals(5, \core_analytics\stats::enabled_models());
}
/**
* Test the {@link \core_analytics\stats::predictions()} implementation.
*/
public function test_predictions(): void {
$this->resetAfterTest(true);
$model = \core_analytics\model::create(
\core_analytics\manager::get_target('test_target_shortname'),
[
\core_analytics\manager::get_indicator('test_indicator_fullname'),
]
);
$model->enable('\core\analytics\time_splitting\no_splitting');
// Train the model.
$this->getDataGenerator()->create_course(['shortname' => 'a', 'fullname' => 'a', 'visible' => 1]);
$this->getDataGenerator()->create_course(['shortname' => 'b', 'fullname' => 'b', 'visible' => 1]);
$model->train();
// No predictions yet.
$this->assertEquals(0, \core_analytics\stats::predictions());
// Get one new prediction.
$this->getDataGenerator()->create_course(['shortname' => 'aa', 'fullname' => 'aa', 'visible' => 0]);
$result = $model->predict();
$this->assertEquals(1, count($result->predictions));
$this->assertEquals(1, \core_analytics\stats::predictions());
// Nothing changes if there is no new prediction.
$result = $model->predict();
$this->assertFalse(isset($result->predictions));
$this->assertEquals(1, \core_analytics\stats::predictions());
// Get two more predictions, we have three in total now.
$this->getDataGenerator()->create_course(['shortname' => 'bb', 'fullname' => 'bb', 'visible' => 0]);
$this->getDataGenerator()->create_course(['shortname' => 'cc', 'fullname' => 'cc', 'visible' => 0]);
$result = $model->predict();
$this->assertEquals(2, count($result->predictions));
$this->assertEquals(3, \core_analytics\stats::predictions());
}
/**
* Test the {@link \core_analytics\stats::actions()} and {@link \core_analytics\stats::actions_not_useful()} implementation.
*/
public function test_actions(): void {
global $DB;
$this->resetAfterTest(true);
$model = \core_analytics\model::create(
\core_analytics\manager::get_target('test_target_shortname'),
[
\core_analytics\manager::get_indicator('test_indicator_fullname'),
]
);
$model->enable('\core\analytics\time_splitting\no_splitting');
// Train the model.
$this->getDataGenerator()->create_course(['shortname' => 'a', 'fullname' => 'a', 'visible' => 1]);
$this->getDataGenerator()->create_course(['shortname' => 'b', 'fullname' => 'b', 'visible' => 1]);
$model->train();
// Generate two predictions.
$this->getDataGenerator()->create_course(['shortname' => 'aa', 'fullname' => 'aa', 'visible' => 0]);
$this->getDataGenerator()->create_course(['shortname' => 'bb', 'fullname' => 'bb', 'visible' => 0]);
$model->predict();
list($p1, $p2) = array_values($DB->get_records('analytics_predictions'));
$p1 = new \core_analytics\prediction($p1, []);
$p2 = new \core_analytics\prediction($p2, []);
// No actions executed at the start.
$this->assertEquals(0, \core_analytics\stats::actions());
$this->assertEquals(0, \core_analytics\stats::actions_not_useful());
// The user has acknowledged the first prediction.
$p1->action_executed(\core_analytics\prediction::ACTION_FIXED, $model->get_target());
$this->assertEquals(1, \core_analytics\stats::actions());
$this->assertEquals(0, \core_analytics\stats::actions_not_useful());
// The user has marked the other prediction as not useful.
$p2->action_executed(\core_analytics\prediction::ACTION_INCORRECTLY_FLAGGED, $model->get_target());
$this->assertEquals(2, \core_analytics\stats::actions());
$this->assertEquals(1, \core_analytics\stats::actions_not_useful());
}
}
+385
View File
@@ -0,0 +1,385 @@
<?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/>.
namespace core_analytics;
use test_timesplitting_seconds;
use test_timesplitting_upcoming_seconds;
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/fixtures/test_timesplitting_seconds.php');
require_once(__DIR__ . '/fixtures/test_timesplitting_upcoming_seconds.php');
require_once(__DIR__ . '/../../lib/enrollib.php');
/**
* Unit tests for core time splitting methods.
*
* @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 time_splittings_test extends \advanced_testcase {
/** @var \stdClass course record. */
protected $course;
/** @var course Moodle course analysable. */
protected $analysable;
/**
* setUp
*
* @return void
*/
public function setUp(): void {
$this->resetAfterTest(true);
// Generate training data.
$params = array(
'startdate' => mktime(8, 15, 32, 10, 24, 2015),
'enddate' => mktime(12, 12, 31, 10, 24, 2016),
);
$this->course = $this->getDataGenerator()->create_course($params);
$this->analysable = new \core_analytics\course($this->course);
}
/**
* test_ranges
*
* @return void
*/
public function test_valid_ranges(): void {
// All core time splitting methods.
$timesplittings = array(
'\core\analytics\time_splitting\deciles',
'\core\analytics\time_splitting\deciles_accum',
'\core\analytics\time_splitting\no_splitting',
'\core\analytics\time_splitting\quarters',
'\core\analytics\time_splitting\quarters_accum',
'\core\analytics\time_splitting\single_range',
'\core\analytics\time_splitting\upcoming_week',
);
// Check that defined ranges are valid (tested through validate_ranges).
foreach ($timesplittings as $timesplitting) {
$instance = new $timesplitting();
$instance->set_analysable($this->analysable);
}
}
/**
* test_range_dates
*
* @return void
*/
public function test_range_dates(): void {
$nov2015 = mktime(0, 0, 0, 11, 24, 2015);
$aug2016 = mktime(0, 0, 0, 8, 29, 2016);
// Equal parts.
$quarters = new \core\analytics\time_splitting\quarters();
$quarters->set_analysable($this->analysable);
$ranges = $quarters->get_all_ranges();
$this->assertCount(4, $ranges);
$this->assertCount(4, $quarters->get_training_ranges());
$this->assertCount(4, $quarters->get_distinct_ranges());
$this->assertGreaterThan($ranges[0]['start'], $ranges[1]['start']);
$this->assertGreaterThan($ranges[0]['end'], $ranges[1]['start']);
$this->assertGreaterThan($ranges[0]['end'], $ranges[1]['end']);
$this->assertGreaterThan($ranges[1]['start'], $ranges[2]['start']);
$this->assertGreaterThan($ranges[1]['end'], $ranges[2]['start']);
$this->assertGreaterThan($ranges[1]['end'], $ranges[2]['end']);
$this->assertGreaterThan($ranges[2]['start'], $ranges[3]['start']);
$this->assertGreaterThan($ranges[2]['end'], $ranges[3]['end']);
$this->assertGreaterThan($ranges[2]['end'], $ranges[3]['start']);
// First range.
$this->assertLessThan($nov2015, $ranges[0]['start']);
$this->assertGreaterThan($nov2015, $ranges[0]['end']);
// Last range.
$this->assertLessThan($aug2016, $ranges[3]['start']);
$this->assertGreaterThan($aug2016, $ranges[3]['end']);
// Accumulative.
$accum = new \core\analytics\time_splitting\quarters_accum();
$accum->set_analysable($this->analysable);
$ranges = $accum->get_all_ranges();
$this->assertCount(4, $ranges);
$this->assertCount(4, $accum->get_training_ranges());
$this->assertCount(4, $accum->get_distinct_ranges());
$this->assertEquals($ranges[0]['start'], $ranges[1]['start']);
$this->assertEquals($ranges[1]['start'], $ranges[2]['start']);
$this->assertEquals($ranges[2]['start'], $ranges[3]['start']);
$this->assertGreaterThan($ranges[0]['end'], $ranges[1]['end']);
$this->assertGreaterThan($ranges[1]['end'], $ranges[2]['end']);
$this->assertGreaterThan($ranges[2]['end'], $ranges[3]['end']);
// Present in all ranges.
$this->assertLessThan($nov2015, $ranges[0]['start']);
$this->assertGreaterThan($nov2015, $ranges[0]['end']);
$this->assertGreaterThan($nov2015, $ranges[1]['end']);
$this->assertGreaterThan($nov2015, $ranges[2]['end']);
$this->assertGreaterThan($nov2015, $ranges[3]['end']);
// Only in the last range.
$this->assertLessThan($aug2016, $ranges[0]['end']);
$this->assertLessThan($aug2016, $ranges[1]['end']);
$this->assertLessThan($aug2016, $ranges[2]['end']);
$this->assertLessThan($aug2016, $ranges[3]['start']);
$this->assertGreaterThan($aug2016, $ranges[3]['end']);
}
/**
* test_ready_predict
*
* @return void
*/
public function test_ready_predict(): void {
$quarters = new \core\analytics\time_splitting\quarters();
$nosplitting = new \core\analytics\time_splitting\no_splitting();
$singlerange = new \core\analytics\time_splitting\single_range();
$range = array(
'start' => time() - 100,
'end' => time() - 20,
);
$range['time'] = $range['end'];
$this->assertTrue($quarters->ready_to_predict($range));
$this->assertTrue($nosplitting->ready_to_predict($range));
// Single range time is 0.
$range['time'] = 0;
$this->assertTrue($singlerange->ready_to_predict($range));
$range = array(
'start' => time() + 20,
'end' => time() + 100,
);
$range['time'] = $range['end'];
$this->assertFalse($quarters->ready_to_predict($range));
$this->assertTrue($nosplitting->ready_to_predict($range));
// Single range time is 0.
$range['time'] = 0;
$this->assertTrue($singlerange->ready_to_predict($range));
}
/**
* test_periodic
*
* @return void
*/
public function test_periodic(): void {
// Using a finished course.
$pastweek = new \core\analytics\time_splitting\past_week();
$pastweek->set_analysable($this->analysable);
$this->assertCount(1, $pastweek->get_distinct_ranges());
$ranges = $pastweek->get_all_ranges();
$this->assertEquals(52, count($ranges));
$this->assertEquals($this->course->startdate, $ranges[0]['start']);
$this->assertNotEquals($this->course->startdate, $ranges[0]['time']);
// The analysable is finished so all ranges are available for training.
$this->assertCount(count($ranges), $pastweek->get_training_ranges());
$ranges = $pastweek->get_most_recent_prediction_range();
$range = reset($ranges);
$this->assertEquals(51, key($ranges));
// We now use an ongoing course not yet ready to generate predictions.
$threedaysago = new \DateTime('-3 days');
$params = array(
'startdate' => $threedaysago->getTimestamp(),
);
$ongoingcourse = $this->getDataGenerator()->create_course($params);
$ongoinganalysable = new \core_analytics\course($ongoingcourse);
$pastweek = new \core\analytics\time_splitting\past_week();
$pastweek->set_analysable($ongoinganalysable);
$ranges = $pastweek->get_all_ranges();
$this->assertEquals(0, count($ranges));
$this->assertCount(0, $pastweek->get_training_ranges());
// We now use a ready-to-predict ongoing course.
$onemonthago = new \DateTime('-30 days');
$params = array(
'startdate' => $onemonthago->getTimestamp(),
);
$ongoingcourse = $this->getDataGenerator()->create_course($params);
$ongoinganalysable = new \core_analytics\course($ongoingcourse);
$pastweek = new \core\analytics\time_splitting\past_week();
$pastweek->set_analysable($ongoinganalysable);
$this->assertCount(1, $pastweek->get_distinct_ranges());
$ranges = $pastweek->get_all_ranges();
$this->assertEquals(4, count($ranges));
$this->assertCount(4, $pastweek->get_training_ranges());
$ranges = $pastweek->get_most_recent_prediction_range();
$range = reset($ranges);
$this->assertEquals(3, key($ranges));
$this->assertEqualsWithDelta(time(), $range['time'], 1);
// 1 second delta for the start just in case a second passes between the set_analysable call
// and this checking below.
$time = new \DateTime();
$time->sub($pastweek->periodicity());
$this->assertEqualsWithDelta($time->getTimestamp(), $range['start'], 1.0);
$this->assertEqualsWithDelta(time(), $range['end'], 1);
$starttime = time();
$upcomingweek = new \core\analytics\time_splitting\upcoming_week();
$upcomingweek->set_analysable($ongoinganalysable);
$this->assertCount(1, $upcomingweek->get_distinct_ranges());
$ranges = $upcomingweek->get_all_ranges();
$this->assertEquals(1, count($ranges));
$range = reset($ranges);
$this->assertEqualsWithDelta(time(), $range['time'], 1);
$this->assertEqualsWithDelta(time(), $range['start'], 1);
$this->assertGreaterThan(time(), $range['end']);
$this->assertCount(0, $upcomingweek->get_training_ranges());
$ranges = $upcomingweek->get_most_recent_prediction_range();
$range = reset($ranges);
$this->assertEquals(0, key($ranges));
$this->assertEqualsWithDelta(time(), $range['time'], 1);
$this->assertEqualsWithDelta(time(), $range['start'], 1);
$this->assertGreaterThanOrEqual($starttime, $range['time']);
$this->assertGreaterThanOrEqual($starttime, $range['start']);
$this->assertGreaterThan(time(), $range['end']);
$this->assertNotEmpty($upcomingweek->get_range_by_index(0));
$this->assertFalse($upcomingweek->get_range_by_index(1));
// We now check how new ranges get added as time passes.
$fewsecsago = new \DateTime('-5 seconds');
$params = array(
'startdate' => $fewsecsago->getTimestamp(),
'enddate' => (new \DateTimeImmutable('+1 year'))->getTimestamp(),
);
$course = $this->getDataGenerator()->create_course($params);
$analysable = new \core_analytics\course($course);
$seconds = new test_timesplitting_seconds();
$seconds->set_analysable($analysable);
// Store the ranges we just obtained.
$ranges = $seconds->get_all_ranges();
$nranges = count($ranges);
$ntrainingranges = count($seconds->get_training_ranges());
$mostrecentrange = $seconds->get_most_recent_prediction_range();
$mostrecentrange = reset($mostrecentrange);
// We wait for the next range to be added.
sleep(1);
// We set the analysable again so the time ranges are recalculated.
$seconds->set_analysable($analysable);
$newranges = $seconds->get_all_ranges();
$nnewranges = count($newranges);
$nnewtrainingranges = $seconds->get_training_ranges();
$newmostrecentrange = $seconds->get_most_recent_prediction_range();
$newmostrecentrange = reset($newmostrecentrange);
$this->assertGreaterThan($nranges, $nnewranges);
$this->assertGreaterThan($ntrainingranges, $nnewtrainingranges);
$this->assertGreaterThan($mostrecentrange['time'], $newmostrecentrange['time']);
// All the ranges but the last one should return the same values.
array_pop($ranges);
array_pop($newranges);
foreach ($ranges as $key => $range) {
$this->assertEquals($newranges[$key]['start'], $range['start']);
$this->assertEquals($newranges[$key]['end'], $range['end']);
$this->assertEquals($newranges[$key]['time'], $range['time']);
}
// Fake model id, we can use any int, we will need to reference it later.
$modelid = 1505347200;
$upcomingseconds = new test_timesplitting_upcoming_seconds();
$upcomingseconds->set_modelid($modelid);
$upcomingseconds->set_analysable($analysable);
// Store the ranges we just obtained.
$ranges = $upcomingseconds->get_all_ranges();
$nranges = count($ranges);
$ntrainingranges = count($upcomingseconds->get_training_ranges());
$mostrecentrange = $upcomingseconds->get_most_recent_prediction_range();
$mostrecentrange = reset($mostrecentrange);
// Mimic the modelfirstanalyses caching in \core_analytics\analysis.
$this->mock_cache_first_analysis_caching($modelid, $analysable->get_id(), end($ranges));
// We wait for the next range to be added.
sleep(1);
// We set the analysable again so the time ranges are recalculated.
$upcomingseconds->set_analysable($analysable);
$newranges = $upcomingseconds->get_all_ranges();
$nnewranges = count($newranges);
$nnewtrainingranges = $upcomingseconds->get_training_ranges();
$newmostrecentrange = $upcomingseconds->get_most_recent_prediction_range();
$newmostrecentrange = reset($newmostrecentrange);
$this->assertGreaterThan($nranges, $nnewranges);
$this->assertGreaterThan($ntrainingranges, $nnewtrainingranges);
$this->assertGreaterThan($mostrecentrange['time'], $newmostrecentrange['time']);
// All the ranges but the last one should return the same values.
array_pop($ranges);
array_pop($newranges);
foreach ($ranges as $key => $range) {
$this->assertEquals($newranges[$key]['start'], $range['start']);
$this->assertEquals($newranges[$key]['end'], $range['end']);
$this->assertEquals($newranges[$key]['time'], $range['time']);
}
}
/**
* Mocks core_analytics\analysis caching of the first time analysables were analysed.
*
* @param int $modelid
* @param int $analysableid
* @param array $range
* @return null
*/
private function mock_cache_first_analysis_caching($modelid, $analysableid, $range) {
$cache = \cache::make('core', 'modelfirstanalyses');
$cache->set($modelid . '_' . $analysableid, $range['time']);
}
}