diff --git a/analytics/classes/local/analyser/student_enrolments.php b/analytics/classes/local/analyser/student_enrolments.php index dbf26e6aa81..acd56b901b0 100644 --- a/analytics/classes/local/analyser/student_enrolments.php +++ b/analytics/classes/local/analyser/student_enrolments.php @@ -153,11 +153,6 @@ class student_enrolments extends by_course { // Some course enrolments. list($enrolsql, $params) = $DB->get_in_or_equal($sampleids, SQL_PARAMS_NAMED); - $sql = "SELECT ue.id AS enrolmentid, u.* FROM {user_enrolments} ue - JOIN {user} u on ue.userid = u.id - WHERE ue.id $enrolsql"; - $enrolments = $DB->get_recordset_sql($sql, $params); - $samplesdata = array(); foreach ($enrolments as $userenrolmentid => $user) { diff --git a/analytics/tests/analysers_test.php b/analytics/tests/analysers_test.php new file mode 100644 index 00000000000..0ab3e1ba2f0 --- /dev/null +++ b/analytics/tests/analysers_test.php @@ -0,0 +1,176 @@ +. + +/** + * Unit tests for core analysers. + * + * @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__ . '/fixtures/test_target_shortname.php'); +require_once(__DIR__ . '/../../lib/enrollib.php'); + +/** + * Unit tests for core analysers. + * + * @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 analytics_analysers_testcase extends advanced_testcase { + + /** + * test_courses_analyser + * + * @return void + */ + public function test_courses_analyser() { + $this->resetAfterTest(true); + + $course = $this->getDataGenerator()->create_course(); + $coursecontext = \context_course::instance($course->id); + + $target = new test_target_shortname(); + $analyser = new \core_analytics\local\analyser\courses(1, $target, [], [], []); + $analysable = new \core_analytics\course($course); + + $this->assertInstanceOf('\core_analytics\course', $analyser->get_sample_analysable($course->id)); + + $this->assertInstanceOf('\context_course', $analyser->sample_access_context($course->id)); + + // Just 1 sample per course. + $class = new ReflectionClass('\core_analytics\local\analyser\courses'); + $method = $class->getMethod('get_all_samples'); + $method->setAccessible(true); + list($sampleids, $samplesdata) = $method->invoke($analyser, $analysable); + $this->assertCount(1, $sampleids); + $sampleid = reset($sampleids); + $this->assertEquals($course->id, $sampleid); + $this->assertEquals($course->fullname, $samplesdata[$sampleid]['course']->fullname); + $this->assertEquals($coursecontext, $samplesdata[$sampleid]['context']); + + // To compare it later. + $prevsampledata = $samplesdata[$sampleid]; + list($sampleids, $samplesdata) = $analyser->get_samples(array($sampleid)); + $this->assertEquals($prevsampledata['context'], $samplesdata[$sampleid]['context']); + $this->assertEquals($prevsampledata['course']->shortname, $samplesdata[$sampleid]['course']->shortname); + } + + /** + * test_site_courses_analyser + * + * @return void + */ + public function test_site_courses_analyser() { + $this->resetAfterTest(true); + + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $course3 = $this->getDataGenerator()->create_course(); + $course1context = \context_course::instance($course1->id); + + $target = new test_target_shortname(); + $analyser = new \core_analytics\local\analyser\site_courses(1, $target, [], [], []); + $analysable = new \core_analytics\site(); + + $this->assertInstanceOf('\core_analytics\site', $analyser->get_sample_analysable($course1->id)); + $this->assertInstanceOf('\core_analytics\site', $analyser->get_sample_analysable($course2->id)); + + $this->assertInstanceOf('\context_system', $analyser->sample_access_context($course1->id)); + $this->assertInstanceOf('\context_system', $analyser->sample_access_context($course3->id)); + + $class = new ReflectionClass('\core_analytics\local\analyser\site_courses'); + $method = $class->getMethod('get_all_samples'); + $method->setAccessible(true); + list($sampleids, $samplesdata) = $method->invoke($analyser, $analysable); + $this->assertCount(3, $sampleids); + + // Use course1 it does not really matter. + $this->assertArrayHasKey($course1->id, $sampleids); + $sampleid = $course1->id; + $this->assertEquals($course1->id, $sampleid); + $this->assertEquals($course1->fullname, $samplesdata[$sampleid]['course']->fullname); + $this->assertEquals($course1context, $samplesdata[$sampleid]['context']); + + // To compare it later. + $prevsampledata = $samplesdata[$sampleid]; + list($sampleids, $samplesdata) = $analyser->get_samples(array($sampleid)); + $this->assertEquals($prevsampledata['context'], $samplesdata[$sampleid]['context']); + $this->assertEquals($prevsampledata['course']->shortname, $samplesdata[$sampleid]['course']->shortname); + } + + /** + * test_site_courses_analyser + * + * @return void + */ + public function test_student_enrolments_analyser() { + global $DB; + + $this->resetAfterTest(true); + + $course = $this->getDataGenerator()->create_course(); + $coursecontext = \context_course::instance($course->id); + + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $user3 = $this->getDataGenerator()->create_user(); + + // Checking that suspended users are also included. + $this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student'); + $this->getDataGenerator()->enrol_user($user2->id, $course->id, 'student', 'manual', 0, 0, ENROL_USER_SUSPENDED); + $this->getDataGenerator()->enrol_user($user3->id, $course->id, 'editingteacher'); + $enrol = $DB->get_record('enrol', array('courseid' => $course->id, 'enrol' => 'manual')); + $ue1 = $DB->get_record('user_enrolments', array('userid' => $user1->id, 'enrolid' => $enrol->id)); + $ue2 = $DB->get_record('user_enrolments', array('userid' => $user2->id, 'enrolid' => $enrol->id)); + + $target = new test_target_shortname(); + $analyser = new \core_analytics\local\analyser\student_enrolments(1, $target, [], [], []); + $analysable = new \core_analytics\course($course); + + $this->assertInstanceOf('\core_analytics\course', $analyser->get_sample_analysable($ue1->id)); + $this->assertInstanceOf('\context_course', $analyser->sample_access_context($ue1->id)); + + $class = new ReflectionClass('\core_analytics\local\analyser\student_enrolments'); + $method = $class->getMethod('get_all_samples'); + $method->setAccessible(true); + list($sampleids, $samplesdata) = $method->invoke($analyser, $analysable); + // Only students. + $this->assertCount(2, $sampleids); + + $this->assertArrayHasKey($ue1->id, $sampleids); + $this->assertArrayHasKey($ue2->id, $sampleids); + + // Shouldn't matter which one we select. + $sampleid = $ue1->id; + $this->assertEquals($ue1, $samplesdata[$sampleid]['user_enrolments']); + $this->assertEquals($course->fullname, $samplesdata[$sampleid]['course']->fullname); + $this->assertEquals($coursecontext, $samplesdata[$sampleid]['context']); + $this->assertEquals($user1->firstname, $samplesdata[$sampleid]['user']->firstname); + + // To compare it later. + $prevsampledata = $samplesdata[$sampleid]; + list($sampleids, $samplesdata) = $analyser->get_samples(array($sampleid)); + $this->assertEquals($prevsampledata['user_enrolments'], $samplesdata[$sampleid]['user_enrolments']); + $this->assertEquals($prevsampledata['context'], $samplesdata[$sampleid]['context']); + $this->assertEquals($prevsampledata['course']->shortname, $samplesdata[$sampleid]['course']->shortname); + $this->assertEquals($prevsampledata['user']->firstname, $samplesdata[$sampleid]['user']->firstname); + } +} diff --git a/analytics/tests/fixtures/test_static_target_shortname.php b/analytics/tests/fixtures/test_static_target_shortname.php index fe2e65cb75a..518c74471e9 100644 --- a/analytics/tests/fixtures/test_static_target_shortname.php +++ b/analytics/tests/fixtures/test_static_target_shortname.php @@ -22,113 +22,37 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +require_once(__DIR__ . '/test_target_shortname.php'); + defined('MOODLE_INTERNAL') || die(); /** * 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 \core_analytics\local\target\binary { +class test_static_target_shortname extends test_target_shortname { /** - * predictions + * based_on_assumptions * - * @var array + * @return bool */ - protected $predictions = array(); + public static function based_on_assumptions() { + return true; + } /** - * get_analyser_class + * Different analyser just to test a different one. * * @return string */ public function get_analyser_class() { - return '\core_analytics\local\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 null - */ - protected function min_prediction_score() { - return null; - } - - /** - * We don't want to discard results. - * @return array - */ - protected 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; - } - - /** - * 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; - } + return '\core_analytics\local\analyser\courses'; } } diff --git a/analytics/tests/prediction_test.php b/analytics/tests/prediction_test.php index 6a41a0883a4..12d3567af5a 100644 --- a/analytics/tests/prediction_test.php +++ b/analytics/tests/prediction_test.php @@ -29,6 +29,7 @@ require_once(__DIR__ . '/fixtures/test_indicator_min.php'); require_once(__DIR__ . '/fixtures/test_indicator_fullname.php'); require_once(__DIR__ . '/fixtures/test_indicator_random.php'); require_once(__DIR__ . '/fixtures/test_target_shortname.php'); +require_once(__DIR__ . '/fixtures/test_static_target_shortname.php'); /** * Unit tests for evaluation, training and prediction. @@ -39,6 +40,65 @@ require_once(__DIR__ . '/fixtures/test_target_shortname.php'); */ class core_analytics_prediction_testcase extends advanced_testcase { + /** + * test_static_prediction + * + * @return void + */ + public function test_static_prediction() { + global $DB; + + $this->resetAfterTest(true); + $this->setAdminuser(); + + $model = $this->add_perfect_model('test_static_target_shortname'); + $model->enable('\\core_analytics\\local\\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_ranges', array('modelid' => $model->get_id())); + $this->assertCount(2, $predictedranges); + $this->assertEquals(1, $DB->count_records('analytics_used_files', + array('modelid' => $model->get_id(), 'action' => 'predicted'))); + // 2 predictions for each range. + $this->assertEquals(2, $DB->count_records('analytics_predictions', + array('modelid' => $model->get_id()))); + + // No new generated files nor records as there are no new courses available. + $model->predict(); + $predictedranges = $DB->get_records('analytics_predict_ranges', array('modelid' => $model->get_id())); + $this->assertCount(2, $predictedranges); + $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()))); + } + /** * test_ml_training_and_prediction * @@ -88,30 +148,22 @@ class core_analytics_prediction_testcase extends advanced_testcase { $this->assertEquals(0, $DB->count_records('analytics_train_samples', array('modelid' => $model->get_id()))); $results = $model->train(); - $this->assertEquals(1, $model->get_model_obj()->enabled); - $this->assertEquals(1, $model->get_model_obj()->trained); + $this->assertEquals(1, $model->is_enabled()); + $this->assertEquals(1, $model->is_trained()); // 1 training file was created. $trainedsamples = $DB->get_records('analytics_train_samples', array('modelid' => $model->get_id())); - $this->assertEquals(1, count($trainedsamples)); + $this->assertCount(1, $trainedsamples); $samples = json_decode(reset($trainedsamples)->sampleids, true); - $this->assertEquals($ncourses * 2, count($samples)); + $this->assertCount($ncourses * 2, $samples); $this->assertEquals(1, $DB->count_records('analytics_used_files', array('modelid' => $model->get_id(), 'action' => 'trained'))); - // Now we create 2 hidden courses (they should not be used for training by the target). $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); - // No more files should be created as the 2 new courses should be skipped by the target (not ready for training). - $results = $model->train(); - $trainedsamples = $DB->get_records('analytics_train_samples', array('modelid' => $model->get_id())); - $this->assertEquals(1, count($trainedsamples)); - $this->assertEquals(1, $DB->count_records('analytics_used_files', - array('modelid' => $model->get_id(), 'action' => 'trained'))); - // They will not be skipped for prediction though. $result = $model->predict(); @@ -125,8 +177,8 @@ class core_analytics_prediction_testcase extends advanced_testcase { } // 2 ranges will be predicted. - $trainedsamples = $DB->get_records('analytics_predict_ranges', array('modelid' => $model->get_id())); - $this->assertEquals($npredictedranges, count($trainedsamples)); + $predictedranges = $DB->get_records('analytics_predict_ranges', array('modelid' => $model->get_id())); + $this->assertCount($npredictedranges, $predictedranges); $this->assertEquals(1, $DB->count_records('analytics_used_files', array('modelid' => $model->get_id(), 'action' => 'predicted'))); // 2 predictions for each range. @@ -135,8 +187,8 @@ class core_analytics_prediction_testcase extends advanced_testcase { // No new generated files nor records as there are no new courses available. $model->predict(); - $trainedsamples = $DB->get_records('analytics_predict_ranges', array('modelid' => $model->get_id())); - $this->assertEquals($npredictedranges, count($trainedsamples)); + $predictedranges = $DB->get_records('analytics_predict_ranges', array('modelid' => $model->get_id())); + $this->assertCount($npredictedranges, $predictedranges); $this->assertEquals(1, $DB->count_records('analytics_used_files', array('modelid' => $model->get_id(), 'action' => 'predicted'))); $this->assertEquals(2 * $npredictedranges, $DB->count_records('analytics_predictions', @@ -287,11 +339,12 @@ class core_analytics_prediction_testcase extends advanced_testcase { /** * add_perfect_model * + * @param string $targetclass * @return \core_analytics\model */ - protected function add_perfect_model() { + protected function add_perfect_model($targetclass = 'test_target_shortname') { - $target = \core_analytics\manager::get_target('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); diff --git a/lib/enrollib.php b/lib/enrollib.php index 01c31401588..7157bc5ebbf 100644 --- a/lib/enrollib.php +++ b/lib/enrollib.php @@ -1476,7 +1476,7 @@ function enrol_get_course_users($courseid = false, $onlyactive = false, $usersfi } if ($uefilter) { - list($uesql, $ueparams) = $DB->get_in_or_equal($usersfilter, SQL_PARAMS_NAMED); + list($uesql, $ueparams) = $DB->get_in_or_equal($uefilter, SQL_PARAMS_NAMED); $conditions[] = "ue.id $uesql"; $params = $params + $ueparams; }