MDL-64636 analytics: base class for course completion targets
authorVíctor Déniz Falcón <victor@moodle.com>
Tue, 26 Mar 2019 22:52:58 +0000 (22:52 +0000)
committerVictor Deniz Falcon <victor@moodle.com>
Fri, 29 Mar 2019 10:27:50 +0000 (10:27 +0000)
Added new parent class for targets that use course as analysable and
student enrolments as samples. course_dropout target was modified to
extend that parent class.

lib/classes/analytics/target/course_dropout.php
lib/classes/analytics/target/course_enrolments.php [new file with mode: 0644]

index b44ec4d..9a7cfd8 100644 (file)
@@ -38,7 +38,7 @@ require_once($CFG->dirroot . '/completion/completion_completion.php');
  * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class course_dropout extends \core_analytics\local\target\binary {
+class course_dropout extends \core\analytics\target\course_enrolments {
 
     /**
      * Returns the name.
@@ -51,39 +51,6 @@ class course_dropout extends \core_analytics\local\target\binary {
         return new \lang_string('target:coursedropout');
     }
 
-    /**
-     * prediction_actions
-     *
-     * @param \core_analytics\prediction $prediction
-     * @param bool $includedetailsaction
-     * @return \core_analytics\prediction_action[]
-     */
-    public function prediction_actions(\core_analytics\prediction $prediction, $includedetailsaction = false) {
-        global $USER;
-
-        $actions = array();
-
-        $sampledata = $prediction->get_sample_data();
-        $studentid = $sampledata['user']->id;
-
-        $attrs = array('target' => '_blank');
-
-        // Send a message.
-        $url = new \moodle_url('/message/index.php', array('user' => $USER->id, 'id' => $studentid));
-        $pix = new \pix_icon('t/message', get_string('sendmessage', 'message'));
-        $actions[] = new \core_analytics\prediction_action('studentmessage', $prediction, $url, $pix,
-            get_string('sendmessage', 'message'), false, $attrs);
-
-        // View outline report.
-        $url = new \moodle_url('/report/outline/user.php', array('id' => $studentid, 'course' => $sampledata['course']->id,
-            'mode' => 'outline'));
-        $pix = new \pix_icon('i/report', get_string('outlinereport'));
-        $actions[] = new \core_analytics\prediction_action('viewoutlinereport', $prediction, $url, $pix,
-            get_string('outlinereport'), false, $attrs);
-
-        return array_merge($actions, parent::prediction_actions($prediction, $includedetailsaction));
-    }
-
     /**
      * classes_description
      *
@@ -107,15 +74,6 @@ class course_dropout extends \core_analytics\local\target\binary {
         return array();
     }
 
-    /**
-     * get_analyser_class
-     *
-     * @return string
-     */
-    public function get_analyser_class() {
-        return '\core\analytics\analyser\student_enrolments';
-    }
-
     /**
      * Discards courses that are not yet ready to be used for training or prediction.
      *
@@ -126,54 +84,17 @@ class course_dropout extends \core_analytics\local\target\binary {
     public function is_valid_analysable(\core_analytics\analysable $course, $fortraining = true) {
         global $DB;
 
-        if (!$course->was_started()) {
-            return get_string('coursenotyetstarted');
-        }
-
-        if (!$students = $course->get_students()) {
-            return get_string('nocoursestudents');
-        }
-
-        if (!course_format_uses_sections($course->get_course_data()->format)) {
-            // We can not split activities in time ranges.
-            return get_string('nocoursesections');
-        }
-
-        if ($course->get_start() == 0) {
-            // We require time start to be set.
-            return get_string('nocoursestarttime');
-        }
-
-        if ($course->get_end() == 0) {
-            // We require time end to be set.
-            return get_string('nocourseendtime');
-        }
+        $isvalid = parent::is_valid_analysable($course, $fortraining);
 
-        if ($course->get_end() < $course->get_start()) {
-            return get_string('errorendbeforestart', 'analytics');
-        }
-
-        // A course that lasts longer than 1 year probably have wrong start or end dates.
-        if ($course->get_end() - $course->get_start() > (YEARSECS + (WEEKSECS * 4))) {
-            return get_string('coursetoolong', 'analytics');
-        }
-
-        // Finished courses can not be used to get predictions.
-        if (!$fortraining && $course->is_finished()) {
-            return get_string('coursealreadyfinished');
-        }
-
-        // Ongoing courses data can not be used to train.
-        if ($fortraining && !$course->is_finished()) {
-            return get_string('coursenotyetfinished');
+        if (is_string($isvalid)) {
+            return $isvalid;
         }
 
         if ($fortraining) {
             // Not a valid target for training if there are not enough course accesses between the course start and end dates.
-
             $params = array('courseid' => $course->get_id(), 'anonymous' => 0, 'start' => $course->get_start(),
                 'end' => $course->get_end());
-            list($studentssql, $studentparams) = $DB->get_in_or_equal($students, SQL_PARAMS_NAMED);
+            list($studentssql, $studentparams) = $DB->get_in_or_equal($this->students, SQL_PARAMS_NAMED);
             // Using anonymous to use the db index, not filtering by timecreated to speed it up.
             $select = 'courseid = :courseid AND anonymous = :anonymous AND timecreated > :start AND timecreated < :end ' .
                 'AND userid ' . $studentssql;
@@ -184,7 +105,7 @@ class course_dropout extends \core_analytics\local\target\binary {
             $nlogs = $logstore->get_events_select_count($select, array_merge($params, $studentparams));
 
             // At least a minimum of students activity.
-            $nstudents = count($students);
+            $nstudents = count($this->students);
             if ($nlogs / $nstudents < 10) {
                 return get_string('nocourseactivity');
             }
@@ -193,43 +114,6 @@ class course_dropout extends \core_analytics\local\target\binary {
         return true;
     }
 
-    /**
-     * Discard student enrolments that are invalid.
-     *
-     * @param int $sampleid
-     * @param \core_analytics\analysable $course
-     * @param bool $fortraining
-     * @return bool
-     */
-    public function is_valid_sample($sampleid, \core_analytics\analysable $course, $fortraining = true) {
-
-        $userenrol = $this->retrieve('user_enrolments', $sampleid);
-        if ($userenrol->timeend && $course->get_start() > $userenrol->timeend) {
-            // Discard enrolments which time end is prior to the course start. This should get rid of
-            // old user enrolments that remain on the course.
-            return false;
-        }
-
-        $limit = $course->get_start() - (YEARSECS + (WEEKSECS * 4));
-        if (($userenrol->timestart && $userenrol->timestart < $limit) ||
-                (!$userenrol->timestart && $userenrol->timecreated < $limit)) {
-            // Following what we do in is_valid_sample, we will discard enrolments that last more than 1 academic year
-            // because they have incorrect start and end dates or because they are reused along multiple years
-            // without removing previous academic years students. This may not be very accurate because some courses
-            // can last just some months, but it is better than nothing and they will be flagged as drop out anyway
-            // in most of the cases.
-            return false;
-        }
-
-        if (($userenrol->timestart && $userenrol->timestart > $course->get_end()) ||
-                (!$userenrol->timestart && $userenrol->timecreated > $course->get_end())) {
-            // Discard user enrolments that starts after the analysable official end.
-            return false;
-        }
-
-        return true;
-    }
-
     /**
      * calculate_sample
      *
diff --git a/lib/classes/analytics/target/course_enrolments.php b/lib/classes/analytics/target/course_enrolments.php
new file mode 100644 (file)
index 0000000..edc33e4
--- /dev/null
@@ -0,0 +1,172 @@
+<?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/>.
+
+/**
+ * Base class for targets whose analysable is a course using user enrolments as samples.
+ *
+ * @package   core
+ * @copyright 2019 Victor Deniz <victor@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\analytics\target;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Base class for targets whose analysable is a course using user enrolments as samples.
+ *
+ * @package   core
+ * @copyright 2019 Victor Deniz <victor@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class course_enrolments extends \core_analytics\local\target\binary {
+
+    /**
+     * Students in the course.
+     * @var int[]
+     */
+    protected $students;
+
+    /**
+     * Returns the analyser class that should be used along with this target.
+     *
+     * @return string The full class name as a string
+     */
+    public function get_analyser_class() {
+        return '\core\analytics\analyser\student_enrolments';
+    }
+
+    /**
+     * Discards courses that are not yet ready to be used for training or prediction.
+     *
+     * @param \core_analytics\analysable $course
+     * @param bool $fortraining
+     * @return true|string
+     */
+    public function is_valid_analysable(\core_analytics\analysable $course, $fortraining = true) {
+
+        if (!$course->was_started()) {
+            return get_string('coursenotyetstarted');
+        }
+
+        if (!$this->students = $course->get_students()) {
+            return get_string('nocoursestudents');
+        }
+
+        if (!course_format_uses_sections($course->get_course_data()->format)) {
+            // We can not split activities in time ranges.
+            return get_string('nocoursesections');
+        }
+
+        if ($course->get_end() == 0) {
+            // We require time end to be set.
+            return get_string('nocourseendtime');
+        }
+
+        if ($course->get_end() < $course->get_start()) {
+            return get_string('errorendbeforestart', 'analytics');
+        }
+
+        // A course that lasts longer than 1 year probably have wrong start or end dates.
+        if ($course->get_end() - $course->get_start() > (YEARSECS + (WEEKSECS * 4))) {
+            return get_string('coursetoolong', 'analytics');
+        }
+
+        // Finished courses can not be used to get predictions.
+        if (!$fortraining && $course->is_finished()) {
+            return get_string('coursealreadyfinished');
+        }
+
+        if ($fortraining) {
+            // Ongoing courses data can not be used to train.
+            if (!$course->is_finished()) {
+                return get_string('coursenotyetfinished');
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Discard student enrolments that are invalid.
+     *
+     * @param int $sampleid
+     * @param \core_analytics\analysable $course
+     * @param bool $fortraining
+     * @return bool
+     */
+    public function is_valid_sample($sampleid, \core_analytics\analysable $course, $fortraining = true) {
+
+        $userenrol = $this->retrieve('user_enrolments', $sampleid);
+        if ($userenrol->timeend && $course->get_start() > $userenrol->timeend) {
+            // Discard enrolments which time end is prior to the course start. This should get rid of
+            // old user enrolments that remain on the course.
+            return false;
+        }
+
+        $limit = $course->get_start() - (YEARSECS + (WEEKSECS * 4));
+        if (($userenrol->timestart && $userenrol->timestart < $limit) ||
+                (!$userenrol->timestart && $userenrol->timecreated < $limit)) {
+            // Following what we do in is_valid_sample, we will discard enrolments that last more than 1 academic year
+            // because they have incorrect start and end dates or because they are reused along multiple years
+            // without removing previous academic years students. This may not be very accurate because some courses
+            // can last just some months, but it is better than nothing.
+            return false;
+        }
+
+        if (($userenrol->timestart && $userenrol->timestart > $course->get_end()) ||
+                (!$userenrol->timestart && $userenrol->timecreated > $course->get_end())) {
+            // Discard user enrolments that starts after the analysable official end.
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * prediction_actions
+     *
+     * @param \core_analytics\prediction $prediction
+     * @param bool $includedetailsaction
+     * @return \core_analytics\prediction_action[]
+     */
+    public function prediction_actions(\core_analytics\prediction $prediction, $includedetailsaction = false) {
+        global $USER;
+
+        $actions = array();
+
+        $sampledata = $prediction->get_sample_data();
+        $studentid = $sampledata['user']->id;
+
+        $attrs = array('target' => '_blank');
+
+        // Send a message.
+        $url = new \moodle_url('/message/index.php', array('user' => $USER->id, 'id' => $studentid));
+        $pix = new \pix_icon('t/message', get_string('sendmessage', 'message'));
+        $actions[] = new \core_analytics\prediction_action('studentmessage', $prediction, $url, $pix,
+                get_string('sendmessage', 'message'), false, $attrs);
+
+        // View outline report.
+        $url = new \moodle_url('/report/outline/user.php', array('id' => $studentid, 'course' => $sampledata['course']->id,
+                'mode' => 'outline'));
+        $pix = new \pix_icon('i/report', get_string('outlinereport'));
+        $actions[] = new \core_analytics\prediction_action('viewoutlinereport', $prediction, $url, $pix,
+                get_string('outlinereport'), false, $attrs);
+
+        return array_merge($actions, parent::prediction_actions($prediction, $includedetailsaction));
+    }
+}