MDL-57791 analytics: Replace sql queries for API calls
authorDavid Monllao <davidm@moodle.com>
Wed, 14 Jun 2017 10:32:27 +0000 (12:32 +0200)
committerDavid Monllao <davidm@moodle.com>
Mon, 24 Jul 2017 06:36:49 +0000 (08:36 +0200)
admin/tool/models/classes/analytics/target/course_dropout.php
admin/tool/models/classes/output/model_logs.php
admin/tool/models/classes/task/predict_models.php
admin/tool/models/classes/task/train_models.php
admin/tool/models/model.php
analytics/classes/local/analyser/by_course.php
analytics/classes/local/analyser/site_courses.php
analytics/classes/manager.php
analytics/classes/model.php
analytics/tests/fixtures/test_static_target_shortname.php [new file with mode: 0644]
analytics/tests/prediction_test.php

index 8d43c80..8e0e7ec 100644 (file)
@@ -162,10 +162,8 @@ class course_dropout extends \core_analytics\local\target\binary {
      * @return void
      */
     protected function calculate_sample($sampleid, \core_analytics\analysable $course, $starttime = false, $endtime = false) {
-        global $DB;
 
-        $sql = "SELECT ue.* FROM {user_enrolments} ue JOIN {user} u ON u.id = ue.userid WHERE ue.id = :ueid";
-        $userenrol = $DB->get_record_sql($sql, array('ueid' => $sampleid));
+        $userenrol = $this->retrieve('user_enrolments', $sampleid);
 
         // We use completion as a success metric only when it is enabled.
         $completion = new \completion_info($course->get_course_data());
index cc2cf54..059727f 100644 (file)
@@ -37,22 +37,22 @@ require_once($CFG->libdir . '/tablelib.php');
 class model_logs extends \table_sql {
 
     /**
-     * @var int
+     * @var \core_analytics\model
      */
-    protected $modelid = null;
+    protected $model = null;
 
     /**
      * Sets up the table_log parameters.
      *
      * @param string $uniqueid unique id of form.
-     * @param int $modelid model id
+     * @param \core_analytics\model $model
      */
-    public function __construct($uniqueid, $modelid) {
+    public function __construct($uniqueid, $model) {
         global $PAGE;
 
         parent::__construct($uniqueid);
 
-        $this->modelid = $modelid;
+        $this->model = $model;
 
         $this->set_attribute('class', 'modellog generaltable generalbox');
         $this->set_attribute('aria-live', 'polite');
@@ -182,11 +182,9 @@ class model_logs extends \table_sql {
     public function query_db($pagesize, $useinitialsbar = true) {
         global $DB;
 
-        $total = $DB->count_records('analytics_models_log', array('modelid' => $this->modelid));
+        $total = count($this->model->get_logs());
         $this->pagesize($pagesize, $total);
-
-        $this->rawdata = $DB->get_records('analytics_models_log', array('modelid' => $this->modelid), 'timecreated DESC', '*',
-            $this->get_page_start(), $this->get_page_size());
+        $this->rawdata = $this->model->get_logs($this->get_page_start(), $this->get_page_size());
 
         // Set initial bars.
         if ($useinitialsbar) {
index 5b047ba..d6eb4eb 100644 (file)
@@ -41,15 +41,13 @@ class predict_models extends \core\task\scheduled_task {
     public function execute() {
         global $DB, $OUTPUT, $PAGE;
 
-        $models = $DB->get_records_select('analytics_models', 'enabled = 1 AND trained = 1 AND timesplitting IS NOT NULL');
+        $models = \core_analytics\manager::get_all_models(true, true);
         if (!$models) {
             mtrace(get_string('errornoenabledandtrainedmodels', 'tool_models'));
             return;
         }
 
-        foreach ($models as $modelobj) {
-            $model = new \core_analytics\model($modelobj);
-
+        foreach ($models as $model) {
             $result = $model->predict();
             if ($result) {
                 echo $OUTPUT->heading(get_string('modelresults', 'tool_models', $model->get_target()->get_name()));
index 3eede64..e8fd027 100644 (file)
@@ -41,19 +41,24 @@ class train_models extends \core\task\scheduled_task {
     public function execute() {
         global $DB, $OUTPUT, $PAGE;
 
-        $models = $DB->get_records_select('analytics_models', 'enabled = 1 AND timesplitting IS NOT NULL');
+        $models = \core_analytics\manager::get_all_models(true);
         if (!$models) {
             mtrace(get_string('errornoenabledmodels', 'tool_models'));
             return;
         }
-        foreach ($models as $modelobj) {
-            $model = new \core_analytics\model($modelobj);
+
+        foreach ($models as $model) {
 
             if ($model->is_static()) {
                 // Skip models based on assumptions.
                 continue;
             }
 
+            if (!$model->get_time_splitting()) {
+                // Can not train if there is no time splitting method selected.
+                continue;
+            }
+
             $result = $model->train();
             if ($result) {
                 echo $OUTPUT->heading(get_string('modelresults', 'tool_models', $model->get_target()->get_name()));
index 7b0782d..8d15d31 100644 (file)
@@ -144,7 +144,7 @@ switch ($action) {
         }
 
         $renderer = $PAGE->get_renderer('tool_models');
-        $modellogstable = new \tool_models\output\model_logs('model-' . $model->get_id(), $model->get_id());
+        $modellogstable = new \tool_models\output\model_logs('model-' . $model->get_id(), $model);
         echo $renderer->render_table($modellogstable);
         break;
 }
index c531d80..51c2ffa 100644 (file)
@@ -34,18 +34,19 @@ defined('MOODLE_INTERNAL') || die();
 abstract class by_course extends base {
 
     public function get_courses() {
-        global $DB;
 
         // Default to all system courses.
         if (!empty($this->options['filter'])) {
-            $it = $this->options['filter'];
+            $courses = $this->options['filter'];
         } else {
             // Iterate through all potentially valid courses.
-            $it = $DB->get_recordset_select('course', 'id != :frontpage', array('frontpage' => SITEID), 'sortorder ASC');
+            $courses = get_courses();
         }
+        unset($courses[SITEID]);
 
         $analysables = array();
-        foreach ($it as $course) {
+        foreach ($courses as $course) {
+            // Skip the frontpage course.
             $analysable = \core_analytics\course::instance($course);
             $analysables[$analysable->get_id()] = $analysable;
         }
@@ -64,7 +65,7 @@ abstract class by_course extends base {
         $filesbytimesplitting = array();
 
         // This class and all children will iterate through a list of courses (\core_analytics\course).
-        $analysables = $this->get_courses();
+        $analysables = $this->get_courses('all', 'c.sortorder ASC');
         foreach ($analysables as $analysableid => $analysable) {
 
             $files = $this->process_analysable($analysable, $includetarget);
index b130f51..a9e781f 100644 (file)
@@ -63,7 +63,8 @@ class site_courses extends sitewide {
         // Getting courses from DB instead of from the site as these samples
         // will be stored in memory and we just want the id.
         $select = 'id != 1';
-        $courses = $DB->get_records_select('course', $select, null, '', '*');
+        $courses = get_courses('all', 'c.sortorder ASC');
+        unset($courses[SITEID]);
 
         $courseids = array_keys($courses);
         $sampleids = array_combine($courseids, $courseids);
index 00771d7..0143783 100644 (file)
@@ -50,6 +50,14 @@ class manager {
      */
     protected static $alltimesplittings = null;
 
+    /**
+     * Returns all system models that match the provided filters.
+     *
+     * @param bool $enabled
+     * @param bool $trained
+     * @param \context $predictioncontext
+     * @return \core_analytics\model[]
+     */
     public static function get_all_models($enabled = false, $trained = false, $predictioncontext = false) {
         global $DB;
 
index 2763197..80a8d65 100644 (file)
@@ -1018,6 +1018,19 @@ class model {
         return $data;
     }
 
+    /**
+     * Returns the model logs data.
+     *
+     * @param int $limitfrom
+     * @param int $limitnum
+     * @return \stdClass[]
+     */
+    public function get_logs($limitfrom = 0, $limitnum = 0) {
+        global $DB;
+        return $DB->get_records('analytics_models_log', array('modelid' => $this->get_id()), 'timecreated DESC', '*',
+            $limitfrom, $limitnum);
+    }
+
     /**
      * flag_file_as_used
      *
diff --git a/analytics/tests/fixtures/test_static_target_shortname.php b/analytics/tests/fixtures/test_static_target_shortname.php
new file mode 100644 (file)
index 0000000..0f36d51
--- /dev/null
@@ -0,0 +1,59 @@
+<?php
+
+class test_static_target_shortname extends \core_analytics\local\target\binary {
+
+    protected $predictions = array();
+
+    public function get_analyser_class() {
+        return '\core_analytics\local\analyser\site_courses';
+    }
+
+    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
+     */
+    protected function ignored_predicted_classes() {
+        return array();
+    }
+
+    public function is_valid_analysable(\core_analytics\analysable $analysable, $fortraining = true) {
+        // This is testing, let's make things easy.
+        return true;
+    }
+
+    protected function calculate_sample($sampleid, \core_analytics\analysable $analysable, $starttime = false, $endtime = false) {
+        global $DB;
+
+        $sample = $DB->get_record('course', array('id' => $sampleid));
+
+        if ($sample->visible == 0) {
+            // We skip not-visible courses as a way to emulate the training data / prediction data difference.
+            // In normal circumstances targets will return null when they receive a sample that can not be
+            // processed, that same sample may be used for prediction.
+            // We can not do this in is_valid_analysable because the analysable there is the site not the course.
+            return null;
+        }
+
+        $firstchar = substr($sample->shortname, 0, 1);
+        if ($firstchar === 'a') {
+            return 1;
+        } else {
+            return 0;
+        }
+    }
+}
index 7d1845d..ad4a641 100644 (file)
@@ -48,12 +48,12 @@ class core_analytics_prediction_testcase extends advanced_testcase {
     public function test_ml_training_and_prediction($timesplittingid, $npredictedranges, $predictionsprocessorclass) {
         global $DB;
 
+        $this->resetAfterTest(true);
+        $this->setAdminuser();
         set_config('enabled_stores', 'logstore_standard', 'tool_log');
 
         $ncourses = 10;
 
-        $this->resetAfterTest(true);
-
         // Generate training data.
         $params = array(
             'startdate' => mktime(0, 0, 0, 10, 24, 2015),
@@ -156,7 +156,7 @@ class core_analytics_prediction_testcase extends advanced_testcase {
      */
     public function test_ml_evaluation($modelquality, $ncourses, $expected, $predictionsprocessorclass) {
         $this->resetAfterTest(true);
-
+        $this->setAdminuser();
         set_config('enabled_stores', 'logstore_standard', 'tool_log');
 
         $sometimesplittings = '\core_analytics\local\time_splitting\weekly,' .