MDL-58859 analytics: Analytics API added to core
authorDavid Monllao <davidm@moodle.com>
Tue, 23 May 2017 09:16:30 +0000 (17:16 +0800)
committerDavid Monllao <davidm@moodle.com>
Mon, 24 Jul 2017 05:53:03 +0000 (07:53 +0200)
Part of MDL-57791 epic.

89 files changed:
admin/settings/analytics.php [new file with mode: 0644]
analytics/classes/admin_setting_predictor.php [new file with mode: 0644]
analytics/classes/analysable.php [new file with mode: 0644]
analytics/classes/calculable.php [new file with mode: 0644]
analytics/classes/course.php [new file with mode: 0644]
analytics/classes/dataset_manager.php [new file with mode: 0644]
analytics/classes/event/action_clicked.php [new file with mode: 0644]
analytics/classes/local/analyser/base.php [new file with mode: 0644]
analytics/classes/local/analyser/by_course.php [new file with mode: 0644]
analytics/classes/local/analyser/courses.php [new file with mode: 0644]
analytics/classes/local/analyser/sitewide.php [new file with mode: 0644]
analytics/classes/local/analyser/student_enrolments.php [new file with mode: 0644]
analytics/classes/local/indicator/activity_cognitive_depth.php [new file with mode: 0644]
analytics/classes/local/indicator/activity_social_breadth.php [new file with mode: 0644]
analytics/classes/local/indicator/any_access_after_end.php [new file with mode: 0644]
analytics/classes/local/indicator/any_access_before_start.php [new file with mode: 0644]
analytics/classes/local/indicator/any_write_action.php [new file with mode: 0644]
analytics/classes/local/indicator/assign/cognitive_depth.php [new file with mode: 0644]
analytics/classes/local/indicator/assign/social_breadth.php [new file with mode: 0644]
analytics/classes/local/indicator/base.php [new file with mode: 0644]
analytics/classes/local/indicator/binary.php [new file with mode: 0644]
analytics/classes/local/indicator/book/cognitive_depth.php [new file with mode: 0644]
analytics/classes/local/indicator/book/social_breadth.php [new file with mode: 0644]
analytics/classes/local/indicator/chat/cognitive_depth.php [new file with mode: 0644]
analytics/classes/local/indicator/chat/social_breadth.php [new file with mode: 0644]
analytics/classes/local/indicator/choice/cognitive_depth.php [new file with mode: 0644]
analytics/classes/local/indicator/choice/social_breadth.php [new file with mode: 0644]
analytics/classes/local/indicator/community_of_inquiry_activity.php [new file with mode: 0644]
analytics/classes/local/indicator/data/cognitive_depth.php [new file with mode: 0644]
analytics/classes/local/indicator/data/social_breadth.php [new file with mode: 0644]
analytics/classes/local/indicator/discrete.php [new file with mode: 0644]
analytics/classes/local/indicator/feedback/cognitive_depth.php [new file with mode: 0644]
analytics/classes/local/indicator/feedback/social_breadth.php [new file with mode: 0644]
analytics/classes/local/indicator/folder/cognitive_depth.php [new file with mode: 0644]
analytics/classes/local/indicator/folder/social_breadth.php [new file with mode: 0644]
analytics/classes/local/indicator/forum/cognitive_depth.php [new file with mode: 0644]
analytics/classes/local/indicator/forum/social_breadth.php [new file with mode: 0644]
analytics/classes/local/indicator/glossary/cognitive_depth.php [new file with mode: 0644]
analytics/classes/local/indicator/glossary/social_breadth.php [new file with mode: 0644]
analytics/classes/local/indicator/imscp/cognitive_depth.php [new file with mode: 0644]
analytics/classes/local/indicator/imscp/social_breadth.php [new file with mode: 0644]
analytics/classes/local/indicator/label/cognitive_depth.php [new file with mode: 0644]
analytics/classes/local/indicator/label/social_breadth.php [new file with mode: 0644]
analytics/classes/local/indicator/lesson/cognitive_depth.php [new file with mode: 0644]
analytics/classes/local/indicator/lesson/social_breadth.php [new file with mode: 0644]
analytics/classes/local/indicator/linear.php [new file with mode: 0644]
analytics/classes/local/indicator/lti/cognitive_depth.php [new file with mode: 0644]
analytics/classes/local/indicator/lti/social_breadth.php [new file with mode: 0644]
analytics/classes/local/indicator/page/cognitive_depth.php [new file with mode: 0644]
analytics/classes/local/indicator/page/social_breadth.php [new file with mode: 0644]
analytics/classes/local/indicator/quiz/cognitive_depth.php [new file with mode: 0644]
analytics/classes/local/indicator/quiz/social_breadth.php [new file with mode: 0644]
analytics/classes/local/indicator/read_actions.php [new file with mode: 0644]
analytics/classes/local/indicator/resource/cognitive_depth.php [new file with mode: 0644]
analytics/classes/local/indicator/resource/social_breadth.php [new file with mode: 0644]
analytics/classes/local/indicator/scorm/cognitive_depth.php [new file with mode: 0644]
analytics/classes/local/indicator/scorm/social_breadth.php [new file with mode: 0644]
analytics/classes/local/indicator/survey/cognitive_depth.php [new file with mode: 0644]
analytics/classes/local/indicator/survey/social_breadth.php [new file with mode: 0644]
analytics/classes/local/indicator/url/cognitive_depth.php [new file with mode: 0644]
analytics/classes/local/indicator/url/social_breadth.php [new file with mode: 0644]
analytics/classes/local/indicator/user_profile_set.php [new file with mode: 0644]
analytics/classes/local/indicator/user_track_forums.php [new file with mode: 0644]
analytics/classes/local/indicator/wiki/cognitive_depth.php [new file with mode: 0644]
analytics/classes/local/indicator/wiki/social_breadth.php [new file with mode: 0644]
analytics/classes/local/indicator/workshop/cognitive_depth.php [new file with mode: 0644]
analytics/classes/local/indicator/workshop/social_breadth.php [new file with mode: 0644]
analytics/classes/local/target/base.php [new file with mode: 0644]
analytics/classes/local/target/binary.php [new file with mode: 0644]
analytics/classes/local/target/discrete.php [new file with mode: 0644]
analytics/classes/local/target/linear.php [new file with mode: 0644]
analytics/classes/local/time_splitting/base.php [new file with mode: 0644]
analytics/classes/local/time_splitting/deciles.php [new file with mode: 0644]
analytics/classes/local/time_splitting/deciles_accum.php [new file with mode: 0644]
analytics/classes/local/time_splitting/no_splitting.php [new file with mode: 0644]
analytics/classes/local/time_splitting/quarters.php [new file with mode: 0644]
analytics/classes/local/time_splitting/quarters_accum.php [new file with mode: 0644]
analytics/classes/local/time_splitting/single_range.php [new file with mode: 0644]
analytics/classes/local/time_splitting/weekly.php [new file with mode: 0644]
analytics/classes/local/time_splitting/weekly_accum.php [new file with mode: 0644]
analytics/classes/manager.php [new file with mode: 0644]
analytics/classes/model.php [new file with mode: 0644]
analytics/classes/prediction.php [new file with mode: 0644]
analytics/classes/prediction_action.php [new file with mode: 0644]
analytics/classes/predictor.php [new file with mode: 0644]
analytics/classes/requirements_exception.php [new file with mode: 0644]
analytics/classes/site.php [new file with mode: 0644]
lang/en/analytics.php [new file with mode: 0644]
lang/en/role.php

diff --git a/admin/settings/analytics.php b/admin/settings/analytics.php
new file mode 100644 (file)
index 0000000..abf00de
--- /dev/null
@@ -0,0 +1,87 @@
+<?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/>.
+
+/**
+ * Adds settings links to admin tree.
+ *
+ * @package   core_analytics
+ * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+if ($hassiteconfig) {
+    $settings = new admin_settingpage('analyticssettings', new lang_string('analyticssettings', 'analytics'));
+    $ADMIN->add('appearance', $settings);
+
+    if ($ADMIN->fulltree) {
+        // Select the site prediction's processor.
+        $predictionprocessors = \core_analytics\manager::get_all_prediction_processors();
+        $predictors = array();
+        foreach ($predictionprocessors as $fullclassname => $predictor) {
+            $pluginname = substr($fullclassname, 1, strpos($fullclassname, '\\', 1) - 1);
+            $predictors[$fullclassname] = new lang_string('pluginname', $pluginname);
+        }
+        $settings->add(new \core_analytics\admin_setting_predictor('analytics/predictionsprocessor',
+            new lang_string('predictionsprocessor', 'analytics'), new lang_string('predictionsprocessor_help', 'analytics'),
+            '\mlbackend_php\processor', $predictors)
+        );
+
+        // Enable/disable time splitting methods.
+        $alltimesplittings = \core_analytics\manager::get_all_time_splittings();
+
+        $timesplittingoptions = array();
+        $timesplittingdefaults = array('\\core_analytics\\local\\time_splitting\\quarters_accum',
+            '\\core_analytics\\local\\time_splitting\\quarters');
+        foreach ($alltimesplittings as $key => $timesplitting) {
+            $timesplittingoptions[$key] = $timesplitting->get_name();
+        }
+        $settings->add(new admin_setting_configmultiselect('analytics/timesplittings',
+            new lang_string('enabledtimesplittings', 'analytics'), new lang_string('enabledtimesplittings_help', 'analytics'),
+            $timesplittingdefaults, $timesplittingoptions)
+        );
+
+        // Predictions processor output dir.
+        $defaultmodeloutputdir = rtrim($CFG->dataroot, '/') . DIRECTORY_SEPARATOR . 'models';
+        $settings->add(new admin_setting_configdirectory('analytics/modeloutputdir', new lang_string('modeloutputdir', 'analytics'),
+            new lang_string('modeloutputdirinfo', 'analytics'), $defaultmodeloutputdir));
+        $studentdefaultroles = [];
+        $teacherdefaultroles = [];
+
+        // Student and teacher roles.
+        $allroles = role_fix_names(get_all_roles());
+        $rolechoices = [];
+        foreach ($allroles as $role) {
+            $rolechoices[$role->id] = $role->localname;
+
+            if ($role->shortname == 'student') {
+                $studentdefaultroles[] = $role->id;
+            } else if ($role->shortname == 'teacher') {
+                $teacherdefaultroles[] = $role->id;
+            } else if ($role->shortname == 'editingteacher') {
+                $teacherdefaultroles[] = $role->id;
+            }
+        }
+
+        $settings->add(new admin_setting_configmultiselect('analytics/teacherroles', new lang_string('teacherroles', 'analytics'),
+           '', $teacherdefaultroles, $rolechoices));
+
+        $settings->add(new admin_setting_configmultiselect('analytics/studentroles', new lang_string('studentroles', 'analytics'),
+           '', $studentdefaultroles, $rolechoices));
+
+    }
+}
diff --git a/analytics/classes/admin_setting_predictor.php b/analytics/classes/admin_setting_predictor.php
new file mode 100644 (file)
index 0000000..cf2be8b
--- /dev/null
@@ -0,0 +1,57 @@
+<?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/>.
+
+/**
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@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__ . '/../../lib/adminlib.php');
+
+class admin_setting_predictor extends \admin_setting_configselect {
+
+    /**
+     * Builds HTML to display the control.
+     *
+     * The main purpose of this is to display a warning if the selected predictions processor is not ready.
+
+     * @param string $data Unused
+     * @param string $query
+     * @return string HTML
+     */
+    public function output_html($data, $query='') {
+        global $CFG, $OUTPUT;
+
+        $html = '';
+
+        // Calling it here without checking if it is ready because we check it below and show it as a controlled case.
+        $selectedprocessor = \core_analytics\manager::get_predictions_processor($data, false);
+
+        $isready = $selectedprocessor->is_ready();
+        if ($isready !== true) {
+            $html .= $OUTPUT->notification(get_string('errorprocessornotready', 'analytics', $isready));
+        }
+
+        $html .= parent::output_html($data, $query);
+        return $html;
+    }
+}
diff --git a/analytics/classes/analysable.php b/analytics/classes/analysable.php
new file mode 100644 (file)
index 0000000..a5187b1
--- /dev/null
@@ -0,0 +1,45 @@
+<?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/>.
+
+/**
+ *
+ * @package   core_analytics
+ * @copyright 2016 David Monllao {@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();
+
+/**
+ *
+ * @package   core_analytics
+ * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+interface analysable {
+
+    const MAX_TIME = 9999999999;
+
+    public function get_id();
+
+    public function get_context();
+
+    public function get_start();
+
+    public function get_end();
+}
diff --git a/analytics/classes/calculable.php b/analytics/classes/calculable.php
new file mode 100644 (file)
index 0000000..935b263
--- /dev/null
@@ -0,0 +1,156 @@
+<?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/>.
+
+/**
+ * Calculable dataset items abstract class.
+ *
+ * @package   core_analytics
+ * @copyright 2016 David Monllao {@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();
+
+/**
+ * Calculable dataset items abstract class.
+ *
+ * @package   core_analytics
+ * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class calculable {
+
+    /**
+     * Returns a visible name for the indicator.
+     *
+     * Used as column identificator.
+     *
+     * Defaults to the indicator class name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_called_class();
+    }
+
+    /**
+     * Returns the number of weeks a time range contains.
+     *
+     * Useful for calculations that depend on the time range duration.
+     *
+     * @param int $starttime
+     * @param int $endtime
+     * @return float
+     */
+    protected function get_time_range_weeks_number($starttime, $endtime) {
+        if ($endtime <= $starttime) {
+            throw new \coding_exception('End time timestamp should be greater than start time.');
+        }
+
+        $diff = $endtime - $starttime;
+
+        // No need to be strict about DST here.
+        return $diff / WEEKSECS;
+    }
+
+    /**
+     * Limits the calculated value to the minimum and maximum values.
+     *
+     * @param float $calculatedvalue
+     * @return float|null
+     */
+    protected function limit_value($calculatedvalue) {
+        return max(min($calculatedvalue, static::get_max_value()), static::get_min_value());
+    }
+
+    /**
+     * Classifies the provided value into the provided range according to the ranges predicates.
+     *
+     * Use:
+     * - eq as 'equal'
+     * - ne as 'not equal'
+     * - lt as 'lower than'
+     * - le as 'lower or equal than'
+     * - gt as 'greater than'
+     * - ge as 'greater or equal than'
+     *
+     * @param int|float $value
+     * @param array $ranges e.g. [ ['lt', 20], ['ge', 20] ]
+     * @return void
+     */
+    protected function classify_value($value, $ranges) {
+
+        // To automatically return calculated values from min to max values.
+        $rangeweight = (static::get_max_value() - static::get_min_value()) / (count($ranges) - 1);
+
+        foreach ($ranges as $key => $range) {
+
+            $match = false;
+
+            if (count($range) != 2) {
+                throw \coding_exception('classify_value() $ranges array param should contain 2 items, the predicate ' .
+                    'e.g. greater (gt), lower or equal (le)... and the value.');
+            }
+
+            list($predicate, $rangevalue) = $range;
+
+            switch ($predicate) {
+                case 'eq':
+                    if ($value == $rangevalue) {
+                        $match = true;
+                    }
+                    break;
+                case 'ne':
+                    if ($value != $rangevalue) {
+                        $match = true;
+                    }
+                    break;
+                case 'lt':
+                    if ($value < $rangevalue) {
+                        $match = true;
+                    }
+                    break;
+                case 'le':
+                    if ($value <= $rangevalue) {
+                        $match = true;
+                    }
+                    break;
+                case 'gt':
+                    if ($value > $rangevalue) {
+                        $match = true;
+                    }
+                    break;
+                case 'ge':
+                    if ($value >= $rangevalue) {
+                        $match = true;
+                    }
+                    break;
+                default:
+                    throw new \coding_exception('Unrecognised predicate ' . $predicate . '. Please use eq, ne, lt, le, ge or gt.');
+            }
+
+            // Calculate and return a linear calculated value for the provided value.
+            if ($match) {
+                return round(static::get_min_value() + ($rangeweight * $key), 2);
+            }
+        }
+
+        throw new \coding_exception('The provided value "' . $value . '" can not be fit into any of the provided ranges, you ' .
+            'should provide ranges for all possible values.');
+    }
+}
diff --git a/analytics/classes/course.php b/analytics/classes/course.php
new file mode 100644 (file)
index 0000000..3cb2fb1
--- /dev/null
@@ -0,0 +1,634 @@
+<?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/>.
+
+/**
+ *
+ * @package   core_analytics
+ * @copyright 2016 David Monllao {@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($CFG->dirroot . '/course/lib.php');
+require_once($CFG->dirroot . '/lib/gradelib.php');
+
+/**
+ *
+ * @package   core_analytics
+ * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class course implements \core_analytics\analysable {
+
+    const MIN_STUDENT_LOGS_PERCENT = 90;
+
+    protected static $instances = array();
+
+    protected $studentroles = [];
+    protected $teacherroles = [];
+
+    protected $course = null;
+    protected $coursecontext = null;
+
+    protected $courseactivities = array();
+
+    protected $starttime = null;
+    protected $started = null;
+    protected $endtime = null;
+    protected $finished = null;
+
+    protected $studentids = [];
+    protected $teacherids = [];
+
+    protected $ntotallogs = null;
+
+    /**
+     * Course manager constructor.
+     *
+     * Use self::instance() instead to get cached copies of the course. Instances obtained
+     * through this constructor will not be cached either.
+     *
+     * Loads course students and teachers.
+     *
+     * Let's try to keep this computationally inexpensive.
+     *
+     * @param int|stdClass $course Course id
+     * @param array $studentroles
+     * @param array $teacherroles
+     * @return void
+     */
+    public function __construct($course) {
+
+        if (is_scalar($course)) {
+            $this->course = get_course($course);
+        } else {
+            $this->course = $course;
+        }
+
+        $this->coursecontext = \context_course::instance($this->course->id);
+
+        $studentroles = get_config('analytics', 'studentroles');
+        $teacherroles = get_config('analytics', 'teacherroles');
+
+        if (empty($studentroles) || empty($teacherroles)) {
+            // Unexpected, site settings should be set with default values.
+            throw new \moodle_exception('errornoroles', 'analytics');
+        }
+
+        $this->studentroles = explode(',', $studentroles);
+        $this->teacherroles = explode(',', $teacherroles);
+
+        $this->now = time();
+
+        // Get the course users, including users assigned to student and teacher roles at an higher context.
+        $this->studentids = $this->get_user_ids($this->studentroles);
+        $this->teacherids = $this->get_user_ids($this->teacherroles);
+    }
+
+    /**
+     * instance
+     *
+     * @param int|stdClass $course Course id
+     * @return void
+     */
+    public static function instance($course) {
+
+        $courseid = $course;
+        if (!is_scalar($courseid)) {
+            $courseid = $course->id;
+        }
+
+        if (!empty(self::$instances[$courseid])) {
+            return self::$instances[$courseid];
+        }
+
+        $instance = new \core_analytics\course($course);
+        self::$instances[$courseid] = $instance;
+        return self::$instances[$courseid];
+    }
+
+    public function get_id() {
+        return $this->course->id;
+    }
+
+    public function get_context() {
+        if ($this->coursecontext === null) {
+            $this->coursecontext = \context_course::instance($this->course->id);
+        }
+        return $this->coursecontext;
+    }
+
+    /**
+     * Get the course start timestamp.
+     *
+     * @return int Timestamp or 0 if has not started yet.
+     */
+    public function get_start() {
+        global $DB;
+
+        if ($this->starttime !== null) {
+            return $this->starttime;
+        }
+
+        // The field always exist but may have no valid if the course is created through a sync process.
+        if (!empty($this->course->startdate)) {
+            $this->starttime = (int)$this->course->startdate;
+        } else {
+            $this->starttime = 0;
+        }
+
+        return $this->starttime;
+    }
+
+    public function guess_start() {
+        global $DB;
+
+        if (!$this->get_total_logs()) {
+            // Can't guess.
+            return 0;
+        }
+
+        // We first try to find current course student logs.
+        list($filterselect, $filterparams) = $this->course_students_query_filter();
+        $sql = "SELECT MIN(timecreated) FROM {logstore_standard_log}
+                  WHERE $filterselect
+                 GROUP BY userid";
+        $firstlogs = $DB->get_fieldset_sql($sql, $filterparams);
+        if (empty($firstlogs)) {
+            return 0;
+        }
+        sort($firstlogs);
+        $firstlogsmedian = $this->median($firstlogs);
+
+        // Not using enrol API because we may be dealing with databases that used
+        // 3rd party enrolment plugins that are not available in the database.
+        // TODO We will need to switch to enrol API once we have enough data.
+        list($studentssql, $studentparams) = $DB->get_in_or_equal($this->studentids, SQL_PARAMS_NAMED);
+        $sql = "SELECT ue.* FROM {user_enrolments} ue
+                  JOIN {enrol} e ON e.id = ue.enrolid
+                 WHERE e.courseid = :courseid AND ue.userid $studentssql";
+        $studentenrolments = $DB->get_records_sql($sql, array('courseid' => $this->course->id) + $studentparams);
+        if (empty($studentenrolments)) {
+            return 0;
+        }
+
+        $enrolstart = array();
+        foreach ($studentenrolments as $studentenrolment) {
+            // I don't like CASE WHEN :P
+            $enrolstart[] = ($studentenrolment->timestart) ? $studentenrolment->timestart : $studentenrolment->timecreated;
+        }
+        sort($enrolstart);
+        $enrolstartmedian = $this->median($enrolstart);
+
+        return intval(($enrolstartmedian + $firstlogsmedian) / 2);
+    }
+
+    /**
+     * Get the course end timestamp.
+     *
+     * @return int Timestamp or 0 if time end was not set.
+     */
+    public function get_end() {
+        global $DB;
+
+        if ($this->endtime !== null) {
+            return $this->endtime;
+        }
+
+        // The enddate field is only available from Moodle 3.2 (MDL-22078).
+        if (!empty($this->course->enddate)) {
+            $this->endtime = (int)$this->course->enddate;
+            return $this->endtime;
+        }
+
+        return 0;
+    }
+
+    /**
+     * Get the course end timestamp.
+     *
+     * @return int Timestamp, \core_analytics\analysable::MAX_TIME if we don't know but ongoing and 0 if we can not work it out.
+     */
+    public function guess_end() {
+        global $DB;
+
+        if ($this->get_total_logs() === 0) {
+            // No way to guess if there are no logs.
+            $this->endtime = 0;
+            return $this->endtime;
+        }
+
+        list($filterselect, $filterparams) = $this->course_students_query_filter('ula');
+
+        // Consider the course open if there are still student accesses.
+        $monthsago = time() - (WEEKSECS * 4 * 2);
+        $select = $filterselect . ' AND timeaccess > :timeaccess';
+        $params = $filterparams + array('timeaccess' => $monthsago);
+        $sql = "SELECT timeaccess FROM {user_lastaccess} ula
+                  JOIN {enrol} e ON e.courseid = ula.courseid
+                  JOIN {user_enrolments} ue ON e.id = ue.enrolid AND ue.userid = ula.userid
+                 WHERE $select";
+        if ($records = $DB->get_records_sql($sql, $params)) {
+            return 0;
+        }
+
+        $sql = "SELECT timeaccess FROM {user_lastaccess} ula
+                  JOIN {enrol} e ON e.courseid = ula.courseid
+                  JOIN {user_enrolments} ue ON e.id = ue.enrolid AND ue.userid = ula.userid
+                 WHERE $filterselect AND ula.timeaccess != 0
+                 ORDER BY timeaccess DESC";
+        $studentlastaccesses = $DB->get_fieldset_sql($sql, $filterparams);
+        if (empty($studentlastaccesses)) {
+            return 0;
+        }
+        sort($studentlastaccesses);
+
+        return $this->median($studentlastaccesses);
+    }
+
+    public function get_course_data() {
+        return $this->course;
+    }
+
+    /**
+     * Is the course valid to extract indicators from it?
+     *
+     * @return bool
+     */
+    public function is_valid() {
+
+        if (!$this->was_started() || !$this->is_finished()) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Has the course started?
+     *
+     * @return bool
+     */
+    public function was_started() {
+
+        if ($this->started === null) {
+            if ($this->get_start() === 0 || $this->now < $this->get_start()) {
+                // Not yet started.
+                $this->started = false;
+            } else {
+                $this->started = true;
+            }
+        }
+
+        return $this->started;
+    }
+
+    /**
+     * Has the course finished?
+     *
+     * @return bool
+     */
+    public function is_finished() {
+
+        if ($this->finished === null) {
+            $endtime = $this->get_end();
+            if ($endtime === 0 || $this->now < $endtime) {
+                // It is not yet finished or no idea when it finishes.
+                $this->finished = false;
+            } else {
+                $this->finished = true;
+            }
+        }
+
+        return $this->finished;
+    }
+
+    /**
+     * Returns a list of user ids matching the specified roles in this course.
+     *
+     * @param array $roleids
+     * @return array
+     */
+    public function get_user_ids($roleids) {
+
+        // We need to index by ra.id as a user may have more than 1 $roles role.
+        $records = get_role_users($roleids, $this->coursecontext, true, 'ra.id, u.id AS userid, r.id AS roleid', 'ra.id ASC');
+
+        // If a user have more than 1 $roles role array_combine will discard the duplicate.
+        $callable = array($this, 'filter_user_id');
+        $userids = array_values(array_map($callable, $records));
+        return array_combine($userids, $userids);
+    }
+
+    /**
+     * Returns the course students.
+     *
+     * @return stdClass[]
+     */
+    public function get_students() {
+        return $this->studentids;
+    }
+
+    /**
+     * Returns the total number of student logs in the course
+     *
+     * @return int
+     */
+    public function get_total_logs() {
+        global $DB;
+
+        // No logs if no students.
+        if (empty($this->studentids)) {
+            return 0;
+        }
+
+        if ($this->ntotallogs === null) {
+            list($filterselect, $filterparams) = $this->course_students_query_filter();
+            $this->ntotallogs = $DB->count_records_select('logstore_standard_log', $filterselect, $filterparams);
+        }
+
+        return $this->ntotallogs;
+    }
+
+    public function get_all_activities($activitytype) {
+
+        // Using is set because we set it to false if there are no activities.
+        if (!isset($this->courseactivities[$activitytype])) {
+            $modinfo = get_fast_modinfo($this->get_course_data(), -1);
+            $instances = $modinfo->get_instances_of($activitytype);
+
+            if ($instances) {
+                $this->courseactivities[$activitytype] = array();
+                foreach ($instances as $instance) {
+                    // By context.
+                    $this->courseactivities[$activitytype][$instance->context->id] = $instance;
+                }
+            } else {
+                $this->courseactivities[$activitytype] = false;
+            }
+        }
+
+        return $this->courseactivities[$activitytype];
+    }
+
+    public function get_student_grades($courseactivities) {
+
+        if (empty($courseactivities)) {
+            return array();
+        }
+
+        $grades = array();
+        foreach ($courseactivities as $contextid => $instance) {
+            $gradesinfo = grade_get_grades($this->course->id, 'mod', $instance->modname, $instance->instance, $this->studentids);
+
+            // Sort them by activity context and user.
+            if ($gradesinfo && $gradesinfo->items) {
+                foreach ($gradesinfo->items as $gradeitem) {
+                    foreach ($gradeitem->grades as $userid => $grade) {
+                        if (empty($grades[$contextid][$userid])) {
+                            // Initialise it as array because a single activity can have multiple grade items (e.g. workshop).
+                            $grades[$contextid][$userid] = array();
+                        }
+                        $grades[$contextid][$userid][$gradeitem->id] = $grade;
+                    }
+                }
+            }
+        }
+
+        return $grades;
+    }
+
+    public function get_activities($activitytype, $starttime, $endtime, $student = false) {
+
+        // $student may not be available, default to not calculating dynamic data.
+        $studentid = -1;
+        if ($student) {
+            $studentid = $student->id;
+        }
+        $modinfo = get_fast_modinfo($this->get_course_data(), $studentid);
+        $activities = $modinfo->get_instances_of($activitytype);
+
+        $timerangeactivities = array();
+        foreach ($activities as $activity) {
+            if (!$this->completed_by($activity, $starttime, $endtime)) {
+                continue;
+            }
+
+            $timerangeactivities[$activity->context->id] = $activity;
+        }
+
+        return $timerangeactivities;
+    }
+
+    protected function completed_by(\cm_info $activity, $starttime, $endtime) {
+
+        // We can't check uservisible because:
+        // - Any activity with available until would not be counted.
+        // - Sites may block student's course view capabilities once the course is closed.
+
+        // Students can not view hidden activities by default, this is not reliable 100% but accurate in most of the cases.
+        if ($activity->visible === false) {
+            return false;
+        }
+
+        // TODO Use course_modules_completion's timemodified + COMPLETION_COMPLETE* to discard
+        // activities that have already been completed.
+
+        // We skip activities that were not yet visible or their 'until' was not in this $starttime - $endtime range.
+        if ($activity->availability) {
+            $info = new \core_availability\info_module($activity);
+            $activityavailability = $this->availability_completed_by($info, $starttime, $endtime);
+            if ($activityavailability === false) {
+                return false;
+            } else if ($activityavailability === true) {
+                // This activity belongs to this time range.
+                return true;
+            }
+        }
+
+        //// We skip activities in sections that were not yet visible or their 'until' was not in this $starttime - $endtime range.
+        $section = $activity->get_modinfo()->get_section_info($activity->sectionnum);
+        if ($section->availability) {
+            $info = new \core_availability\info_section($section);
+            $sectionavailability = $this->availability_completed_by($info, $starttime, $endtime);
+            if ($sectionavailability === false) {
+                return false;
+            } else if ($sectionavailability === true) {
+                // This activity belongs to this section time range.
+                return true;
+            }
+        }
+
+        // When the course is using format weeks we use the week's end date.
+        $format = course_get_format($activity->get_modinfo()->get_course());
+        if ($this->course->format === 'weeks') {
+            $dates = $format->get_section_dates($section);
+
+            // We need to consider the +2 hours added by get_section_dates.
+            // Avoid $starttime <= $dates->end because $starttime may be the start of the next week.
+            if ($starttime < ($dates->end - 7200) && $endtime >= ($dates->end - 7200)) {
+                return true;
+            } else {
+                return false;
+            }
+        }
+
+        // TODO Think about activities in sectionnum 0.
+        if ($activity->sectionnum == 0) {
+            return false;
+        }
+
+        if (!$this->get_end() || !$this->get_start()) {
+            debugging('Activities which due date is in a time range can not be calculated ' .
+                'if the course doesn\'t have start and end date', DEBUG_DEVELOPER);
+            return false;
+        }
+
+        if (!course_format_uses_sections($this->course->format)) {
+            // If it does not use sections and there are no availability conditions to access it it is available
+            // and we can not magically classify it into any other time range than this one.
+            return true;
+        }
+
+        // Split the course duration in the number of sections and consider the end of each section the due
+        // date of all activities contained in that section.
+        $formatoptions = $format->get_format_options();
+        if (!empty($formatoptions['numsections'])) {
+            $nsections = $formatoptions['numsections'];
+        } else {
+            // There are course format that use sections but without numsections, we fallback to the number
+            // of cached sections in get_section_info_all, not that accurate though.
+            $coursesections = $activity->get_modinfo()->get_section_info_all();
+            $nsections = count($coursesections);
+            if (isset($coursesections[0])) {
+                // We don't count section 0 if it exists.
+                $nsections--;
+            }
+        }
+
+        $courseduration = $this->get_end() - $this->get_start();
+        $sectionduration = round($courseduration / $nsections);
+        $activitysectionenddate = $this->get_start() + ($sectionduration * $activity->sectionnum);
+        if ($activitysectionenddate > $starttime && $activitysectionenddate <= $endtime) {
+            return true;
+        }
+
+        return false;
+    }
+
+    protected function availability_completed_by(\core_availability\info $info, $starttime, $endtime) {
+
+        $dateconditions = $info->get_availability_tree()->get_all_children('\availability_date\condition');
+        foreach ($dateconditions as $condition) {
+            // Availability API does not allow us to check from / to dates nicely, we need to be naughty.
+            // TODO Would be nice to expand \availability_date\condition API for this calling a save that
+            // does not save is weird.
+            $conditiondata = $condition->save();
+
+            if ($conditiondata->d === \availability_date\condition::DIRECTION_FROM &&
+                    $conditiondata->t > $endtime) {
+                // Skip this activity if any 'from' date is later than the end time.
+                return false;
+
+            } else if ($conditiondata->d === \availability_date\condition::DIRECTION_UNTIL &&
+                    ($conditiondata->t < $starttime || $conditiondata->t > $endtime)) {
+                // Skip activity if any 'until' date is not in $starttime - $endtime range.
+                return false;
+            } else if ($conditiondata->d === \availability_date\condition::DIRECTION_UNTIL &&
+                    $conditiondata->t < $endtime && $conditiondata->t > $starttime) {
+                return true;
+            }
+        }
+
+        // This can be interpreted as 'the activity was available but we don't know if its expected completion date
+        // was during this period.
+        return null;
+    }
+
+    /**
+     * Used by get_user_ids to extract the user id.
+     *
+     * @param \stdClass $record
+     * @return int The user id.
+     */
+    protected function filter_user_id($record) {
+        return $record->userid;
+    }
+
+    /**
+     * Returns the average time between 2 timestamps.
+     *
+     * @param int $start
+     * @param int $end
+     * @return array [starttime, averagetime, endtime]
+     */
+    protected function update_loop_times($start, $end) {
+        $avg = intval(($start + $end) / 2);
+        return array($start, $avg, $end);
+    }
+
+    /**
+     * Returns the query and params used to filter {logstore_standard_log} table by this course students.
+     *
+     * @return array
+     */
+    protected function course_students_query_filter($prefix = false) {
+        global $DB;
+
+        if ($prefix) {
+            $prefix = $prefix . '.';
+        }
+
+        // Check the amount of student logs in the 4 previous weeks.
+        list($studentssql, $studentsparams) = $DB->get_in_or_equal($this->studentids, SQL_PARAMS_NAMED);
+        $filterselect = $prefix . 'courseid = :courseid AND ' . $prefix . 'userid ' . $studentssql;
+        $filterparams = array('courseid' => $this->course->id) + $studentsparams;
+
+        return array($filterselect, $filterparams);
+    }
+
+    /**
+     * Calculate median
+     *
+     * Keys are ignored.
+     *
+     * @param int|float $values Sorted array of values
+     * @return int
+     */
+    protected function median($values) {
+        $count = count($values);
+
+        if ($count === 1) {
+            return reset($values);
+        }
+
+        $middlevalue = floor(($count - 1) / 2);
+
+        if ($count % 2) {
+            // Odd number, middle is the median.
+            $median = $values[$middlevalue];
+        } else {
+            // Even number, calculate avg of 2 medians.
+            $low = $values[$middlevalue];
+            $high = $values[$middlevalue + 1];
+            $median = (($low + $high) / 2);
+        }
+        return intval($median);
+    }
+}
diff --git a/analytics/classes/dataset_manager.php b/analytics/classes/dataset_manager.php
new file mode 100644 (file)
index 0000000..676b2c1
--- /dev/null
@@ -0,0 +1,354 @@
+<?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/>.
+
+/**
+ *
+ * @package   core_analytics
+ * @copyright 2016 David Monllao {@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();
+
+/**
+ *
+ * @package   core_analytics
+ * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class dataset_manager {
+
+    const LABELLED_FILEAREA = 'labelled';
+    const UNLABELLED_FILEAREA = 'unlabelled';
+    const EVALUATION_FILENAME = 'evaluation.csv';
+
+    /**
+     * The model id.
+     *
+     * @var int
+     */
+    protected $modelid;
+
+    /**
+     * Range processor in use.
+     *
+     * @var string
+     */
+    protected $timesplittingid;
+
+    /**
+     * @var int
+     */
+    protected $analysableid;
+
+    /**
+     * Whether this is a dataset for evaluation or not.
+     *
+     * @var bool
+     */
+    protected $evaluation;
+
+    /**
+     * Labelled (true) or unlabelled data (false).
+     *
+     * @var bool
+     */
+    protected $includetarget;
+
+    /**
+     * Simple constructor.
+     *
+     * @return void
+     */
+    public function __construct($modelid, $analysableid, $timesplittingid, $evaluation = false, $includetarget = false) {
+        $this->modelid = $modelid;
+        $this->analysableid = $analysableid;
+        $this->timesplittingid = $timesplittingid;
+        $this->evaluation = $evaluation;
+        $this->includetarget = $includetarget;
+    }
+
+    /**
+     * Mark the analysable as being analysed.
+     *
+     * @return void
+     */
+    public function init_process() {
+        $lockkey = 'modelid:' . $this->modelid . '-analysableid:' . $this->analysableid .
+            '-timesplitting:' . self::convert_to_int($this->timesplittingid) . '-includetarget:' . (int)$this->includetarget;
+
+        // Large timeout as processes may be quite long.
+        $lockfactory = \core\lock\lock_config::get_lock_factory('core_analytics');
+        $this->lock = $lockfactory->get_lock($lockkey, WEEKSECS);
+
+        // We release the lock if there is an error during the process.
+        \core_shutdown_manager::register_function(array($this, 'release_lock'), array($this->lock));
+    }
+
+    /**
+     * Store the dataset in the internal file system.
+     *
+     * @param array $data
+     * @return \stored_file
+     */
+    public function store($data) {
+
+        // Delete previous file if it exists.
+        $fs = get_file_storage();
+        $filerecord = [
+            'component' => 'analytics',
+            'filearea' => self::get_filearea($this->includetarget),
+            'itemid' => $this->modelid,
+            'contextid' => \context_system::instance()->id,
+            'filepath' => '/analysable/' . $this->analysableid . '/' . self::convert_to_int($this->timesplittingid) . '/',
+            'filename' => self::get_filename($this->evaluation)
+        ];
+
+        // Delete previous and old (we already checked that previous copies are not recent) evaluation files for this analysable.
+        $select = " = {$filerecord['itemid']} AND filepath = :filepath";
+        $fs->delete_area_files_select($filerecord['contextid'], $filerecord['component'], $filerecord['filearea'],
+            $select, array('filepath' => $filerecord['filepath']));
+
+        // Write all this stuff to a tmp file.
+        $filepath = make_request_directory() . DIRECTORY_SEPARATOR . $filerecord['filename'];
+        $fh = fopen($filepath, 'w+');
+        foreach ($data as $line) {
+            fputcsv($fh, $line);
+        }
+        fclose($fh);
+
+        return $fs->create_file_from_pathname($filerecord, $filepath);
+    }
+
+    /**
+     * Mark as analysed.
+     *
+     * @return void
+     */
+    public function close_process() {
+        $this->lock->release();
+    }
+
+    public function release_lock(\core\lock\lock $lock) {
+        $lock->release();
+    }
+
+    /**
+     * Returns the previous evaluation file.
+     *
+     * Important to note that this is per modelid + timesplittingid, when dealing with multiple
+     * analysables this is the merged file. Do not confuse with self::get_evaluation_analysable_file
+     *
+     * @param int $modelid
+     * @param string $timesplittingid
+     * @return \stored_file
+     */
+    public static function get_previous_evaluation_file($modelid, $timesplittingid) {
+        $fs = get_file_storage();
+        // Evaluation data is always labelled.
+        return $fs->get_file(\context_system::instance()->id, 'analytics', self::LABELLED_FILEAREA, $modelid,
+            '/timesplitting/' . self::convert_to_int($timesplittingid) . '/', self::EVALUATION_FILENAME);
+    }
+
+    public static function delete_previous_evaluation_file($modelid, $timesplittingid) {
+        $fs = get_file_storage();
+        if ($file = self::get_previous_evaluation_file($modelid, $timesplittingid)) {
+            $file->delete();
+            return true;
+        }
+
+        return false;
+    }
+
+    public static function get_evaluation_analysable_file($modelid, $analysableid, $timesplittingid) {
+
+        // Delete previous file if it exists.
+        $fs = get_file_storage();
+
+        // Always evaluation.csv and labelled as it is an evaluation file.
+        $filearea = self::get_filearea(true);
+        $filename = self::get_filename(true);
+        $filepath = '/analysable/' . $analysableid . '/' . self::convert_to_int($timesplittingid) . '/';
+        return $fs->get_file(\context_system::instance()->id, 'analytics', $filearea, $modelid, $filepath, $filename);
+    }
+
+    /**
+     * Merge multiple files into one.
+     *
+     * Important! It is the caller responsability to ensure that the datasets are compatible.
+     *
+     * @param array  $files
+     * @param string $filename
+     * @param int    $modelid
+     * @param string $timesplittingid
+     * @param bool   $evaluation
+     * @param bool   $includetarget
+     * @return \stored_file
+     */
+    public static function merge_datasets(array $files, $modelid, $timesplittingid, $evaluation, $includetarget) {
+
+        $tmpfilepath = make_request_directory() . DIRECTORY_SEPARATOR . 'tmpfile.csv';
+
+        // Add headers.
+        // We could also do this with a single iteration gathering all files headers and appending them to the beginning of the file
+        // once all file contents are merged.
+        $varnames = '';
+        $analysablesvalues = array();
+        foreach ($files as $file) {
+            $rh = $file->get_content_file_handle();
+
+            // Copy the var names as they are, all files should have the same var names.
+            $varnames = fgetcsv($rh);
+
+            $analysablesvalues[] = fgetcsv($rh);
+
+            // Copy the columns as they are, all files should have the same columns.
+            $columns = fgetcsv($rh);
+        }
+
+        // Merge analysable values skipping the ones that are the same in all analysables.
+        $values = array();
+        foreach ($analysablesvalues as $analysablevalues) {
+            foreach ($analysablevalues as $varkey => $value) {
+                // Sha1 to make it unique.
+                $values[$varkey][sha1($value)] = $value;
+            }
+        }
+        foreach ($values as $varkey => $varvalues) {
+            $values[$varkey] = implode('|', $varvalues);
+        }
+
+        // Start writing to the merge file.
+        $wh = fopen($tmpfilepath, 'w');
+
+        fputcsv($wh, $varnames);
+        fputcsv($wh, $values);
+        fputcsv($wh, $columns);
+
+        // Iterate through all files and add them to the tmp one. We don't want file contents in memory.
+        foreach ($files as $file) {
+            $rh = $file->get_content_file_handle();
+
+            // Skip headers.
+            fgets($rh);
+            fgets($rh);
+            fgets($rh);
+
+            // Copy all the following lines.
+            while ($line = fgets($rh)) {
+                fwrite($wh, $line);
+            }
+            fclose($rh);
+        }
+        fclose($wh);
+
+        $filerecord = [
+            'component' => 'analytics',
+            'filearea' => self::get_filearea($includetarget),
+            'itemid' => $modelid,
+            'contextid' => \context_system::instance()->id,
+            'filepath' => '/timesplitting/' . self::convert_to_int($timesplittingid) . '/',
+            'filename' => self::get_filename($evaluation)
+        ];
+
+        $fs = get_file_storage();
+
+        return $fs->create_file_from_pathname($filerecord, $tmpfilepath);
+    }
+
+    public static function get_structured_data(\stored_file $dataset) {
+
+        if ($dataset->get_filearea() !== 'unlabelled') {
+            throw new \coding_exception('Sorry, only support for unlabelled data');
+        }
+
+        $rh = $dataset->get_content_file_handle();
+
+        // Skip dataset info.
+        fgets($rh);
+        fgets($rh);
+
+        $calculations = array();
+
+        $headers = fgetcsv($rh);
+        // Get rid of the sampleid column name.
+        array_shift($headers);
+
+        while ($columns = fgetcsv($rh)) {
+            $uniquesampleid = array_shift($columns);
+
+            // Unfortunately fgetcsv does not respect line's var types.
+            $calculations[$uniquesampleid] = array_map(function($value) {
+
+                if ($value === '') {
+                    // We really want them as null because converted to float become 0
+                    // and we need to treat the values separately.
+                    return null;
+                } else if (is_numeric($value)) {
+                    return floatval($value);
+                }
+                return $value;
+            }, array_combine($headers, $columns));
+        }
+
+        return $calculations;
+    }
+
+    public static function clear_model_files($modelid) {
+        $fs = get_file_storage();
+        return $fs->delete_area_files(\context_system::instance()->id, 'analytics', false, $modelid);
+    }
+
+    /**
+     * I know it is not very orthodox...
+     *
+     * @param string $string
+     * @return int
+     */
+    protected static function convert_to_int($string) {
+        $sum = 0;
+        for ($i = 0; $i < strlen($string); $i++) {
+            $sum += ord($string[$i]);
+        }
+        return $sum;
+    }
+
+    protected static function get_filename($evaluation) {
+
+        if ($evaluation === true) {
+            $filename = self::EVALUATION_FILENAME;
+        } else {
+            // Incremental time, the lock will make sure we don't have concurrency problems.
+            $filename = time() . '.csv';
+        }
+
+        return $filename;
+    }
+
+    protected static function get_filearea($includetarget) {
+
+        if ($includetarget === true) {
+            $filearea = self::LABELLED_FILEAREA;
+        } else {
+            $filearea = self::UNLABELLED_FILEAREA;
+        }
+
+        return $filearea;
+    }
+
+}
diff --git a/analytics/classes/event/action_clicked.php b/analytics/classes/event/action_clicked.php
new file mode 100644 (file)
index 0000000..58c080f
--- /dev/null
@@ -0,0 +1,95 @@
+<?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/>.
+
+/**
+ * Prediction action clicked event.
+ *
+ * @property-read array $other {
+ *      Extra information about event.
+ *
+ *      - string actionname: The action name
+ * }
+ *
+ * @package    core_analytics
+ * @copyright  2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\event;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Event triggered after a user clicked on one of the prediction suggested actions.
+ *
+ * @package    core_analytics
+ * @copyright  2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class action_clicked extends \core\event\base {
+
+    /**
+     * Set basic properties for the event.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'analytics_predictions';
+        $this->data['crud'] = 'r';
+        $this->data['edulevel'] = self::LEVEL_TEACHING;
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventactionclicked', 'analytics');
+    }
+
+    /**
+     * Returns non-localised event description with id's for admin use only.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' has clicked '{$this->other['actionname']}' action for the prediction with id '".$this->objectid."'.";
+    }
+
+    /**
+     * Returns relevant URL.
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/report/insights/prediction.php', array('id' => $this->objectid));
+    }
+
+    /**
+     * Custom validations.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->objectid)) {
+            throw new \coding_exception('The \'objectid\' must be set.');
+        }
+    }
+
+    public static function get_objectid_mapping() {
+        return array('db' => 'analytics_predictions');
+    }
+}
diff --git a/analytics/classes/local/analyser/base.php b/analytics/classes/local/analyser/base.php
new file mode 100644 (file)
index 0000000..168a36c
--- /dev/null
@@ -0,0 +1,431 @@
+<?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/>.
+
+/**
+ *
+ * @package   core_analytics
+ * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\analyser;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ *
+ * @package   core_analytics
+ * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class base {
+
+    protected $modelid;
+
+    protected $target;
+    protected $indicators;
+    protected $timesplittings;
+
+    protected $options;
+
+    protected $log;
+
+    public function __construct($modelid, \core_analytics\local\target\base $target, $indicators, $timesplittings, $options) {
+        $this->modelid = $modelid;
+        $this->target = $target;
+        $this->indicators = $indicators;
+        $this->timesplittings = $timesplittings;
+
+        if (empty($options['evaluation'])) {
+            $options['evaluation'] = false;
+        }
+        $this->options = $options;
+
+        // Checks if the analyser satisfies the indicators requirements.
+        $this->check_indicators_requirements();
+
+        $this->log = array();
+    }
+
+    /**
+     * This function returns the list of samples that can be calculated.
+     *
+     * @param \core_analytics\analysable $analysable
+     * @return array array[0] = int[], array[1] = array
+     */
+    abstract protected function get_all_samples(\core_analytics\analysable $analysable);
+
+    abstract public function get_samples($sampleids);
+
+    abstract protected function get_samples_origin();
+
+    /**
+     * moodle/analytics:listinsights will be required at this level to access the sample predictions.
+     *
+     * @param int $sampleid
+     * @return \context
+     */
+    abstract public function sample_access_context($sampleid);
+
+    abstract public function sample_description($sampleid, $contextid, $sampledata);
+
+    protected function provided_sample_data() {
+        return array($this->get_samples_origin());
+    }
+
+    /**
+     * Main analyser method which processes the site analysables.
+     *
+     * \core_analytics\local\analyser\by_course and \core_analytics\local\analyser\sitewide are implementing
+     * this method returning site courses (by_course) and the whole system (sitewide) as analysables.
+     * In most of the cases you should have enough extending from one of these classes so you don't need
+     * to reimplement this method.
+     *
+     * @return \stored_file[]
+     */
+    abstract public function get_analysable_data($includetarget);
+
+    public function get_labelled_data() {
+        return $this->get_analysable_data(true);
+    }
+
+    public function get_unlabelled_data() {
+        return $this->get_analysable_data(false);
+    }
+
+    /**
+     * Checks if the analyser satisfies all the model indicators requirements.
+     *
+     * @throws \core_analytics\requirements_exception
+     * @return void
+     */
+    protected function check_indicators_requirements() {
+
+        foreach ($this->indicators as $indicator) {
+            $missingrequired = $this->check_indicator_requirements($indicator);
+            if ($missingrequired !== true) {
+                throw new \core_analytics\requirements_exception(get_class($indicator) . ' indicator requires ' .
+                    json_encode($missingrequired) . ' sample data which is not provided by ' . get_class($this));
+            }
+        }
+    }
+
+    /**
+     * check_indicator_requirements
+     *
+     * @param \core_analytics\local\indicator\base $indicator
+     * @return true|string[] True if all good, missing requirements list otherwise
+     */
+    public function check_indicator_requirements(\core_analytics\local\indicator\base $indicator) {
+
+        $providedsampledata = $this->provided_sample_data();
+
+        $requiredsampledata = $indicator::required_sample_data();
+        if (empty($requiredsampledata)) {
+            // The indicator does not need any sample data.
+            return true;
+        }
+        $missingrequired = array_diff($requiredsampledata, $providedsampledata);
+
+        if (empty($missingrequired)) {
+            return true;
+        }
+
+        return $missingrequired;
+    }
+
+    /**
+     * Processes an analysable
+     *
+     * This method returns the general analysable status, an array of files by time splitting method and
+     * an error message if there is any problem.
+     *
+     * @param \core_analytics\analysable $analysable
+     * @param bool $includetarget
+     * @return \stored_file[] Files by time splitting method
+     */
+    public function process_analysable($analysable, $includetarget) {
+
+        // Default returns.
+        $files = array();
+        $message = null;
+
+        // Target instances scope is per-analysable (it can't be lower as calculations run once per
+        // analysable, not time splitting method nor time range).
+        $target = forward_static_call(array($this->target, 'instance'));
+
+        // We need to check that the analysable is valid for the target even if we don't include targets
+        // as we still need to discard invalid analysables for the target.
+        $result = $target->is_valid_analysable($analysable, $includetarget);
+        if ($result !== true) {
+            $a = new \stdClass();
+            $a->analysableid = $analysable->get_id();
+            $a->result = $result;
+            $this->log[] = get_string('analysablenotvalidfortarget', 'analytics', $a);
+            return array();
+        }
+
+        // Process all provided time splitting methods.
+        $results = array();
+        foreach ($this->timesplittings as $timesplitting) {
+
+            // For evaluation purposes we don't need to be that strict about how updated the data is,
+            // if this analyser was analysed less that 1 week ago we skip generating a new one. This
+            // helps scale the evaluation process as sites with tons of courses may a lot of time to
+            // complete an evaluation.
+            if (!empty($this->options['evaluation']) && !empty($this->options['reuseprevanalysed'])) {
+
+                $previousanalysis = \core_analytics\dataset_manager::get_evaluation_analysable_file($this->modelid,
+                    $analysable->get_id(), $timesplitting->get_id());
+                $boundary = time() - WEEKSECS;
+                if ($previousanalysis && $previousanalysis->get_timecreated() > $boundary) {
+                    // Recover the previous analysed file and avoid generating a new one.
+
+                    // Don't bother filling a result object as it is only useful when there are no files generated.
+                    $files[$timesplitting->get_id()] = $previousanalysis;
+                    continue;
+                }
+            }
+
+            if ($includetarget) {
+                $result = $this->process_time_splitting($timesplitting, $analysable, $target);
+            } else {
+                $result = $this->process_time_splitting($timesplitting, $analysable);
+            }
+
+            if (!empty($result->file)) {
+                $files[$timesplitting->get_id()] = $result->file;
+            }
+            $results[] = $result;
+        }
+
+        if (empty($files)) {
+            $errors = array();
+            foreach ($results as $timesplittingid => $result) {
+                $errors[] = $timesplittingid . ': ' . $result->message;
+            }
+
+            $a = new \stdClass();
+            $a->analysableid = $analysable->get_id();
+            $a->errors =  implode(', ', $errors);
+            $this->log[] = get_string('analysablenotused', 'analytics', $a);
+        }
+
+        return $files;
+    }
+
+    public function get_logs() {
+        return $this->log;
+    }
+
+    protected function process_time_splitting($timesplitting, $analysable, $target = false) {
+
+        $result = new \stdClass();
+
+        if (!$timesplitting->is_valid_analysable($analysable)) {
+            $result->status = \core_analytics\model::ANALYSE_REJECTED_RANGE_PROCESSOR;
+            $result->message = get_string('invalidanalysablefortimesplitting', 'analytics',
+                $timesplitting->get_name());
+            return $result;
+        }
+        $timesplitting->set_analysable($analysable);
+
+        if (CLI_SCRIPT && !PHPUNIT_TEST) {
+            mtrace('Analysing id "' . $analysable->get_id() . '" with "' . $timesplitting->get_name() . '" time splitting method...');
+        }
+
+        // What is a sample is defined by the analyser, it can be an enrolment, a course, a user, a question
+        // attempt... it is on what we will base indicators calculations.
+        list($sampleids, $samplesdata) = $this->get_all_samples($analysable);
+
+        if (count($sampleids) === 0) {
+            $result->status = \core_analytics\model::ANALYSE_REJECTED_RANGE_PROCESSOR;
+            $result->message = get_string('nodata', 'analytics');
+            return $result;
+        }
+
+        if ($target) {
+            // All ranges are used when we are calculating data for training.
+            $ranges = $timesplitting->get_all_ranges();
+        } else {
+            // Only some ranges can be used for prediction (it depends on the time range where we are right now).
+            $ranges = $this->get_prediction_ranges($timesplitting);
+        }
+
+        // There is no need to keep track of the evaluated samples and ranges as we always evaluate the whole dataset.
+        if ($this->options['evaluation'] === false) {
+
+            if (empty($ranges)) {
+                $result->status = \core_analytics\model::ANALYSE_REJECTED_RANGE_PROCESSOR;
+                $result->message = get_string('nonewdata', 'analytics');
+                return $result;
+            }
+
+            // We skip all samples that are already part of a training dataset, even if they have noe been used for training yet.
+            $sampleids = $this->filter_out_train_samples($sampleids, $timesplitting);
+
+            if (count($sampleids) === 0) {
+                $result->status = \core_analytics\model::ANALYSE_REJECTED_RANGE_PROCESSOR;
+                $result->message = get_string('nonewdata', 'analytics');
+                return $result;
+            }
+
+            // TODO We may be interested in limiting $samplesdata contents to $sampleids after filtering out some sampleids.
+
+            // Only when processing data for predictions.
+            if ($target === false) {
+                // We also filter out ranges that have already been used for predictions.
+                $ranges = $this->filter_out_prediction_ranges($ranges, $timesplitting);
+            }
+
+            if (count($ranges) === 0) {
+                $result->status = \core_analytics\model::ANALYSE_REJECTED_RANGE_PROCESSOR;
+                $result->message = get_string('nonewtimeranges', 'analytics');
+                return $result;
+            }
+        }
+
+        $dataset = new \core_analytics\dataset_manager($this->modelid, $analysable->get_id(), $timesplitting->get_id(),
+            $this->options['evaluation'], !empty($target));
+
+        // Flag the model + analysable + timesplitting as being analysed (prevent concurrent executions).
+        $dataset->init_process();
+
+        foreach ($this->indicators as $key => $indicator) {
+            // The analyser attaches the main entities the sample depends on and are provided to the
+            // indicator to calculate the sample.
+            $this->indicators[$key]->set_sample_data($samplesdata);
+        }
+
+        // Here we start the memory intensive process that will last until $data var is
+        // unset (until the method is finished basically).
+        $data = $timesplitting->calculate($sampleids, $this->get_samples_origin(), $this->indicators, $ranges, $target);
+
+        if (!$data) {
+            $result->status = \core_analytics\model::ANALYSE_REJECTED_RANGE_PROCESSOR;
+            $result->message = get_string('novaliddata', 'analytics');
+            return $result;
+        }
+
+        // Write all calculated data to a file.
+        $file = $dataset->store($data);
+
+        // Flag the model + analysable + timesplitting as analysed.
+        $dataset->close_process();
+
+        // No need to keep track of analysed stuff when evaluating.
+        if ($this->options['evaluation'] === false) {
+            // Save the samples that have been already analysed so they are not analysed again in future.
+
+            if ($target) {
+                $this->save_train_samples($sampleids, $timesplitting, $file);
+            } else {
+                $this->save_prediction_ranges($ranges, $timesplitting);
+            }
+        }
+
+        $result->status = \core_analytics\model::OK;
+        $result->message = get_string('successfullyanalysed', 'analytics');
+        $result->file = $file;
+        return $result;
+    }
+
+    protected function get_prediction_ranges($timesplitting) {
+
+        $now = time();
+
+        // We already provided the analysable to the time splitting method, there is no need to feed it back.
+        $predictionranges = array();
+        foreach ($timesplitting->get_all_ranges() as $rangeindex => $range) {
+            if ($timesplitting->ready_to_predict($range)) {
+                // We need to maintain the same indexes.
+                $predictionranges[$rangeindex] = $range;
+            }
+        }
+
+        return $predictionranges;
+    }
+
+    protected function filter_out_train_samples($sampleids, $timesplitting) {
+        global $DB;
+
+        $params = array('modelid' => $this->modelid, 'analysableid' => $timesplitting->get_analysable()->get_id(),
+            'timesplitting' => $timesplitting->get_id());
+
+        $trainingsamples = $DB->get_records('analytics_train_samples', $params);
+
+        // Skip each file trained samples.
+        foreach ($trainingsamples as $trainingfile) {
+
+            $usedsamples = json_decode($trainingfile->sampleids, true);
+
+            if (!empty($usedsamples)) {
+                // Reset $sampleids to $sampleids minus this file's $usedsamples.
+                $sampleids = array_diff_key($sampleids, $usedsamples);
+            }
+        }
+
+        return $sampleids;
+    }
+
+    protected function filter_out_prediction_ranges($ranges, $timesplitting) {
+        global $DB;
+
+        $params = array('modelid' => $this->modelid, 'analysableid' => $timesplitting->get_analysable()->get_id(),
+            'timesplitting' => $timesplitting->get_id());
+
+        $predictedranges = $DB->get_records('analytics_predict_ranges', $params);
+        foreach ($predictedranges as $predictedrange) {
+            if (!empty($ranges[$predictedrange->rangeindex])) {
+                unset($ranges[$predictedrange->rangeindex]);
+            }
+        }
+
+        return $ranges;
+
+    }
+
+    protected function save_train_samples($sampleids, $timesplitting, $file) {
+        global $DB;
+
+        $trainingsamples = new \stdClass();
+        $trainingsamples->modelid = $this->modelid;
+        $trainingsamples->analysableid = $timesplitting->get_analysable()->get_id();
+        $trainingsamples->timesplitting = $timesplitting->get_id();
+        $trainingsamples->fileid = $file->get_id();
+
+        // TODO We just need the keys, we can save some space by removing the values.
+        $trainingsamples->sampleids = json_encode($sampleids);
+        $trainingsamples->timecreated = time();
+
+        return $DB->insert_record('analytics_train_samples', $trainingsamples);
+    }
+
+    protected function save_prediction_ranges($ranges, $timesplitting) {
+        global $DB;
+
+        $predictionrange = new \stdClass();
+        $predictionrange->modelid = $this->modelid;
+        $predictionrange->analysableid = $timesplitting->get_analysable()->get_id();
+        $predictionrange->timesplitting = $timesplitting->get_id();
+        $predictionrange->timecreated = time();
+
+        foreach ($ranges as $rangeindex => $unused) {
+            $predictionrange->rangeindex = $rangeindex;
+            $DB->insert_record('analytics_predict_ranges', $predictionrange);
+        }
+    }
+}
diff --git a/analytics/classes/local/analyser/by_course.php b/analytics/classes/local/analyser/by_course.php
new file mode 100644 (file)
index 0000000..c4e4a47
--- /dev/null
@@ -0,0 +1,101 @@
+<?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/>.
+
+/**
+ *
+ * @package   core_analytics
+ * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\analyser;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ *
+ * @package   core_analytics
+ * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class by_course extends base {
+
+    public function get_courses() {
+        global $DB;
+
+        // Default to all system courses.
+        if (!empty($this->options['filter'])) {
+            $courseids = $this->options['filter'];
+        } else {
+            // Iterate through all potentially valid courses.
+            $courseids = $DB->get_fieldset_select('course', 'id', 'id != :frontpage', array('frontpage' => SITEID), 'sortorder ASC');
+        }
+
+        $analysables = array();
+        foreach ($courseids as $courseid) {
+            $analysable = new \core_analytics\course($courseid);
+            $analysables[$analysable->get_id()] = $analysable;
+        }
+
+        if (empty($analysables)) {
+            $this->logs[] = get_string('nocourses', 'analytics');
+        }
+
+        return $analysables;
+    }
+
+    public function get_analysable_data($includetarget) {
+
+        $status = array();
+        $messages = array();
+        $filesbytimesplitting = array();
+
+        // This class and all children will iterate through a list of courses (\core_analytics\course).
+        $analysables = $this->get_courses();
+        foreach ($analysables as $analysableid => $analysable) {
+
+            $files = $this->process_analysable($analysable, $includetarget);
+
+            // Later we will need to aggregate data by time splitting method.
+            foreach ($files as $timesplittingid => $file) {
+                $filesbytimesplitting[$timesplittingid][$analysableid] = $file;
+            }
+        }
+
+        // We join the datasets by time splitting method.
+        $timesplittingfiles = $this->merge_analysable_files($filesbytimesplitting, $includetarget);
+
+        return $timesplittingfiles;
+    }
+
+    protected function merge_analysable_files($filesbytimesplitting, $includetarget) {
+
+        $timesplittingfiles = array();
+        foreach ($filesbytimesplitting as $timesplittingid => $files) {
+
+            if ($this->options['evaluation'] === true) {
+                // Delete the previous copy. Only when evaluating.
+                \core_analytics\dataset_manager::delete_previous_evaluation_file($this->modelid, $timesplittingid);
+            }
+
+            // Merge all course files into one.
+            $timesplittingfiles[$timesplittingid] = \core_analytics\dataset_manager::merge_datasets($files,
+                $this->modelid, $timesplittingid, $this->options['evaluation'], $includetarget);
+        }
+
+        return $timesplittingfiles;
+    }
+}
diff --git a/analytics/classes/local/analyser/courses.php b/analytics/classes/local/analyser/courses.php
new file mode 100644 (file)
index 0000000..9cb04d6
--- /dev/null
@@ -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/>.
+
+/**
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\analyser;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class courses extends sitewide {
+
+    public function get_samples_origin() {
+        return 'course';
+    }
+
+    protected function provided_sample_data() {
+        return array('course', 'context');
+    }
+
+    public function sample_access_context($sampleid) {
+        return \context_system::instance();
+    }
+
+    protected function get_all_samples(\core_analytics\analysable $site) {
+        global $DB;
+
+        // 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, '', '*');
+
+        $courseids = array_keys($courses);
+        $sampleids = array_combine($courseids, $courseids);
+
+        $courses = array_map(function($course) {
+            return array('course' => $course, 'context' => \context_course::instance($course->id));
+        }, $courses);
+
+        // No related data attached.
+        return array($sampleids, $courses);
+    }
+
+    public function get_samples($sampleids) {
+        global $DB;
+
+        list($sql, $params) = $DB->get_in_or_equal($sampleids, SQL_PARAMS_NAMED);
+        $courses = $DB->get_records_select('course', "id $sql", $params);
+
+        $courseids = array_keys($courses);
+        $sampleids = array_combine($courseids, $courseids);
+
+        $courses = array_map(function($course) {
+            return array('course' => $course, 'context' => \context_course::instance($course->id));
+        }, $courses);
+
+        // No related data attached.
+        return array($sampleids, $courses);
+    }
+
+    public function sample_description($sampleid, $contextid, $sampledata) {
+        $description = format_string($sampledata['course'], true, array('context' => $contextid));
+        return array($description, false);
+    }
+}
diff --git a/analytics/classes/local/analyser/sitewide.php b/analytics/classes/local/analyser/sitewide.php
new file mode 100644 (file)
index 0000000..3bae3bb
--- /dev/null
@@ -0,0 +1,66 @@
+<?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/>.
+
+/**
+ *
+ * @package   core_analytics
+ * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\analyser;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ *
+ * @package   core_analytics
+ * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class sitewide extends base {
+
+    protected function get_site() {
+        return new \core_analytics\site();
+    }
+
+    public function get_analysable_data($includetarget) {
+
+        // Here there is a single analysable and it is the system.
+        $analysable = $this->get_site();
+
+        $return = array();
+
+        $files = $this->process_analysable($analysable, $includetarget);
+
+        // Copy to range files as there is just one analysable.
+        // TODO Not abstracted as we should ideally directly store it as range-scope file.
+        foreach ($files as $timesplittingid => $file) {
+
+            if ($this->options['evaluation'] === true) {
+                // Delete the previous copy. Only when evaluating.
+                \core_analytics\dataset_manager::delete_previous_evaluation_file($this->modelid, $timesplittingid);
+            }
+
+            // We use merge but it is just a copy
+            // TODO use copy or move if there are performance issues.
+            $files[$timesplittingid] = \core_analytics\dataset_manager::merge_datasets(array($file), $this->modelid,
+                $timesplittingid, $this->options['evaluation'], $includetarget);
+        }
+
+        return $files;
+    }
+}
diff --git a/analytics/classes/local/analyser/student_enrolments.php b/analytics/classes/local/analyser/student_enrolments.php
new file mode 100644 (file)
index 0000000..322da74
--- /dev/null
@@ -0,0 +1,160 @@
+<?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/>.
+
+/**
+ *
+ * @package   core_analytics
+ * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\analyser;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot . '/lib/enrollib.php');
+
+/**
+ *
+ * @package   core_analytics
+ * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class student_enrolments extends by_course {
+
+    /**
+     * @var array Cache for user_enrolment id - course id relation.
+     */
+    protected $samplecourses = array();
+
+    protected function get_samples_origin() {
+        return 'user_enrolments';
+    }
+
+    public function sample_access_context($sampleid) {
+        return \context_course::instance($this->get_sample_course($sampleid));
+    }
+
+    protected function provided_sample_data() {
+        return array('user_enrolments', 'context', 'course', 'user');
+    }
+
+    /**
+     * All course enrolments.
+     *
+     * @param \core_analytics\analysable $course
+     * @return void
+     */
+    protected function get_all_samples(\core_analytics\analysable $course) {
+        global $DB;
+
+        // Using a custom SQL query because we want to include all course enrolments.
+        // TODO Review this is future as does not look ideal
+        // Although we load all the course users data in memory anyway, using recordsets we will
+        // not use the double of the memory required by the end of the iteration.
+        $sql = "SELECT ue.id AS enrolmentid, u.* FROM {user_enrolments} ue
+                  JOIN {enrol} e ON e.id = ue.enrolid
+                  JOIN {user} u ON ue.userid = u.id
+                  WHERE e.courseid = :courseid";
+        $enrolments = $DB->get_recordset_sql($sql, array('courseid' => $course->get_id()));
+
+        // We fetch all enrolments, but we are only interested in students.
+        $studentids = $course->get_students();
+
+        $samplesdata = array();
+        foreach ($enrolments as $user) {
+
+            if (empty($studentids[$user->id])) {
+                // Not a student.
+                continue;
+            }
+
+            $sampleid = $user->enrolmentid;
+            unset($user->enrolmentid);
+
+            $samplesdata[$sampleid]['course'] = $course->get_course_data();
+            $samplesdata[$sampleid]['context'] = $course->get_context();
+            $samplesdata[$sampleid]['user'] = $user;
+
+            // Fill the cache.
+            $this->samplecourses[$sampleid] = $course->get_id();
+        }
+        $enrolments->close();
+
+        $enrolids = array_keys($samplesdata);
+        return array(array_combine($enrolids, $enrolids), $samplesdata);
+    }
+
+    public function get_samples($sampleids) {
+        global $DB;
+
+        // Some course enrolments.
+        list($enrolsql, $params) = $DB->get_in_or_equal($sampleids, SQL_PARAMS_NAMED);
+
+        // Although we load all the course users data in memory anyway, using recordsets we will
+        // not use the double of the memory required by the end of the iteration.
+        $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 $user) {
+
+            $sampleid = $user->enrolmentid;
+            unset($user->enrolmentid);
+
+            // Enrolment samples are grouped by the course they belong to, so all $sampleids belong to the same
+            // course, $courseid and $coursemodinfo will only query the DB once and cache the course data in memory.
+            $courseid = $this->get_sample_course($sampleid);
+            $coursemodinfo = get_fast_modinfo($courseid);
+            $coursecontext = \context_course::instance($courseid);
+
+            $samplesdata[$sampleid]['course'] = $coursemodinfo->get_course();
+            $samplesdata[$sampleid]['context'] = $coursecontext;
+            $samplesdata[$sampleid]['user'] = $user;
+
+            // Fill the cache.
+            $this->samplecourses[$sampleid] = $coursemodinfo->get_course()->id;
+        }
+        $enrolments->close();
+
+        $enrolids = array_keys($samplesdata);
+        return array(array_combine($enrolids, $enrolids), $samplesdata);
+    }
+
+    protected function get_sample_course($sampleid) {
+        global $DB;
+
+        if (empty($this->samplecourses[$sampleid])) {
+            // TODO New function in enrollib.php.
+            $sql = "SELECT e.courseid
+                      FROM {enrol} e
+                      JOIN {user_enrolments} ue ON ue.enrolid = e.id
+                     WHERE ue.id = :userenrolmentid";
+
+            $this->samplecourses[$sampleid] = $DB->get_field_sql($sql, array('userenrolmentid' => $sampleid));
+        }
+
+        return $this->samplecourses[$sampleid];
+    }
+
+    public function sample_description($sampleid, $contextid, $sampledata) {
+        $description = fullname($sampledata['user'], true, array('context' => $contextid));
+        return array($description, new \user_picture($sampledata['user']));
+    }
+
+}
diff --git a/analytics/classes/local/indicator/activity_cognitive_depth.php b/analytics/classes/local/indicator/activity_cognitive_depth.php
new file mode 100644 (file)
index 0000000..ccd6b8b
--- /dev/null
@@ -0,0 +1,116 @@
+<?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/>.
+
+/**
+ * Cognitive depth abstract indicator.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Cognitive depth abstract indicator.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class activity_cognitive_depth extends community_of_inquiry_activity {
+
+    public function calculate_sample($sampleid, $tablename, $starttime = false, $endtime = false) {
+
+        // May not be available.
+        $user = $this->retrieve('user', $sampleid);
+
+        if (!$useractivities = $this->get_student_activities($sampleid, $tablename, $starttime, $endtime)) {
+            // Null if no activities.
+            return null;
+        }
+
+        $scoreperactivity = (self::get_max_value() - self::get_min_value()) / count($useractivities);
+
+        $score = self::get_min_value();
+
+        // Iterate through the module activities/resources which due date is part of this time range.
+        foreach ($useractivities as $contextid => $cm) {
+
+            $potentiallevel = $this->get_cognitive_depth_level($cm);
+            if (!is_int($potentiallevel) || $potentiallevel > 5 || $potentiallevel < 1) {
+                throw new \coding_exception('Activities\' potential level of engagement possible values go from 1 to 5.');
+            }
+            $scoreperlevel = $scoreperactivity / $potentiallevel;
+
+            switch ($potentiallevel) {
+                case 5:
+                    // Cognitive level 4 is to comment on feedback.
+                    if ($this->any_feedback('submitted', $cm, $contextid, $user)) {
+                        $score += $scoreperlevel * 5;
+                        break;
+                    }
+                    // The user didn't reach the activity max cognitive depth, continue with level 2.
+
+                case 4:
+                    // Cognitive level 4 is to comment on feedback.
+                    if ($this->any_feedback('replied', $cm, $contextid, $user)) {
+                        $score += $scoreperlevel * 4;
+                        break;
+                    }
+                    // The user didn't reach the activity max cognitive depth, continue with level 2.
+
+                case 3:
+                    // Cognitive level 3 is to view feedback.
+
+                    if ($this->any_feedback('viewed', $cm, $contextid, $user)) {
+                        // Max score for level 3.
+                        $score += $scoreperlevel * 3;
+                        break;
+                    }
+                    // The user didn't reach the activity max cognitive depth, continue with level 2.
+
+                case 2:
+                    // Cognitive depth level 2 is to submit content.
+
+                    if ($this->any_write_log($contextid, $user)) {
+                        $score += $scoreperlevel * 2;
+                        break;
+                    }
+                    // The user didn't reach the activity max cognitive depth, continue with level 1.
+
+                case 1:
+                    // Cognitive depth level 1 is just accessing the activity.
+
+                    if ($this->any_log($contextid, $user)) {
+                        $score += $scoreperlevel;
+                    }
+
+                default:
+            }
+        }
+
+        // To avoid decimal problems.
+        if ($score > self::MAX_VALUE) {
+            return self::MAX_VALUE;
+        } else if ($score < self::MIN_VALUE) {
+            return self::MIN_VALUE;
+        }
+        return $score;
+    }
+}
diff --git a/analytics/classes/local/indicator/activity_social_breadth.php b/analytics/classes/local/indicator/activity_social_breadth.php
new file mode 100644 (file)
index 0000000..11295e5
--- /dev/null
@@ -0,0 +1,67 @@
+<?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/>.
+
+/**
+ * Social breadth abstract indicator.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Social breadth abstract indicator.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class activity_social_breadth extends community_of_inquiry_activity {
+
+    public function calculate_sample($sampleid, $tablename, $starttime = false, $endtime = false) {
+
+        // May not be available.
+        $user = $this->retrieve('user', $sampleid);
+
+        if (!$useractivities = $this->get_student_activities($sampleid, $tablename, $starttime, $endtime)) {
+            // Null if no activities.
+            return null;
+        }
+
+        $scoreperactivity = (self::get_max_value() - self::get_min_value()) / count($useractivities);
+
+        $score = self::get_min_value();
+
+        foreach ($useractivities as $contextid => $cm) {
+            // TODO Add support for other levels than 1.
+            if ($this->any_log($contextid, $user)) {
+                $score += $scoreperactivity;
+            }
+        }
+
+        // To avoid decimal problems.
+        if ($score > self::MAX_VALUE) {
+            return self::MAX_VALUE;
+        } else if ($score < self::MIN_VALUE) {
+            return self::MIN_VALUE;
+        }
+        return $score;
+    }
+}
diff --git a/analytics/classes/local/indicator/any_access_after_end.php b/analytics/classes/local/indicator/any_access_after_end.php
new file mode 100644 (file)
index 0000000..c1cc78c
--- /dev/null
@@ -0,0 +1,61 @@
+<?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/>.
+
+/**
+ * Any access after the official end of the course.
+ *
+ * @package   core_analytics
+ * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Any access after the official end of the course.
+ *
+ * @package   core_analytics
+ * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class any_access_after_end extends binary {
+
+    public static function get_name() {
+        return get_string('indicator:accessesafterend', 'analytics');
+    }
+
+    public static function required_sample_data() {
+        return array('user', 'course', 'context');
+    }
+
+    public function calculate_sample($sampleid, $samplesorigin, $starttime = false, $endtime = false) {
+        global $DB;
+
+        $user = $this->retrieve('user', $sampleid);
+        $course = \core_analytics\course::instance($this->retrieve('course', $sampleid));
+
+        // Filter by context to use the db table index.
+        $context = $this->retrieve('context', $sampleid);
+        $select = "userid = :userid AND contextlevel = :contextlevel AND contextinstanceid = :contextinstanceid AND " .
+            "timecreated > :end";
+        $params = array('userid' => $user->id, 'contextlevel' => $context->contextlevel,
+            'contextinstanceid' => $context->instanceid, 'end' => $course->get_end());
+        return $DB->record_exists_select('logstore_standard_log', $select, $params) ? self::get_max_value() : self::get_min_value();
+
+    }
+}
diff --git a/analytics/classes/local/indicator/any_access_before_start.php b/analytics/classes/local/indicator/any_access_before_start.php
new file mode 100644 (file)
index 0000000..5ab2ad4
--- /dev/null
@@ -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/>.
+
+/**
+ * Any access before the official start of the course.
+ *
+ * @package   core_analytics
+ * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Any access before the official start of the course.
+ *
+ * @package   core_analytics
+ * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class any_access_before_start extends binary {
+
+    public static function get_name() {
+        return get_string('indicator:accessesbeforestart', 'analytics');
+    }
+
+    public static function required_sample_data() {
+        return array('user', 'course', 'context');
+    }
+
+    public function calculate_sample($sampleid, $samplesorigin, $starttime = false, $endtime = false) {
+        global $DB;
+
+        $user = $this->retrieve('user', $sampleid);
+        $course = \core_analytics\course::instance($this->retrieve('course', $sampleid));
+
+        // Filter by context to use the db table index.
+        $context = $this->retrieve('context', $sampleid);
+        $select = "userid = :userid AND contextlevel = :contextlevel AND contextinstanceid = :contextinstanceid AND " .
+            "timecreated < :start";
+        $params = array('userid' => $user->id, 'contextlevel' => $context->contextlevel,
+            'contextinstanceid' => $context->instanceid, 'start' => $course->get_start());
+        return $DB->record_exists_select('logstore_standard_log', $select, $params) ? self::get_max_value() : self::get_min_value();
+    }
+}
diff --git a/analytics/classes/local/indicator/any_write_action.php b/analytics/classes/local/indicator/any_write_action.php
new file mode 100644 (file)
index 0000000..2c1b173
--- /dev/null
@@ -0,0 +1,66 @@
+<?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/>.
+
+/**
+ * Write actions indicator.
+ *
+ * @package   core_analytics
+ * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Write actions indicator.
+ *
+ * @package   core_analytics
+ * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class any_write_action extends binary {
+
+    public static function get_name() {
+        return get_string('indicator:anywrite', 'analytics');
+    }
+
+    public static function required_sample_data() {
+        // User is not required, calculate_sample can handle its absence.
+        return array('context');
+    }
+
+    public function calculate_sample($sampleid, $sampleorigin, $starttime = false, $endtime = false) {
+        global $DB;
+
+        $select = '';
+        $params = array();
+
+        if ($user = $this->retrieve('user', $sampleid)) {
+            $select .= "userid = :userid AND ";
+            $params = $params + array('userid' => $user->id);
+        }
+
+        // Filter by context to use the db table index.
+        $context = $this->retrieve('context', $sampleid);
+        $select .= "contextlevel = :contextlevel AND contextinstanceid = :contextinstanceid AND " .
+            "(crud = 'c' OR crud = 'u') AND timecreated > :starttime AND timecreated <= :endtime";
+        $params = $params + array('contextlevel' => $context->contextlevel,
+            'contextinstanceid' => $context->instanceid, 'starttime' => $starttime, 'endtime' => $endtime);
+        return $DB->record_exists_select('logstore_standard_log', $select, $params) ? self::get_max_value() : self::get_min_value();
+    }
+}
diff --git a/analytics/classes/local/indicator/assign/cognitive_depth.php b/analytics/classes/local/indicator/assign/cognitive_depth.php
new file mode 100644 (file)
index 0000000..1744c3e
--- /dev/null
@@ -0,0 +1,67 @@
+<?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/>.
+
+/**
+ * Cognitive depth indicator - assign.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator\assign;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Cognitive depth indicator - assign.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cognitive_depth extends \core_analytics\local\indicator\activity_cognitive_depth {
+
+    public static function get_name() {
+        return get_string('indicator:cognitivedepthassign', 'analytics');
+    }
+
+    protected function get_activity_type() {
+        return 'assign';
+    }
+
+    protected function get_cognitive_depth_level(\cm_info $cm) {
+        return 5;
+    }
+
+    protected function feedback_viewed_events() {
+        return array('\mod_assign\event\feedback_viewed');
+    }
+
+    protected function feedback_submitted_events() {
+        return array('\mod_assign\event\assessable_submitted');
+    }
+
+    protected function feedback_replied(\cm_info $cm, $contextid, $userid, $after = false) {
+        // No level 4.
+        return false;
+    }
+
+    protected function feedback_check_grades() {
+        // We need the grade to be released to the student to consider that feedback has been provided.
+        return true;
+    }
+}
diff --git a/analytics/classes/local/indicator/assign/social_breadth.php b/analytics/classes/local/indicator/assign/social_breadth.php
new file mode 100644 (file)
index 0000000..db580db
--- /dev/null
@@ -0,0 +1,45 @@
+<?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/>.
+
+/**
+ * Social breadth indicator - assign.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator\assign;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Social breadth indicator - assign.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class social_breadth extends \core_analytics\local\indicator\activity_social_breadth {
+
+    public static function get_name() {
+        return get_string('indicator:socialbreadthassign', 'analytics');
+    }
+
+    protected function get_activity_type() {
+        return 'assign';
+    }
+}
diff --git a/analytics/classes/local/indicator/base.php b/analytics/classes/local/indicator/base.php
new file mode 100644 (file)
index 0000000..726fa17
--- /dev/null
@@ -0,0 +1,152 @@
+<?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/>.
+
+/**
+ * Abstract base indicator.
+ *
+ * @package   core_analytics
+ * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Abstract base indicator.
+ *
+ * @package   core_analytics
+ * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class base extends \core_analytics\calculable {
+
+    const MIN_VALUE = -1;
+
+    const MAX_VALUE = 1;
+
+    /**
+     * @var array[]
+     */
+    protected $sampledata = array();
+
+    /**
+     * Converts the calculated indicators to feature/s.
+     *
+     * @param float|int[] $calculatedvalues
+     * @return array
+     */
+    abstract protected function to_features($calculatedvalues);
+
+    /**
+     * Calculates the sample.
+     *
+     * Return a value from self::MIN_VALUE to self::MAX_VALUE or null if the indicator can not be calculated for this sample.
+     *
+     * @param int $sampleid
+     * @param string $sampleorigin
+     * @param integer $starttime Limit the calculation to this timestart
+     * @param integer $endtime Limit the calculation to this timeend
+     * @return float|null
+     */
+    abstract protected function calculate_sample($sampleid, $sampleorigin, $starttime, $endtime);
+
+    public function should_be_displayed($value, $subtype) {
+        // We should everything by default.
+        return true;
+    }
+
+    /**
+     * @return null|string
+     */
+    public static function required_sample_data() {
+        return null;
+    }
+
+    public static function instance() {
+        return new static();
+    }
+
+    public static function get_max_value() {
+        return self::MAX_VALUE;
+    }
+
+    public static function get_min_value() {
+        return self::MIN_VALUE;
+    }
+
+    /**
+     * Calculates the indicator.
+     *
+     * Returns an array of values which size matches $sampleids size.
+     *
+     * @param array $sampleids
+     * @param string $samplesorigin
+     * @param integer $starttime Limit the calculation to this timestart
+     * @param integer $endtime Limit the calculation to this timeend
+     * @return array The format to follow is [userid] = int|float[]
+     */
+    public function calculate($sampleids, $samplesorigin, $starttime = false, $endtime = false) {
+
+        if (!PHPUNIT_TEST && CLI_SCRIPT) {
+            echo '.';
+        }
+
+        $calculations = array();
+        foreach ($sampleids as $sampleid => $unusedsampleid) {
+
+            $calculatedvalue = $this->calculate_sample($sampleid, $samplesorigin, $starttime, $endtime);
+
+            if (!is_null($calculatedvalue) && ($calculatedvalue > self::MAX_VALUE || $calculatedvalue < self::MIN_VALUE)) {
+                throw new \coding_exception('Calculated values should be higher than ' . self::MIN_VALUE .
+                    ' and lower than ' . self::MAX_VALUE . ' ' . $calculatedvalue . ' received');
+            }
+
+            $calculations[$sampleid] = $calculatedvalue;
+        }
+
+        $calculations = $this->to_features($calculations);
+
+        return $calculations;
+    }
+
+    public function set_sample_data($data) {
+        $this->sampledata = $data;
+    }
+
+    protected function retrieve($tablename, $sampleid) {
+        if (empty($this->sampledata[$sampleid]) || empty($this->sampledata[$sampleid][$tablename])) {
+            // We don't throw an exception because indicators should be able to
+            // try multiple tables until they find something they can use.
+            return false;
+        }
+        return $this->sampledata[$sampleid][$tablename];
+    }
+
+    protected static function add_samples_averages() {
+        return false;
+    }
+
+    protected static function get_average_columns() {
+        return array('mean');
+    }
+
+    protected function calculate_averages($values) {
+        $mean = array_sum($values) / count($values);
+        return array($mean);
+    }
+}
diff --git a/analytics/classes/local/indicator/binary.php b/analytics/classes/local/indicator/binary.php
new file mode 100644 (file)
index 0000000..5827484
--- /dev/null
@@ -0,0 +1,81 @@
+<?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/>.
+
+/**
+ * Abstract binary indicator.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Abstract binary indicator.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class binary extends discrete {
+
+    public static final function get_classes() {
+        return array(0, 1);
+    }
+
+    public function get_display_value($value, $subtype = false) {
+
+        // No subtypes for binary values by default.
+        if ($value == -1) {
+            return get_string('no');
+        } else if ($value == 1) {
+            return get_string('yes');
+        } else {
+            throw new \moodle_exception('errorpredictionformat', 'analytics');
+        }
+    }
+
+    public function get_value_style($value, $subtype = false) {
+
+        // No subtypes for binary values by default.
+        if ($value == -1) {
+            return 'alert alert-warning';
+        } else if ($value == 1) {
+            return 'alert alert-info';
+        } else {
+            throw new \moodle_exception('errorpredictionformat', 'analytics');
+        }
+    }
+
+    public static function get_feature_headers() {
+        // Just 1 single feature obtained from the calculated value.
+        return array(get_called_class());
+    }
+
+    protected function to_features($calculatedvalues) {
+        // Indicators with binary values have only 1 feature for indicator, here we do nothing else
+        // than converting each sample scalar value to an array of scalars with 1 element.
+        array_walk($calculatedvalues, function(&$calculatedvalue) {
+            // Just return it as an array.
+            $calculatedvalue = array($calculatedvalue);
+        });
+
+        return $calculatedvalues;
+    }
+}
diff --git a/analytics/classes/local/indicator/book/cognitive_depth.php b/analytics/classes/local/indicator/book/cognitive_depth.php
new file mode 100644 (file)
index 0000000..2b4d735
--- /dev/null
@@ -0,0 +1,49 @@
+<?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/>.
+
+/**
+ * Cognitive depth indicator - book.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator\book;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Cognitive depth indicator - book.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cognitive_depth extends \core_analytics\local\indicator\activity_cognitive_depth {
+
+    public static function get_name() {
+        return get_string('indicator:cognitivedepthbook', 'analytics');
+    }
+
+    protected function get_activity_type() {
+        return 'book';
+    }
+
+    protected function get_cognitive_depth_level(\cm_info $cm) {
+        return 1;
+    }
+}
diff --git a/analytics/classes/local/indicator/book/social_breadth.php b/analytics/classes/local/indicator/book/social_breadth.php
new file mode 100644 (file)
index 0000000..f28312c
--- /dev/null
@@ -0,0 +1,45 @@
+<?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/>.
+
+/**
+ * Social breadth indicator - book.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator\book;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Social breadth indicator - book.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class social_breadth extends \core_analytics\local\indicator\activity_social_breadth {
+
+    public static function get_name() {
+        return get_string('indicator:socialbreadthbook', 'analytics');
+    }
+
+    protected function get_activity_type() {
+        return 'book';
+    }
+}
diff --git a/analytics/classes/local/indicator/chat/cognitive_depth.php b/analytics/classes/local/indicator/chat/cognitive_depth.php
new file mode 100644 (file)
index 0000000..1d48cf5
--- /dev/null
@@ -0,0 +1,98 @@
+<?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/>.
+
+/**
+ * Cognitive depth indicator - chat.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator\chat;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Cognitive depth indicator - chat.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cognitive_depth extends \core_analytics\local\indicator\activity_cognitive_depth {
+
+    public static function get_name() {
+        return get_string('indicator:cognitivedepthchat', 'analytics');
+    }
+
+    protected function get_activity_type() {
+        return 'chat';
+    }
+
+    public function get_cognitive_depth_level(\cm_info $cm) {
+        return 4;
+    }
+
+    protected function feedback_viewed_events() {
+        return array('\mod_chat\event\course_module_viewed', '\mod_chat\event\message_sent',
+            '\mod_chat\event\sessions_viewed');
+    }
+
+    protected function feedback_replied_events() {
+        return array('\mod_chat\event\message_sent');
+    }
+
+    protected function feedback_post_action(\cm_info $cm, $contextid, $userid, $eventnames, $after = false) {
+
+        if (empty($this->activitylogs[$contextid][$userid])) {
+            return false;
+        }
+
+        $logs = $this->activitylogs[$contextid][$userid];
+
+        if (empty($logs['\mod_chat\event\message_sent'])) {
+            // No feedback viewed if there is no submission.
+            return false;
+        }
+
+        // First user message time.
+        $firstmessage = $logs['\mod_chat\event\message_sent']->timecreated[0];
+
+        // We consider feedback another user messages.
+        foreach ($this->activitylogs[$contextid] as $anotheruserid => $logs) {
+            if ($anotheruserid == $userid) {
+                continue;
+            }
+            if (empty($logs['\mod_chat\event\message_sent'])) {
+                continue;
+            }
+            $firstmessagesenttime = $logs['\mod_chat\event\message_sent']->timecreated[0];
+
+            if (parent::feedback_post_action($cm, $contextid, $userid, $eventnames, $firstmessagesenttime)) {
+                return true;
+            }
+            // Continue with the next user.
+        }
+
+        return false;
+    }
+
+    protected function feedback_check_grades() {
+        // Chat's feedback is not contained in grades.
+        return false;
+    }
+}
diff --git a/analytics/classes/local/indicator/chat/social_breadth.php b/analytics/classes/local/indicator/chat/social_breadth.php
new file mode 100644 (file)
index 0000000..1fdafd0
--- /dev/null
@@ -0,0 +1,45 @@
+<?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/>.
+
+/**
+ * Social breadth indicator - chat.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator\chat;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Social breadth indicator - chat.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class social_breadth extends \core_analytics\local\indicator\activity_social_breadth {
+
+    public static function get_name() {
+        return get_string('indicator:socialbreadthchat', 'analytics');
+    }
+
+    protected function get_activity_type() {
+        return 'chat';
+    }
+}
diff --git a/analytics/classes/local/indicator/choice/cognitive_depth.php b/analytics/classes/local/indicator/choice/cognitive_depth.php
new file mode 100644 (file)
index 0000000..b1c5324
--- /dev/null
@@ -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/>.
+
+/**
+ * Cognitive depth indicator - choice.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator\choice;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Cognitive depth indicator - choice.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cognitive_depth extends \core_analytics\local\indicator\activity_cognitive_depth {
+
+    protected $choicedata = array();
+
+    public static function get_name() {
+        return get_string('indicator:cognitivedepthchoice', 'analytics');
+    }
+
+    protected function get_activity_type() {
+        return 'choice';
+    }
+
+    protected function get_cognitive_depth_level(\cm_info $cm) {
+        global $DB;
+
+        if (!isset($this->choicedata[$cm->instance])) {
+            $this->choicedata[$cm->instance] = $DB->get_record('choice', array('id' => $cm->instance), 'id, showresults, timeclose', MUST_EXIST);
+        }
+
+        if ($this->choicedata[$cm->instance]->showresults == 0 || $this->choicedata[$cm->instance]->showresults == 4) {
+            // Results are not shown to students or are always shown.
+            return 2;
+        }
+
+        return 3;
+    }
+
+    protected function any_feedback_view(\cm_info $cm, $contextid, $user) {
+
+        // If results are shown after they answer a write action counts as feedback viewed.
+        if ($this->choicedata[$cm->instance]->showresults == 1) {
+            return $this->any_write_log($contextid, $user);
+        }
+
+        if (empty($this->activitylogs[$contextid])) {
+            return false;
+        }
+
+        // Define the iteration, over all users if $user is set or a specific user.
+        $it = $this->activitylogs[$contextid];
+        if ($user) {
+            if (empty($this->activitylogs[$contextid][$user->id])) {
+                return false;
+            }
+            $it = array($user->id => $this->activitylogs[$contextid][$user->id]);
+        }
+
+        // Now we look for any log after the choice time close so we can confirm that the results were viewed.
+        foreach ($it as $userid => $logs) {
+            foreach ($logs as $log) {
+                foreach ($log->timecreated as $timecreated) {
+                    if ($timecreated >= $this->choicedata[$cm->instance]->timeclose) {
+                        return true;
+                    }
+                }
+            }
+        }
+
+        return false;
+    }
+}
diff --git a/analytics/classes/local/indicator/choice/social_breadth.php b/analytics/classes/local/indicator/choice/social_breadth.php
new file mode 100644 (file)
index 0000000..001f9db
--- /dev/null
@@ -0,0 +1,45 @@
+<?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/>.
+
+/**
+ * Social breadth indicator - choice.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator\choice;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Social breadth indicator - choice.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class social_breadth extends \core_analytics\local\indicator\activity_social_breadth {
+
+    public static function get_name() {
+        return get_string('indicator:socialbreadthchoice', 'analytics');
+    }
+
+    protected function get_activity_type() {
+        return 'choice';
+    }
+}
diff --git a/analytics/classes/local/indicator/community_of_inquiry_activity.php b/analytics/classes/local/indicator/community_of_inquiry_activity.php
new file mode 100644 (file)
index 0000000..83b83c2
--- /dev/null
@@ -0,0 +1,334 @@
+<?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/>.
+
+/**
+ * Community of inquire abstract indicator.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Community of inquire abstract indicator.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class community_of_inquiry_activity extends linear {
+
+    protected $course = null;
+    /**
+     * TODO This should ideally be reused by cognitive depth and social breadth.
+     *
+     * @var array Array of logs by [contextid][userid]
+     */
+    protected $activitylogs = null;
+
+    /**
+     * @var array Array of grades by [contextid][userid]
+     */
+    protected $grades = null;
+
+    /**
+     * TODO Automate this when merging into core.
+     * @var string The activity name (e.g. assign or quiz)
+     */
+    abstract protected function get_activity_type();
+
+    protected function get_cognitive_depth_level(\cm_info $cm) {
+        throw new \coding_exception('Overwrite get_cognitive_depth_level method to set your activity potential cognitive ' .
+            'depth level');
+    }
+
+    public static function required_sample_data() {
+        // Only course because the indicator is valid even without students.
+        return array('course');
+    }
+
+    protected final function any_log($contextid, $user) {
+        if (empty($this->activitylogs[$contextid])) {
+            return false;
+        }
+
+        // Someone interacted with the activity if there is no user or the user interacted with the
+        // activity if there is a user.
+        if (empty($user) ||
+                (!empty($user) && !empty($this->activitylogs[$contextid][$user->id]))) {
+            return true;
+        }
+
+        return false;
+    }
+
+    protected final function any_write_log($contextid, $user) {
+        if (empty($this->activitylogs[$contextid])) {
+            return false;
+        }
+
+        // No specific user, we look at all activity logs.
+        $it = $this->activitylogs[$contextid];
+        if ($user) {
+            if (empty($this->activitylogs[$contextid][$user->id])) {
+                return false;
+            }
+            $it = array($user->id => $this->activitylogs[$contextid][$user->id]);
+        }
+        foreach ($it as $logs) {
+            foreach ($logs as $log) {
+                if ($log->crud === 'c' || $log->crud === 'u') {
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    }
+
+    protected function any_feedback($action, \cm_info $cm, $contextid, $user) {
+        if (empty($this->activitylogs[$contextid])) {
+            return false;
+        }
+
+        if (empty($this->grades[$contextid]) && $this->feedback_check_grades()) {
+            // If there are no grades there is no feedback.
+            return false;
+        }
+
+        $it = $this->activitylogs[$contextid];
+        if ($user) {
+            if (empty($this->activitylogs[$contextid][$user->id])) {
+                return false;
+            }
+            $it = array($user->id => $this->activitylogs[$contextid][$user->id]);
+        }
+
+        foreach ($this->activitylogs[$contextid] as $userid => $logs) {
+            $methodname = 'feedback_' . $action;
+            if ($this->{$methodname}($cm, $contextid, $userid)) {
+                return true;
+            }
+            // If it wasn't viewed try with the next user.
+        }
+        return false;
+    }
+
+    /**
+     * $cm is used for this method overrides.
+     *
+     * This function must be fast.
+     *
+     * @param \cm_info $cm
+     * @param mixed $contextid
+     * @param mixed $userid
+     * @param int $after Timestamp, defaults to the graded date or false if we don't check the date.
+     * @return bool
+     */
+    protected function feedback_viewed(\cm_info $cm, $contextid, $userid, $after = null) {
+        return $this->feedback_post_action($cm, $contextid, $userid, $this->feedback_viewed_events(), $after);
+    }
+
+    protected function feedback_replied(\cm_info $cm, $contextid, $userid, $after = null) {
+        return $this->feedback_post_action($cm, $contextid, $userid, $this->feedback_replied_events(), $after);
+    }
+
+    protected function feedback_submitted(\cm_info $cm, $contextid, $userid, $after = null) {
+        return $this->feedback_post_action($cm, $contextid, $userid, $this->feedback_submitted_events(), $after);
+    }
+
+    protected function feedback_viewed_events() {
+        throw new \coding_exception('Activities with a potential cognitive level that include viewing feedback should define ' .
+            '"feedback_viewed_events" method or should override feedback_viewed method.');
+    }
+
+    protected function feedback_replied_events() {
+        throw new \coding_exception('Activities with a potential cognitive level that include replying to feedback should define ' .
+            '"feedback_replied_events" method or should override feedback_replied method.');
+    }
+
+    protected function feedback_submitted_events() {
+        throw new \coding_exception('Activities with a potential cognitive level that include viewing feedback should define ' .
+            '"feedback_submitted_events" method or should override feedback_submitted method.');
+    }
+
+    protected function feedback_post_action(\cm_info $cm, $contextid, $userid, $eventnames, $after = null) {
+        if ($after === null) {
+            if ($this->feedback_check_grades()) {
+                if (!$after = $this->get_graded_date($contextid, $userid)) {
+                    return false;
+                }
+            } else {
+                $after = false;
+            }
+        }
+
+        if (empty($this->activitylogs[$contextid][$userid])) {
+            return false;
+        }
+
+        foreach ($eventnames as $eventname) {
+            if (!$after) {
+                if (!empty($this->activitylogs[$contextid][$userid][$eventname])) {
+                    // If we don't care about when the feedback has been seen we consider this enough.
+                    return true;
+                }
+            } else {
+                if (empty($this->activitylogs[$contextid][$userid][$eventname])) {
+                    continue;
+                }
+                $timestamps = $this->activitylogs[$contextid][$userid][$eventname]->timecreated;
+                // Faster to start by the end.
+                rsort($timestamps);
+                foreach ($timestamps as $timestamp) {
+                    if ($timestamp > $after) {
+                        return true;
+                    }
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * get_graded_date
+     *
+     * @param int $contextid
+     * @param int $userid
+     * @param bool $checkfeedback Check that the student was graded or check that feedback was given
+     * @return int|false
+     */
+    protected function get_graded_date($contextid, $userid, $checkfeedback = false) {
+        if (empty($this->grades[$contextid][$userid])) {
+            return false;
+        }
+        foreach ($this->grades[$contextid][$userid] as $gradeitemid => $gradeitem) {
+
+            // We check that either feedback or the grade is set.
+            if (($checkfeedback && $gradeitem->feedback) || $gradeitem->grade) {
+
+                // Grab the first graded date.
+                if ($gradeitem->dategraded && (empty($after) || $gradeitem->dategraded < $after)) {
+                    $after = $gradeitem->dategraded;
+                }
+            }
+        }
+
+        if (!isset($after)) {
+            // False if there are no graded items.
+            return false;
+        }
+
+        return $after;
+    }
+
+    protected function get_student_activities($sampleid, $tablename, $starttime, $endtime) {
+
+        // May not be available.
+        $user = $this->retrieve('user', $sampleid);
+
+        if ($this->course === null) {
+            // The indicator scope is a range, so all activities belong to the same course.
+            $this->course = \core_analytics\course::instance($this->retrieve('course', $sampleid));
+        }
+
+        if ($this->activitylogs === null) {
+            // Fetch all activity logs in each activity in the course, not restricted to a specific sample so we can cache it.
+
+            $courseactivities = $this->course->get_all_activities($this->get_activity_type());
+
+            // Null if no activities of this type in this course.
+            if (empty($courseactivities)) {
+                $this->activitylogs = false;
+                return null;
+            }
+            $this->activitylogs = $this->fetch_activity_logs($courseactivities, $starttime, $endtime);
+        }
+
+        if ($this->grades === null) {
+            $courseactivities = $this->course->get_all_activities($this->get_activity_type());
+            $this->grades = $this->course->get_student_grades($courseactivities);
+        }
+
+        if ($cm = $this->retrieve('cm', $sampleid)) {
+            // Samples are at cm level or below.
+            $useractivities = array(\context_module::instance($cm->id)->id => $cm);
+        } else {
+            // All course activities.
+            $useractivities = $this->course->get_activities($this->get_activity_type(), $starttime, $endtime, $user);
+        }
+
+        return $useractivities;
+    }
+
+    protected function fetch_activity_logs($activities, $starttime = false, $endtime = false) {
+        global $DB;
+
+        // Filter by context to use the db table index.
+        list($contextsql, $contextparams) = $DB->get_in_or_equal(array_keys($activities), SQL_PARAMS_NAMED);
+
+        // Keeping memory usage as low as possible by using recordsets and storing only 1 log
+        // per contextid-userid-eventname + 1 timestamp for each of this combination records.
+        $fields = 'eventname, crud, contextid, contextlevel, contextinstanceid, userid, courseid';
+        $select = "contextid $contextsql AND timecreated > :starttime AND timecreated <= :endtime";
+        $sql = "SELECT $fields, timecreated " .
+            "FROM {logstore_standard_log} " .
+            "WHERE $select " .
+            "ORDER BY timecreated ASC";
+        $params = $contextparams + array('starttime' => $starttime, 'endtime' => $endtime);
+        $logs = $DB->get_recordset_sql($sql, $params);
+
+        // Returs the logs organised by contextid, userid and eventname so it is easier to calculate activities data later.
+        // At the same time we want to keep this array reasonably "not-massive".
+        $processedlogs = array();
+        foreach ($logs as $log) {
+            if (!isset($processedlogs[$log->contextid])) {
+                $processedlogs[$log->contextid] = array();
+            }
+            if (!isset($processedlogs[$log->contextid][$log->userid])) {
+                $processedlogs[$log->contextid][$log->userid] = array();
+            }
+
+            // contextid and userid have already been used to index the logs, the next field to index by is eventname:
+            // crud is unique per eventname, courseid is the same for all records and we append timecreated.
+            if (!isset($processedlogs[$log->contextid][$log->userid][$log->eventname])) {
+                $processedlogs[$log->contextid][$log->userid][$log->eventname] = $log;
+
+                // We want timecreated attribute to be an array containing all user access times.
+                $processedlogs[$log->contextid][$log->userid][$log->eventname]->timecreated = array(intval($log->timecreated));
+            } else {
+                // Add the event timecreated.
+                $processedlogs[$log->contextid][$log->userid][$log->eventname]->timecreated[] = intval($log->timecreated);
+            }
+        }
+        $logs->close();
+
+        return $processedlogs;
+    }
+
+    /**
+     * Whether grades should be checked or not when looking for feedback.
+     *
+     * @return void
+     */
+    protected function feedback_check_grades() {
+        return true;
+    }
+}
diff --git a/analytics/classes/local/indicator/data/cognitive_depth.php b/analytics/classes/local/indicator/data/cognitive_depth.php
new file mode 100644 (file)
index 0000000..a9c593c
--- /dev/null
@@ -0,0 +1,49 @@
+<?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/>.
+
+/**
+ * Cognitive depth indicator - data.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator\data;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Cognitive depth indicator - data.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cognitive_depth extends \core_analytics\local\indicator\activity_cognitive_depth {
+
+    public static function get_name() {
+        return get_string('indicator:cognitivedepthdata', 'analytics');
+    }
+
+    protected function get_activity_type() {
+        return 'data';
+    }
+
+    public function get_cognitive_depth_level(\cm_info $cm) {
+        return 2;
+    }
+}
diff --git a/analytics/classes/local/indicator/data/social_breadth.php b/analytics/classes/local/indicator/data/social_breadth.php
new file mode 100644 (file)
index 0000000..c3d617b
--- /dev/null
@@ -0,0 +1,45 @@
+<?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/>.
+
+/**
+ * Social breadth indicator - data.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator\data;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Social breadth indicator - data.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class social_breadth extends \core_analytics\local\indicator\activity_social_breadth {
+
+    public static function get_name() {
+        return get_string('indicator:socialbreadthdata', 'analytics');
+    }
+
+    protected function get_activity_type() {
+        return 'data';
+    }
+}
diff --git a/analytics/classes/local/indicator/discrete.php b/analytics/classes/local/indicator/discrete.php
new file mode 100644 (file)
index 0000000..b1b6168
--- /dev/null
@@ -0,0 +1,114 @@
+<?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/>.
+
+/**
+ * Abstract discrete indicator.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Abstract discrete indicator.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class discrete extends base {
+
+    /**
+     * Classes need to be defined so they can be converted internally to individual dataset features.
+     *
+     * @return string[]
+     */
+    protected static function get_classes() {
+        throw new \coding_exception('Please overwrite get_classes() specifying your discrete-values\' indicator classes');
+    }
+
+    public static function get_feature_headers() {
+        $fullclassname = get_called_class();
+
+        $headers = array($fullclassname);
+        foreach (self::get_classes() as $class) {
+            $headers[] = $fullclassname . '/' . $class;
+        }
+
+        return $headers;
+    }
+
+    public function should_be_displayed($value, $subtype) {
+        if ($value != static::get_max_value()) {
+            // Discrete values indicators are converted internally to 1 feature per indicator, we are only interested
+            // in showing the feature flagged with the max value.
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Returns the value to display when the prediction is $value.
+     *
+     * @param mixed $value
+     * @param string $subtype
+     * @return void
+     */
+    public function get_display_value($value, $subtype) {
+
+        $displayvalue = array_search($subtype, static::get_classes());
+
+        debugging('Please overwrite \core_analytics\local\indicator\discrete::get_display_value to show something ' .
+            'different than the default "' . $displayvalue . '"', DEBUG_DEVELOPER);
+
+        return $displayvalue;
+    }
+
+    public function get_display_style($value, $subtype) {
+        // No style attached to indicators classes, they are what they are, a cat,
+        // a horse or a sandwich, they are not good or bad.
+        return '';
+    }
+
+    protected function to_features($calculatedvalues) {
+
+        $classes = self::get_classes();
+
+        foreach ($calculatedvalues as $sampleid => $calculatedvalue) {
+
+            $classindex = array_search($calculatedvalue, $classes, true);
+
+            if (!$classindex) {
+                throw new \coding_exception(get_class($this) . ' calculated "' . $calculatedvalue .
+                    '" which is not one of its defined classes (' . json_encode($classes) . ')');
+            }
+
+            // We transform the calculated value into multiple features, one for each of the possible classes.
+            $features = array_fill(0, count($classes), 0);
+
+            // 1 to the selected value.
+            $features[$classindex] = 1;
+
+            $calculatedvalues[$sampleid] = $features;
+        }
+
+        return $calculatedvalues;
+    }
+}
diff --git a/analytics/classes/local/indicator/feedback/cognitive_depth.php b/analytics/classes/local/indicator/feedback/cognitive_depth.php
new file mode 100644 (file)
index 0000000..526e8fe
--- /dev/null
@@ -0,0 +1,69 @@
+<?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/>.
+
+/**
+ * Cognitive depth indicator - feedback.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator\feedback;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Cognitive depth indicator - feedback.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cognitive_depth extends \core_analytics\local\indicator\activity_cognitive_depth {
+
+    /**
+     * @var int[] Tiny cache to hold feedback instance - publish_stats field relation.
+     */
+    protected $publishstats = array();
+
+    public static function get_name() {
+        return get_string('indicator:cognitivedepthfeedback', 'analytics');
+    }
+
+    protected function get_activity_type() {
+        return 'feedback';
+    }
+
+    protected function get_cognitive_depth_level(\cm_info $cm) {
+        global $DB;
+
+        if (!isset($this->publishstats[$cm->instance])) {
+            $this->publishstats[$cm->instance] = $DB->get_field('feedback', 'publish_stats', array('id' => $cm->instance));
+        }
+
+        if (!empty($this->publishstats[$cm->instance])) {
+            // If stats are published we count that the user viewed feedback.
+            return 3;
+        }
+        return 2;
+    }
+
+    protected function any_feedback_view(\cm_info $cm, $contextid, $user) {
+        // If stats are published any write action counts as viewed feedback.
+        return $this->any_write_log($contextid, $user);
+    }
+}
diff --git a/analytics/classes/local/indicator/feedback/social_breadth.php b/analytics/classes/local/indicator/feedback/social_breadth.php
new file mode 100644 (file)
index 0000000..c9dac7d
--- /dev/null
@@ -0,0 +1,45 @@
+<?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/>.
+
+/**
+ * Social breadth indicator - feedback.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator\feedback;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Social breadth indicator - feedback.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class social_breadth extends \core_analytics\local\indicator\activity_social_breadth {
+
+    public static function get_name() {
+        return get_string('indicator:socialbreadthfeedback', 'analytics');
+    }
+
+    protected function get_activity_type() {
+        return 'feedback';
+    }
+}
diff --git a/analytics/classes/local/indicator/folder/cognitive_depth.php b/analytics/classes/local/indicator/folder/cognitive_depth.php
new file mode 100644 (file)
index 0000000..d9e9679
--- /dev/null
@@ -0,0 +1,48 @@
+<?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/>.
+
+/**
+ * Cognitive depth indicator - folder.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator\folder;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Cognitive depth indicator - folder.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cognitive_depth extends \core_analytics\local\indicator\activity_cognitive_depth {
+
+    public static function get_name() {
+        return get_string('indicator:cognitivedepthfolder', 'analytics');
+    }
+
+    protected function get_activity_type() {
+        return 'folder';
+    }
+    protected function get_cognitive_depth_level(\cm_info $cm) {
+        return 1;
+    }
+}
diff --git a/analytics/classes/local/indicator/folder/social_breadth.php b/analytics/classes/local/indicator/folder/social_breadth.php
new file mode 100644 (file)
index 0000000..1911fcb
--- /dev/null
@@ -0,0 +1,45 @@
+<?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/>.
+
+/**
+ * Social breadth indicator - folder.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator\folder;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Social breadth indicator - folder.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class social_breadth extends \core_analytics\local\indicator\activity_social_breadth {
+
+    public static function get_name() {
+        return get_string('indicator:socialbreadthfolder', 'analytics');
+    }
+
+    protected function get_activity_type() {
+        return 'folder';
+    }
+}
diff --git a/analytics/classes/local/indicator/forum/cognitive_depth.php b/analytics/classes/local/indicator/forum/cognitive_depth.php
new file mode 100644 (file)
index 0000000..3ff2996
--- /dev/null
@@ -0,0 +1,97 @@
+<?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/>.
+
+/**
+ * Cognitive depth indicator - forum.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator\forum;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Cognitive depth indicator - forum.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cognitive_depth extends \core_analytics\local\indicator\activity_cognitive_depth {
+
+    public static function get_name() {
+        return get_string('indicator:cognitivedepthforum', 'analytics');
+    }
+
+    protected function get_activity_type() {
+        return 'forum';
+    }
+
+    public function get_cognitive_depth_level(\cm_info $cm) {
+        return 4;
+    }
+
+    protected function feedback_check_grades() {
+        return false;
+    }
+
+    protected function feedback_viewed_events() {
+        // We could add any forum event, but it will make feedback_post_action slower
+        return array('\mod_forum\event\assessable_uploaded', '\mod_forum\event\course_module_viewed', '\mod_forum\event\discussion_viewed');
+    }
+
+    protected function feedback_replied_events() {
+        return array('\mod_forum\event\assessable_uploaded');
+    }
+
+    protected function feedback_post_action(\cm_info $cm, $contextid, $userid, $eventnames, $after = false) {
+
+        if (empty($this->activitylogs[$contextid][$userid])) {
+            return false;
+        }
+
+        $logs = $this->activitylogs[$contextid][$userid];
+
+        if (empty($logs['\mod_forum\event\assessable_uploaded'])) {
+            // No feedback viewed if there is no submission.
+            return false;
+        }
+
+        // First user post time.
+        $firstpost = $logs['\mod_forum\event\assessable_uploaded']->timecreated[0];
+
+        // We consider feedback any other user post in any of this forum discussions.
+        foreach ($this->activitylogs[$contextid] as $anotheruserid => $logs) {
+            if ($anotheruserid == $userid) {
+                continue;
+            }
+            if (empty($logs['\mod_forum\event\assessable_uploaded'])) {
+                continue;
+            }
+            $firstpostsenttime = $logs['\mod_forum\event\assessable_uploaded']->timecreated[0];
+
+            if (parent::feedback_post_action($cm, $contextid, $userid, $eventnames, $firstpostsenttime)) {
+                return true;
+            }
+            // Continue with the next user.
+        }
+
+        return false;
+    }
+}
diff --git a/analytics/classes/local/indicator/forum/social_breadth.php b/analytics/classes/local/indicator/forum/social_breadth.php
new file mode 100644 (file)
index 0000000..ae6ebf0
--- /dev/null
@@ -0,0 +1,45 @@
+<?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/>.
+
+/**
+ * Social breadth indicator - forum.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator\forum;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Social breadth indicator - forum.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class social_breadth extends \core_analytics\local\indicator\activity_social_breadth {
+
+    public static function get_name() {
+        return get_string('indicator:socialbreadthforum', 'analytics');
+    }
+
+    protected function get_activity_type() {
+        return 'forum';
+    }
+}
diff --git a/analytics/classes/local/indicator/glossary/cognitive_depth.php b/analytics/classes/local/indicator/glossary/cognitive_depth.php
new file mode 100644 (file)
index 0000000..6231b4e
--- /dev/null
@@ -0,0 +1,49 @@
+<?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/>.
+
+/**
+ * Cognitive depth indicator - glossary.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator\glossary;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Cognitive depth indicator - glossary.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cognitive_depth extends \core_analytics\local\indicator\activity_cognitive_depth {
+
+    public static function get_name() {
+        return get_string('indicator:cognitivedepthglossary', 'analytics');
+    }
+
+    protected function get_activity_type() {
+        return 'glossary';
+    }
+
+    public function get_cognitive_depth_level(\cm_info $cm) {
+        return 2;
+    }
+}
diff --git a/analytics/classes/local/indicator/glossary/social_breadth.php b/analytics/classes/local/indicator/glossary/social_breadth.php
new file mode 100644 (file)
index 0000000..594fce1
--- /dev/null
@@ -0,0 +1,45 @@
+<?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/>.
+
+/**
+ * Social breadth indicator - glossary.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator\glossary;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Social breadth indicator - glossary.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class social_breadth extends \core_analytics\local\indicator\activity_social_breadth {
+
+    public static function get_name() {
+        return get_string('indicator:socialbreadthglossary', 'analytics');
+    }
+
+    protected function get_activity_type() {
+        return 'glossary';
+    }
+}
diff --git a/analytics/classes/local/indicator/imscp/cognitive_depth.php b/analytics/classes/local/indicator/imscp/cognitive_depth.php
new file mode 100644 (file)
index 0000000..99359cf
--- /dev/null
@@ -0,0 +1,50 @@
+<?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/>.
+
+/**
+ * Cognitive depth indicator - imscp.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator\imscp;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Cognitive depth indicator - imscp.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cognitive_depth extends \core_analytics\local\indicator\activity_cognitive_depth {
+
+    public static function get_name() {
+        return get_string('indicator:cognitivedepthimscp', 'analytics');
+    }
+
+    protected function get_activity_type() {
+        return 'imscp';
+    }
+
+    protected function get_cognitive_depth_level(\cm_info $cm) {
+        return 1;
+    }
+
+}
diff --git a/analytics/classes/local/indicator/imscp/social_breadth.php b/analytics/classes/local/indicator/imscp/social_breadth.php
new file mode 100644 (file)
index 0000000..d8dd7ec
--- /dev/null
@@ -0,0 +1,45 @@
+<?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/>.
+
+/**
+ * Social breadth indicator - imscp.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator\imscp;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Social breadth indicator - imscp.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class social_breadth extends \core_analytics\local\indicator\activity_social_breadth {
+
+    public static function get_name() {
+        return get_string('indicator:socialbreadthimscp', 'analytics');
+    }
+
+    protected function get_activity_type() {
+        return 'imscp';
+    }
+}
diff --git a/analytics/classes/local/indicator/label/cognitive_depth.php b/analytics/classes/local/indicator/label/cognitive_depth.php
new file mode 100644 (file)
index 0000000..5a2066a
--- /dev/null
@@ -0,0 +1,50 @@
+<?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/>.
+
+/**
+ * Cognitive depth indicator - label.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator\label;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Cognitive depth indicator - label.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cognitive_depth extends \core_analytics\local\indicator\activity_cognitive_depth {
+
+    public static function get_name() {
+        return get_string('indicator:cognitivedepthlabel', 'analytics');
+    }
+
+    protected function get_activity_type() {
+        return 'label';
+    }
+
+    protected function get_cognitive_depth_level(\cm_info $cm) {
+        return 1;
+    }
+
+}
diff --git a/analytics/classes/local/indicator/label/social_breadth.php b/analytics/classes/local/indicator/label/social_breadth.php
new file mode 100644 (file)
index 0000000..7c9ac5c
--- /dev/null
@@ -0,0 +1,45 @@
+<?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/>.
+
+/**
+ * Social breadth indicator - label.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator\label;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Social breadth indicator - label.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class social_breadth extends \core_analytics\local\indicator\activity_social_breadth {
+
+    public static function get_name() {
+        return get_string('indicator:socialbreadthlabel', 'analytics');
+    }
+
+    protected function get_activity_type() {
+        return 'label';
+    }
+}
diff --git a/analytics/classes/local/indicator/lesson/cognitive_depth.php b/analytics/classes/local/indicator/lesson/cognitive_depth.php
new file mode 100644 (file)
index 0000000..172a6c0
--- /dev/null
@@ -0,0 +1,74 @@
+<?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/>.
+
+/**
+ * Cognitive depth indicator - lesson.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator\lesson;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Cognitive depth indicator - lesson.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cognitive_depth extends \core_analytics\local\indicator\activity_cognitive_depth {
+
+    public static function get_name() {
+        return get_string('indicator:cognitivedepthlesson', 'analytics');
+    }
+
+    protected function get_activity_type() {
+        return 'lesson';
+    }
+
+    protected function get_cognitive_depth_level(\cm_info $cm) {
+        return 5;
+    }
+
+    protected function feedback_viewed_events() {
+        return array('\mod_lesson\event\lesson_ended');
+    }
+
+    protected function feedback_submitted(\cm_info $cm, $contextid, $userid, $after = false) {
+        if (empty($this->activitylogs[$contextid][$userid]) ||
+                empty($this->activitylogs[$contextid][$userid]['\mod_lesson\event\lesson_ended'])) {
+            return false;
+        }
+
+        // Multiple lesson attempts completed counts as submitted after feedback.
+        return (2 >= count($this->activitylogs[$contextid][$userid]['\mod_lesson\event\lesson_ended']));
+    }
+
+    protected function feedback_check_grades() {
+        // We don't need to check grades as we get the feedback while completing the activity.
+        return false;
+    }
+
+    protected function feedback_replied(\cm_info $cm, $contextid, $userid, $after = false) {
+        // No level 4.
+        return false;
+    }
+
+}
diff --git a/analytics/classes/local/indicator/lesson/social_breadth.php b/analytics/classes/local/indicator/lesson/social_breadth.php
new file mode 100644 (file)
index 0000000..6eb4d94
--- /dev/null
@@ -0,0 +1,45 @@
+<?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/>.
+
+/**
+ * Social breadth indicator - lesson.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator\lesson;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Social breadth indicator - lesson.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class social_breadth extends \core_analytics\local\indicator\activity_social_breadth {
+
+    public static function get_name() {
+        return get_string('indicator:socialbreadthlesson', 'analytics');
+    }
+
+    protected function get_activity_type() {
+        return 'lesson';
+    }
+}
diff --git a/analytics/classes/local/indicator/linear.php b/analytics/classes/local/indicator/linear.php
new file mode 100644 (file)
index 0000000..222df2b
--- /dev/null
@@ -0,0 +1,115 @@
+<?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/>.
+
+/**
+ * Abstract linear indicator.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Abstract linear indicator.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class linear extends base {
+
+    /**
+     * Set to false to avoid context features to be added as dataset features.
+     *
+     * @return bool
+     */
+    protected static function include_averages() {
+        return true;
+    }
+
+    public static function get_feature_headers() {
+
+        $fullclassname = get_called_class();
+
+        if (static::include_averages()) {
+            // The calculated value + context indicators.
+            $headers = array($fullclassname, $fullclassname . '/mean');
+        } else {
+            $headers = array($fullclassname);
+        }
+        return $headers;
+    }
+
+    public function should_be_displayed($value, $subtype) {
+        if ($subtype != false) {
+            return false;
+        }
+        return true;
+    }
+
+    public function get_display_value($value, $subtype = false) {
+        $diff = static::get_max_value() - static::get_min_value();
+        return round(100 * ($value - static::get_min_value()) / $diff) . '%';
+    }
+
+    public function get_value_style($value, $subtype = false) {
+        if ($value < 0) {
+            return 'alert alert-warning';
+        } else {
+            return 'alert alert-info';
+        }
+    }
+
+    protected function to_features($calculatedvalues) {
+
+        // Null mean if all calculated values are null.
+        $nullmean = true;
+        foreach ($calculatedvalues as $value) {
+            if (!is_null($value)) {
+                // Early break, we don't want to spend a lot of time here.
+                $nullmean = false;
+                break;
+            }
+        }
+
+        if ($nullmean) {
+            $mean = null;
+        } else {
+            $mean = round(array_sum($calculatedvalues) / count($calculatedvalues), 2);
+        }
+
+        foreach ($calculatedvalues as $sampleid => $calculatedvalue) {
+
+            if (!is_null($calculatedvalue)) {
+                $calculatedvalue = round($calculatedvalue, 2);
+            }
+
+            if (static::include_averages()) {
+                $calculatedvalues[$sampleid] = array($calculatedvalue, $mean);
+            } else {
+                // Basically just convert the scalar to an array of scalars with a single value.
+                $calculatedvalues[$sampleid] = array($calculatedvalue);
+            }
+        }
+
+        // Returns each sample as an array of values, appending the mean to the calculated value.
+        return $calculatedvalues;
+    }
+}
diff --git a/analytics/classes/local/indicator/lti/cognitive_depth.php b/analytics/classes/local/indicator/lti/cognitive_depth.php
new file mode 100644 (file)
index 0000000..ed57db4
--- /dev/null
@@ -0,0 +1,55 @@
+<?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/>.
+
+/**
+ * Cognitive depth indicator - lti.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator\lti;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Cognitive depth indicator - lti.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cognitive_depth extends \core_analytics\local\indicator\activity_cognitive_depth {
+
+    public static function get_name() {
+        return get_string('indicator:cognitivedepthlti', 'analytics');
+    }
+
+    protected function get_activity_type() {
+        return 'lti';
+    }
+
+    public function get_cognitive_depth_level(\cm_info $cm) {
+        return 3;
+    }
+
+    protected function feedback_viewed_events() {
+        // Any view after the data graded counts as feedback viewed.
+        return array('\mod_lti\event\course_module_viewed');
+    }
+
+}
diff --git a/analytics/classes/local/indicator/lti/social_breadth.php b/analytics/classes/local/indicator/lti/social_breadth.php
new file mode 100644 (file)
index 0000000..4304bd3
--- /dev/null
@@ -0,0 +1,45 @@
+<?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/>.
+
+/**
+ * Social breadth indicator - lti.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator\lti;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Social breadth indicator - lti.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class social_breadth extends \core_analytics\local\indicator\activity_social_breadth {
+
+    public static function get_name() {
+        return get_string('indicator:socialbreadthlti', 'analytics');
+    }
+
+    protected function get_activity_type() {
+        return 'lti';
+    }
+}
diff --git a/analytics/classes/local/indicator/page/cognitive_depth.php b/analytics/classes/local/indicator/page/cognitive_depth.php
new file mode 100644 (file)
index 0000000..f6318ff
--- /dev/null
@@ -0,0 +1,49 @@
+<?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/>.
+
+/**
+ * Cognitive depth indicator - page.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator\page;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Cognitive depth indicator - page.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cognitive_depth extends \core_analytics\local\indicator\activity_cognitive_depth {
+
+    public static function get_name() {
+        return get_string('indicator:cognitivedepthpage', 'analytics');
+    }
+
+    protected function get_activity_type() {
+        return 'page';
+    }
+
+    protected function get_cognitive_depth_level(\cm_info $cm) {
+        return 1;
+    }
+}
diff --git a/analytics/classes/local/indicator/page/social_breadth.php b/analytics/classes/local/indicator/page/social_breadth.php
new file mode 100644 (file)
index 0000000..2333aa9
--- /dev/null
@@ -0,0 +1,45 @@
+<?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/>.
+
+/**
+ * Social breadth indicator - page.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator\page;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Social breadth indicator - page.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class social_breadth extends \core_analytics\local\indicator\activity_social_breadth {
+
+    public static function get_name() {
+        return get_string('indicator:socialbreadthpage', 'analytics');
+    }
+
+    protected function get_activity_type() {
+        return 'page';
+    }
+}
diff --git a/analytics/classes/local/indicator/quiz/cognitive_depth.php b/analytics/classes/local/indicator/quiz/cognitive_depth.php
new file mode 100644 (file)
index 0000000..22ea141
--- /dev/null
@@ -0,0 +1,67 @@
+<?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/>.
+
+/**
+ * Cognitive depth indicator - quiz.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator\quiz;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Cognitive depth indicator - quiz.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cognitive_depth extends \core_analytics\local\indicator\activity_cognitive_depth {
+
+    public static function get_name() {
+        return get_string('indicator:cognitivedepthquiz', 'analytics');
+    }
+
+    protected function get_activity_type() {
+        return 'quiz';
+    }
+
+    public function get_cognitive_depth_level(\cm_info $cm) {
+        return 5;
+    }
+
+    protected function feedback_check_grades() {
+        // We need the grade to be released to the student to consider that feedback has been provided.
+        return true;
+    }
+
+    protected function feedback_viewed_events() {
+        return array('\mod_quiz\event\course_module_viewed');
+    }
+
+    protected function feedback_submitted_events() {
+        return array('\mod_quiz\event\attempt_submitted');
+    }
+
+    protected function feedback_replied(\cm_info $cm, $contextid, $userid, $after = false) {
+        // No level 4.
+        return false;
+    }
+}
diff --git a/analytics/classes/local/indicator/quiz/social_breadth.php b/analytics/classes/local/indicator/quiz/social_breadth.php
new file mode 100644 (file)
index 0000000..604d9d9
--- /dev/null
@@ -0,0 +1,45 @@
+<?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/>.
+
+/**
+ * Social breadth indicator - quiz.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator\quiz;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Social breadth indicator - quiz.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class social_breadth extends \core_analytics\local\indicator\activity_social_breadth {
+
+    public static function get_name() {
+        return get_string('indicator:socialbreadthquiz', 'analytics');
+    }
+
+    protected function get_activity_type() {
+        return 'quiz';
+    }
+}
diff --git a/analytics/classes/local/indicator/read_actions.php b/analytics/classes/local/indicator/read_actions.php
new file mode 100644 (file)
index 0000000..36f4553
--- /dev/null
@@ -0,0 +1,87 @@
+<?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/>.
+
+/**
+ * Read actions indicator.
+ *
+ * @package   core_analytics
+ * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Read actions indicator.
+ *
+ * @package   core_analytics
+ * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class read_actions extends linear {
+
+    public static function get_name() {
+        return get_string('indicator:readactions', 'analytics');
+    }
+
+    public static function required_sample_data() {
+        // User is not required, calculate_sample can handle its absence.
+        return array('context');
+    }
+
+    public function calculate_sample($sampleid, $sampleorigin, $starttime = false, $endtime = false) {
+        global $DB;
+
+        $select = '';
+        $params = array();
+
+        if ($user = $this->retrieve('user', $sampleid)) {
+            $select .= "userid = :userid AND ";
+            $params = $params + array('userid' => $user->id);
+        }
+
+        // Filter by context to use the db table index.
+        $context = $this->retrieve('context', $sampleid);
+        $select .= "contextlevel = :contextlevel AND contextinstanceid = :contextinstanceid AND " .
+            "crud = 'r' AND timecreated > :starttime AND timecreated <= :endtime";
+        $params = $params + array('contextlevel' => $context->contextlevel,
+            'contextinstanceid' => $context->instanceid, 'starttime' => $starttime, 'endtime' => $endtime);
+        $nrecords = $DB->count_records_select('logstore_standard_log', $select, $params);
+
+        // We define a list of ranges to fit $nrecords into it
+        // # Done absolutely nothing
+        // # Not much really, just accessing the course once a week
+        // # More than just accessing the course, some interaction
+        // # Significant contribution, will depend on the course anyway
+
+        // We need to adapt the limits to the time range duration.
+        $nweeks = $this->get_time_range_weeks_number($starttime, $endtime);
+
+        // Careful with this, depends on the course.
+        $limit = $nweeks * 3 * 10;
+        $ranges = array(
+            array('eq', 0),
+            // 1 course access per week (3 records are easily generated).
+            array('le', $nweeks * 3),
+            // 3 course accesses per week doing some stuff.
+            array('le', $limit),
+            array('gt', $limit)
+        );
+        return $this->classify_value($nrecords, $ranges);
+    }
+}
diff --git a/analytics/classes/local/indicator/resource/cognitive_depth.php b/analytics/classes/local/indicator/resource/cognitive_depth.php
new file mode 100644 (file)
index 0000000..7ba0eaa
--- /dev/null
@@ -0,0 +1,49 @@
+<?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/>.
+
+/**
+ * Cognitive depth indicator - resource.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator\resource;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Cognitive depth indicator - resource.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cognitive_depth extends \core_analytics\local\indicator\activity_cognitive_depth {
+
+    public static function get_name() {
+        return get_string('indicator:cognitivedepthresource', 'analytics');
+    }
+
+    protected function get_activity_type() {
+        return 'resource';
+    }
+
+    protected function get_cognitive_depth_level(\cm_info $cm) {
+        return 1;
+    }
+}
diff --git a/analytics/classes/local/indicator/resource/social_breadth.php b/analytics/classes/local/indicator/resource/social_breadth.php
new file mode 100644 (file)
index 0000000..5aea5c9
--- /dev/null
@@ -0,0 +1,45 @@
+<?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/>.
+
+/**
+ * Social breadth indicator - resource.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator\resource;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Social breadth indicator - resource.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class social_breadth extends \core_analytics\local\indicator\activity_social_breadth {
+
+    public static function get_name() {
+        return get_string('indicator:socialbreadthresource', 'analytics');
+    }
+
+    protected function get_activity_type() {
+        return 'resource';
+    }
+}
diff --git a/analytics/classes/local/indicator/scorm/cognitive_depth.php b/analytics/classes/local/indicator/scorm/cognitive_depth.php
new file mode 100644 (file)
index 0000000..bef36e7
--- /dev/null
@@ -0,0 +1,54 @@
+<?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/>.
+
+/**
+ * Cognitive depth indicator - scorm.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator\scorm;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Cognitive depth indicator - scorm.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cognitive_depth extends \core_analytics\local\indicator\activity_cognitive_depth {
+
+    public static function get_name() {
+        return get_string('indicator:cognitivedepthscorm', 'analytics');
+    }
+
+    protected function get_activity_type() {
+        return 'scorm';
+    }
+
+    public function get_cognitive_depth_level(\cm_info $cm) {
+        return 3;
+    }
+
+    protected function feedback_viewed_events() {
+        // Any view after the data graded counts as feedback viewed.
+        return array('\mod_scorm\event\course_module_viewed');
+    }
+}
diff --git a/analytics/classes/local/indicator/scorm/social_breadth.php b/analytics/classes/local/indicator/scorm/social_breadth.php
new file mode 100644 (file)
index 0000000..8e1da06
--- /dev/null
@@ -0,0 +1,45 @@
+<?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/>.
+
+/**
+ * Social breadth indicator - scorm.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator\scorm;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Social breadth indicator - scorm.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class social_breadth extends \core_analytics\local\indicator\activity_social_breadth {
+
+    public static function get_name() {
+        return get_string('indicator:socialbreadthscorm', 'analytics');
+    }
+
+    protected function get_activity_type() {
+        return 'scorm';
+    }
+}
diff --git a/analytics/classes/local/indicator/survey/cognitive_depth.php b/analytics/classes/local/indicator/survey/cognitive_depth.php
new file mode 100644 (file)
index 0000000..686bfa6
--- /dev/null
@@ -0,0 +1,49 @@
+<?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/>.
+
+/**
+ * Cognitive depth indicator - survey.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator\survey;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Cognitive depth indicator - survey.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cognitive_depth extends \core_analytics\local\indicator\activity_cognitive_depth {
+
+    public static function get_name() {
+        return get_string('indicator:cognitivedepthsurvey', 'analytics');
+    }
+
+    protected function get_activity_type() {
+        return 'survey';
+    }
+
+    protected function get_cognitive_depth_level(\cm_info $cm) {
+        return 2;
+    }
+}
diff --git a/analytics/classes/local/indicator/survey/social_breadth.php b/analytics/classes/local/indicator/survey/social_breadth.php
new file mode 100644 (file)
index 0000000..21099bc
--- /dev/null
@@ -0,0 +1,45 @@
+<?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/>.
+
+/**
+ * Social breadth indicator - survey.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator\survey;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Social breadth indicator - survey.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class social_breadth extends \core_analytics\local\indicator\activity_social_breadth {
+
+    public static function get_name() {
+        return get_string('indicator:socialbreadthsurvey', 'analytics');
+    }
+
+    protected function get_activity_type() {
+        return 'survey';
+    }
+}
diff --git a/analytics/classes/local/indicator/url/cognitive_depth.php b/analytics/classes/local/indicator/url/cognitive_depth.php
new file mode 100644 (file)
index 0000000..9cf32d9
--- /dev/null
@@ -0,0 +1,49 @@
+<?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/>.
+
+/**
+ * Cognitive depth indicator - url.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator\url;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Cognitive depth indicator - url.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cognitive_depth extends \core_analytics\local\indicator\activity_cognitive_depth {
+
+    public static function get_name() {
+        return get_string('indicator:cognitivedepthurl', 'analytics');
+    }
+
+    protected function get_activity_type() {
+        return 'url';
+    }
+
+    protected function get_cognitive_depth_level(\cm_info $cm) {
+        return 1;
+    }
+}
diff --git a/analytics/classes/local/indicator/url/social_breadth.php b/analytics/classes/local/indicator/url/social_breadth.php
new file mode 100644 (file)
index 0000000..c064146
--- /dev/null
@@ -0,0 +1,45 @@
+<?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/>.
+
+/**
+ * Social breadth indicator - url.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator\url;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Social breadth indicator - url.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class social_breadth extends \core_analytics\local\indicator\activity_social_breadth {
+
+    public static function get_name() {
+        return get_string('indicator:socialbreadthurl', 'analytics');
+    }
+
+    protected function get_activity_type() {
+        return 'url';
+    }
+}
diff --git a/analytics/classes/local/indicator/user_profile_set.php b/analytics/classes/local/indicator/user_profile_set.php
new file mode 100644 (file)
index 0000000..e8af023
--- /dev/null
@@ -0,0 +1,81 @@
+<?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/>.
+
+/**
+ * User profile set indicator.
+ *
+ * @package   core_analytics
+ * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * User profile set indicator.
+ *
+ * @package   core_analytics
+ * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class user_profile_set extends linear {
+
+
+    public static function get_name() {
+        return get_string('indicator:completeduserprofile', 'analytics');
+    }
+
+    public static function required_sample_data() {
+        return array('user');
+    }
+
+    public function calculate_sample($sampleid, $sampleorigin, $starttime = false, $endtime = false) {
+        global $CFG;
+
+        $user = $this->retrieve('user', $sampleid);
+
+        // Nothing set results in -1.
+        $calculatedvalue = self::MIN_VALUE;
+
+        if (!empty($CFG->sitepolicy) && !$user->policyagreed) {
+            return self::MIN_VALUE;
+        }
+
+        if (!$user->confirmed) {
+            return self::MIN_VALUE;
+        }
+
+        if ($user->description != '') {
+            $calculatedvalue += 1;
+        }
+
+        if ($user->picture != '') {
+            $calculatedvalue += 1;
+        }
+
+        // 0.2 for any of the following fields being set (some of them may even be compulsory or have a default).
+        $fields = array('institution', 'department', 'address', 'city', 'country', 'url');
+        foreach ($fields as $fieldname) {
+            if ($user->{$fieldname} != '') {
+                $calculatedvalue += 0.2;
+            }
+        }
+
+        return $this->limit_value($calculatedvalue);
+    }
+}
diff --git a/analytics/classes/local/indicator/user_track_forums.php b/analytics/classes/local/indicator/user_track_forums.php
new file mode 100644 (file)
index 0000000..60569f6
--- /dev/null
@@ -0,0 +1,53 @@
+<?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/>.
+
+/**
+ * User tracks forums indicator.
+ *
+ * @package   core_analytics
+ * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * User tracks forums indicator.
+ *
+ * @package   core_analytics
+ * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class user_track_forums extends binary {
+
+    public static function get_name() {
+        return get_string('indicator:userforumstracking', 'analytics');
+    }
+
+    public static function required_sample_data() {
+        return array('user');
+    }
+
+    public function calculate_sample($sampleid, $samplesorigin, $starttime = false, $endtime = false) {
+
+        $user = $this->retrieve('user', $sampleid);
+
+        // TODO Return null if forums tracking is the default.
+        return ($user->trackforums) ? self::get_max_value() : self::get_min_value();
+    }
+}
diff --git a/analytics/classes/local/indicator/wiki/cognitive_depth.php b/analytics/classes/local/indicator/wiki/cognitive_depth.php
new file mode 100644 (file)
index 0000000..b02da88
--- /dev/null
@@ -0,0 +1,49 @@
+<?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/>.
+
+/**
+ * Cognitive depth indicator - wiki.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator\wiki;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Cognitive depth indicator - wiki.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cognitive_depth extends \core_analytics\local\indicator\activity_cognitive_depth {
+
+    public static function get_name() {
+        return get_string('indicator:cognitivedepthwiki', 'analytics');
+    }
+
+    protected function get_activity_type() {
+        return 'wiki';
+    }
+
+    public function get_cognitive_depth_level(\cm_info $cm) {
+        return 2;
+    }
+}
diff --git a/analytics/classes/local/indicator/wiki/social_breadth.php b/analytics/classes/local/indicator/wiki/social_breadth.php
new file mode 100644 (file)
index 0000000..4323b91
--- /dev/null
@@ -0,0 +1,45 @@
+<?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/>.
+
+/**
+ * Social breadth indicator - wiki.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator\wiki;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Social breadth indicator - wiki.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class social_breadth extends \core_analytics\local\indicator\activity_social_breadth {
+
+    public static function get_name() {
+        return get_string('indicator:socialbreadthwiki', 'analytics');
+    }
+
+    protected function get_activity_type() {
+        return 'wiki';
+    }
+}
diff --git a/analytics/classes/local/indicator/workshop/cognitive_depth.php b/analytics/classes/local/indicator/workshop/cognitive_depth.php
new file mode 100644 (file)
index 0000000..556b93c
--- /dev/null
@@ -0,0 +1,67 @@
+<?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/>.
+
+/**
+ * Cognitive depth indicator - workshop.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator\workshop;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Cognitive depth indicator - workshop.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cognitive_depth extends \core_analytics\local\indicator\activity_cognitive_depth {
+
+    public static function get_name() {
+        return get_string('indicator:cognitivedepthworkshop', 'analytics');
+    }
+
+    protected function get_activity_type() {
+        return 'workshop';
+    }
+
+    public function get_cognitive_depth_level(\cm_info $cm) {
+        return 5;
+    }
+
+    protected function feedback_check_grades() {
+        return true;
+    }
+
+    protected function feedback_viewed_events() {
+        return array('\mod_workshop\event\course_module_viewed', '\mod_workshop\event\submission_viewed');
+    }
+
+    protected function feedback_replied_events() {
+        return array('\mod_workshop\event\submission_assessed', '\mod_workshop\event\submission_reassessed');
+    }
+
+    protected function feedback_submitted_events() {
+        // Can't use assessable_uploaded instead of submission_* as mod_workshop only triggers it during submission_updated
+        return array('\mod_workshop\event\submission_updated', '\mod_workshop\event\submission_created',
+            '\mod_workshop\event\submission_reassessed');
+    }
+}
diff --git a/analytics/classes/local/indicator/workshop/social_breadth.php b/analytics/classes/local/indicator/workshop/social_breadth.php
new file mode 100644 (file)
index 0000000..6e3c462
--- /dev/null
@@ -0,0 +1,45 @@
+<?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/>.
+
+/**
+ * Social breadth indicator - workshop.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\indicator\workshop;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Social breadth indicator - workshop.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class social_breadth extends \core_analytics\local\indicator\activity_social_breadth {
+
+    public static function get_name() {
+        return get_string('indicator:socialbreadthworkshop', 'analytics');
+    }
+
+    protected function get_activity_type() {
+        return 'workshop';
+    }
+}
diff --git a/analytics/classes/local/target/base.php b/analytics/classes/local/target/base.php
new file mode 100644 (file)
index 0000000..05a273c
--- /dev/null
@@ -0,0 +1,242 @@
+<?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/>.
+
+/**
+ * Abstract base target.
+ *
+ * @package   core_analytics
+ * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\target;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Abstract base target.
+ *
+ * @package   core_analytics
+ * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class base extends \core_analytics\calculable {
+
+    /**
+     * This target have linear or discrete values.
+     *
+     * @return bool
+     */
+    abstract public function is_linear();
+
+    /**
+     * Returns the analyser class that should be used along with this target.
+     *
+     * @return string
+     */
+    abstract public function get_analyser_class();
+
+    /**
+     * Allows the target to verify that the analysable is a good candidate.
+     *
+     * This method can be used as a quick way to discard invalid analysables.
+     * e.g. Imagine that your analysable don't have students and you need them.
+     *
+     * @param \core_analytics\analysable $analysable
+     * @param bool $fortraining
+     * @return true|string
+     */
+    abstract public function is_valid_analysable(\core_analytics\analysable $analysable, $fortraining = true);
+
+    /**
+     * Calculates this target for the provided samples.
+     *
+     * In case there are no values to return or the provided sample is not applicable just return null.
+     *
+     * @param int $sample
+     * @param \core_analytics\analysable $analysable
+     * @return float|null
+     */
+    abstract protected function calculate_sample($sampleid, \core_analytics\analysable $analysable);
+
+    public function prediction_actions(\core_analytics\prediction $prediction) {
+        global $PAGE;
+
+        $predictionurl = new \moodle_url('/report/insights/prediction.php',
+            array('id' => $prediction->get_prediction_data()->id));
+        if ($predictionurl->compare($PAGE->url)) {
+            // We don't show the link to prediction.php if we are already in prediction.php
+            // prediction.php's $PAGE->set_url call is prior to any core_analytics namespace method call.
+            return array();
+        }
+
+        return array('predictiondetails' => new \core_analytics\prediction_action('predictiondetails', $prediction, $predictionurl,
+            new \pix_icon('t/preview', get_string('viewprediction', 'analytics')),
+            get_string('viewprediction', 'analytics'))
+        );
+    }
+
+    public function get_display_value($value) {
+        return $value;
+    }
+
+    public function get_value_style($value) {
+        throw new \coding_exception('Please overwrite \core_analytics\local\target\base::get_value_style');
+    }
+
+    /**
+     * Callback to execute once a prediction has been returned from the predictions processor.
+     *
+     * @param int $sampleid
+     * @param float|int $prediction
+     * @param float $predictionscore
+     * @return void
+     */
+    public function prediction_callback($modelid, $sampleid, $samplecontext, $prediction, $predictionscore) {
+        return;
+    }
+
+    public function generate_insights($modelid, $samplecontexts) {
+        global $CFG;
+
+        foreach ($samplecontexts as $context) {
+
+            $insightinfo = new \stdClass();
+            $insightinfo->insightname = $this->get_name();
+            $insightinfo->contextname = $context->get_context_name();
+            $subject = get_string('insightmessagesubject', 'analytics', $insightinfo);
+
+            if ($context->contextlevel >= CONTEXT_COURSE) {
+                // Course level notification.
+                $users = get_enrolled_users($context, 'moodle/analytics:listinsights');
+            } else {
+                $users = get_users_by_capability($context, 'moodle/analytics:listinsights');
+            }
+
+            if (!$coursecontext = $context->get_course_context(false)) {
+                $coursecontext = \context_course::instance(SITEID);
+            }
+
+            foreach ($users as $user) {
+
+                $message = new \core\message\message();
+                $message->component = 'analytics';
+                $message->name = 'insights';
+
+                $message->userfrom = get_admin();
+                $message->userto = $user;
+
+                $insighturl = new \moodle_url('/report/insights/insights.php?modelid=' . $modelid . '&contextid=' . $context->id);
+                $message->subject = $subject;
+                // Same than the subject.
+                $message->contexturlname = $message->subject;
+                $message->courseid = $coursecontext->instanceid;
+
+                $message->fullmessage = get_string('insightinfomessage', 'analytics', $insighturl->out());
+                $message->fullmessageformat = FORMAT_PLAIN;
+                $message->fullmessagehtml = get_string('insightinfomessage', 'analytics', $insighturl->out());
+                $message->smallmessage = get_string('insightinfomessage', 'analytics', $insighturl->out());
+                $message->contexturl = $insighturl->out(false);
+
+
+                message_send($message);
+            }
+        }
+
+    }
+
+    public static function instance() {
+        return new static();
+    }
+
+    /**
+     * Defines a boundary to ignore predictions below the specified prediction score.
+     *
+     * Value should go from 0 to 1.
+     *
+     * @return float
+     */
+    protected function min_prediction_score() {
+        // The default minimum discards predictions with a low score.
+        return \core_analytics\model::MIN_SCORE;
+    }
+
+    /**
+     * Should the model callback be triggered?
+     *
+     * @param mixed $class
+     * @return bool
+     */
+    public function triggers_callback($predictedvalue, $predictionscore) {
+
+        $minscore = floatval($this->min_prediction_score());
+        if ($minscore < 0) {
+            debugging(get_class($this) . ' minimum prediction score is below 0, please update it to a value between 0 and 1.');
+        } else if ($minscore > 1) {
+            debugging(get_class($this) . ' minimum prediction score is above 1, please update it to a value between 0 and 1.');
+        }
+
+        // We need to consider that targets may not have a min score.
+        if (!empty($minscore) && floatval($predictionscore) < $minscore) {
+            return false;
+        }
+
+        if (!$this->is_linear()) {
+            if (in_array($predictedvalue, $this->ignored_predicted_classes())) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Calculates the target.
+     *
+     * Returns an array of values which size matches $sampleids size.
+     *
+     * Rows with null values will be skipped as invalid by time splitting methods.
+     *
+     * @param array $sampleids
+     * @param \core_analytics\analysable $analysable
+     * @param integer $starttime startime is not necessary when calculating targets
+     * @param integer $endtime endtime is not necessary when calculating targets
+     * @return array The format to follow is [userid] = scalar|null
+     */
+    public function calculate($sampleids, \core_analytics\analysable $analysable) {
+
+        if (!PHPUNIT_TEST && CLI_SCRIPT) {
+            echo '.';
+        }
+
+        $calculations = [];
+        foreach ($sampleids as $sampleid => $unusedsampleid) {
+            $calculatedvalue = $this->calculate_sample($sampleid, $analysable);
+
+            if (!is_null($calculatedvalue)) {
+                if ($this->is_linear() && ($calculatedvalue > static::get_max_value() || $calculatedvalue < static::get_min_value())) {
+                    throw new \coding_exception('Calculated values should be higher than ' . static::get_min_value() .
+                        ' and lower than ' . static::get_max_value() . '. ' . $calculatedvalue . ' received');
+                } else if (!$this->is_linear() && static::is_a_class($calculatedvalue) === false) {
+                    throw new \coding_exception('Calculated values should be one of the target classes (' .
+                        json_encode(static::get_classes()) . '). ' . $calculatedvalue . ' received');
+                }
+            }
+            $calculations[$sampleid] = $calculatedvalue;
+        }
+        return $calculations;
+    }
+}
diff --git a/analytics/classes/local/target/binary.php b/analytics/classes/local/target/binary.php
new file mode 100644 (file)
index 0000000..b77ca9c
--- /dev/null
@@ -0,0 +1,91 @@
+<?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/>.
+
+/**
+ * Binary classifier target.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\target;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Binary classifier target.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class binary extends discrete {
+
+    public function is_linear() {
+        // TODO Remove this discrete override once prediction processors support
+        // multiclass classifiers; this method will be moved to discrete.
+        return false;
+    }
+
+    /**
+     * Returns the target discrete values.
+     *
+     * Only useful for targets using discrete values, must be overwriten if it is the case.
+     *
+     * @return array
+     */
+    public static final function get_classes() {
+        return array(0, 1);
+    }
+
+    /**
+     * Returns the predicted classes that will be ignored.
+     *
+     * @return array
+     */
+    protected function ignored_predicted_classes() {
+        // Zero-value class is usually ignored in binary classifiers.
+        return array(0);
+    }
+
+    public function get_value_style($value) {
+
+        if (!self::is_a_class($value)) {
+            throw new \moodle_exception('errorpredictionformat', 'analytics');
+        }
+
+        if (in_array($value, $this->ignored_predicted_classes())) {
+            // 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 'alert alert-success';
+        }
+
+        // Default binaries are danger when prediction = 1.
+        if ($value) {
+            return 'alert alert-danger';
+        }
+        return 'alert alert-success';
+    }
+
+    protected static function classes_description() {
+        return array(
+            get_string('yes'),
+            get_string('no')
+        );
+    }
+
+}
diff --git a/analytics/classes/local/target/discrete.php b/analytics/classes/local/target/discrete.php
new file mode 100644 (file)
index 0000000..49df96c
--- /dev/null
@@ -0,0 +1,116 @@
+<?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/>.
+
+/**
+ * Discrete values target.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\target;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Discrete values target.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class discrete extends base {
+
+    public function is_linear() {
+        // Not supported yet.
+        throw new \coding_exception('Sorry, this version\'s prediction processors only support targets with binary values.');
+    }
+
+    protected static function is_a_class($class) {
+        return (in_array($class, static::get_classes()));
+    }
+
+    public function get_display_value($value) {
+
+        if (!self::is_a_class($value)) {
+            throw new \moodle_exception('errorpredictionformat', 'analytics');
+        }
+
+        // array_values to discard any possible weird keys devs used.
+        $classes = array_values(static::get_classes());
+        $descriptions = array_values(static::classes_description());
+
+        if (count($classes) !== count($descriptions)) {
+            throw new \coding_exception('You need to describe all your classes (' . json_encode($classes) . ') in self::classes_description');
+        }
+
+        $key = array_search($value, $classes);
+        if ($key === false) {
+            throw new \coding_exception('You need to describe all your classes (' . json_encode($classes) . ') in self::classes_description');
+        }
+
+        return $descriptions[$key];
+    }
+
+    public function get_value_style($value) {
+
+        if (!self::is_a_class($value)) {
+            throw new \moodle_exception('errorpredictionformat', 'analytics');
+        }
+
+        if (in_array($value, $this->ignored_predicted_classes())) {
+            // Just in case, if it is ignored the prediction should not even be recorded.
+            return '';
+        }
+
+        debugging('Please overwrite \core_analytics\local\target\discrete::get_value_style, all your target classes are styled ' .
+            'the same way otherwise', DEBUG_DEVELOPER);
+        return 'alert alert-danger';
+    }
+
+    /**
+     * Returns the target discrete values.
+     *
+     * Only useful for targets using discrete values, must be overwriten if it is the case.
+     *
+     * @return array
+     */
+    public static function get_classes() {
+        // Coding exception as this will only be called if this target have non-linear values.
+        throw new \coding_exception('Overwrite get_classes() and return an array with the different target classes');
+    }
+
+    protected static function classes_description() {
+        throw new \coding_exception('Overwrite classes_description() and return an array with the target classes description and ' .
+            'indexes matching self::get_classes');
+    }
+
+    /**
+     * Returns the predicted classes that will be ignored.
+     *
+     * Better be keen to add more than less classes here, the callback is always able to discard some classes. As an example
+     * a target with classes 'grade 0-3', 'grade 3-6', 'grade 6-8' and 'grade 8-10' is interested in flagging both 'grade 0-3'
+     * and 'grade 3-6'. On the other hand, a target like dropout risk with classes 'yes', 'no' may just be interested in 'yes'.
+     *
+     * @return array
+     */
+    protected function ignored_predicted_classes() {
+        // Coding exception as this will only be called if this target have non-linear values.
+        throw new \coding_exception('Overwrite ignored_predicted_classes() and return an array with the classes that triggers ' .
+            'the callback');
+    }
+}
diff --git a/analytics/classes/local/target/linear.php b/analytics/classes/local/target/linear.php
new file mode 100644 (file)
index 0000000..1d1ce28
--- /dev/null
@@ -0,0 +1,83 @@
+<?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/>.
+
+/**
+ * Linear values target.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\target;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Linear values target.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class linear extends base {
+
+    public function is_linear() {
+        // Not supported yet.
+        throw new \coding_exception('Sorry, this version\'s prediction processors only support targets with binary values.');
+    }
+
+    public function get_value_style($value) {
+
+        // This is very generic, targets will probably be interested in overwriting this.
+        $diff = static::get_max_value() - static::get_min_value();
+        if (($value - static::get_min_value()) / $diff >= 0.5) {
+            return 'alert alert-success';
+        }
+        return 'alert alert-danger';
+    }
+
+    /**
+     * Gets the maximum value for this target
+     *
+     * @return float
+     */
+    protected static function get_max_value() {
+        // Coding exception as this will only be called if this target have linear values.
+        throw new \coding_exception('Overwrite get_max_value() and return the target max value');
+    }
+
+    /**
+     * Gets the minimum value for this target
+     *
+     * @return float
+     */
+    protected static function get_min_value() {
+        // Coding exception as this will only be called if this target have linear values.
+        throw new \coding_exception('Overwrite get_min_value() and return the target min value');
+    }
+
+    /**
+     * Returns the minimum value that triggers the callback.
+     *
+     * @return float
+     */
+    protected function get_callback_boundary() {
+        // Coding exception as this will only be called if this target have linear values.
+        throw new \coding_exception('Overwrite get_callback_boundary() and return the min value that ' .
+            'should trigger the callback');
+    }
+}
diff --git a/analytics/classes/local/time_splitting/base.php b/analytics/classes/local/time_splitting/base.php
new file mode 100644 (file)
index 0000000..b1b7957
--- /dev/null
@@ -0,0 +1,331 @@
+<?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 time splitting method.
+ *
+ * @package   core_analytics
+ * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\time_splitting;
+
+defined('MOODLE_INTERNAL') || die();
+
+abstract class base {
+
+    /**
+     * @var string
+     */
+    protected $id;
+
+    /**
+     * @var \core_analytics\analysable
+     */
+    protected $analysable;
+
+
+    /**
+     * @var int[]
+     */
+    protected $sampleids;
+
+    /**
+     * @var string
+     */
+    protected $samplesorigin;
+
+    /**
+     * @var array
+     */
+    protected $ranges = [];
+
+    /**
+     * @var \core_analytics\indicator\base
+     */
+    protected static $indicators = [];
+
+    abstract protected function define_ranges();
+
+    public function get_name() {
+        return $this->get_id();
+    }
+
+    /**
+     * Returns the time splitting method id.
+     *
+     * @return string
+     */
+    public function get_id() {
+        return '\\' . get_class($this);
+    }
+
+    /**
+     * Assigns the analysable and updates the time ranges according to the analysable start and end dates.
+     *
+     * @param \core_analytics\analysable $analysable
+     * @return void
+     */
+    public function set_analysable(\core_analytics\analysable $analysable) {
+        $this->analysable = $analysable;
+        $this->ranges = $this->define_ranges();
+        $this->validate_ranges();
+    }
+
+    public function get_analysable() {
+        return $this->analysable;
+    }
+
+    /**
+     * Returns whether the course can be processed by this time splitting method or not.
+     *
+     * @return bool
+     */
+    public function is_valid_analysable(\core_analytics\analysable $analysable) {
+        return true;
+    }
+
+    public function ready_to_predict($range) {
+        if ($range['end'] <= time()) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Calculates the course students indicators and targets.
+     *
+     * @return void
+     */
+    public function calculate($sampleids, $samplesorigin, $indicators, $ranges, $target = false) {
+
+        $calculatedtarget = false;
+        if ($target) {
+            // We first calculate the target because analysable data may still be invalid, we need to stop if it is not.
+            $calculatedtarget = $target->calculate($sampleids, $this->analysable);
+
+            // We remove samples we can not calculate their target.
+            $sampleids = array_filter($sampleids, function($sampleid) use ($calculatedtarget) {
+                if (is_null($calculatedtarget[$sampleid])) {
+                    return false;
+                }
+                return true;
+            });
+        }
+
+        // No need to continue calculating if the target couldn't be calculated for any sample.
+        if (empty($sampleids)) {
+            return false;
+        }
+
+        $dataset = $this->calculate_indicators($sampleids, $samplesorigin, $indicators, $ranges);
+
+        // Now that we have the indicators in place we can add the time range indicators (and target if provided) to each of them.
+        $this->fill_dataset($dataset, $calculatedtarget);
+
+        $this->add_metadata($dataset, $indicators, $target);
+
+        if (!PHPUNIT_TEST && CLI_SCRIPT) {
+            echo PHP_EOL;
+        }
+
+        return $dataset;
+    }
+
+    /**
+     * Calculates indicators for all course students.
+     *
+     * @return array
+     */
+    protected function calculate_indicators($sampleids, $samplesorigin, $indicators, $ranges) {
+
+        $dataset = array();
+
+        // Fill the dataset samples with indicators data.
+        foreach ($indicators as $indicator) {
+
+            // Per-range calculations.
+            foreach ($ranges as $rangeindex => $range) {
+
+                // Indicator instances are per-range.
+                $rangeindicator = clone $indicator;
+
+                // Calculate the indicator for each sample in this time range.
+                $calculated = $rangeindicator->calculate($sampleids, $samplesorigin, $range['start'], $range['end']);
+
+                // Copy the calculated data to the dataset.
+                foreach ($calculated as $analysersampleid => $calculatedvalues) {
+
+                    $uniquesampleid = $this->append_rangeindex($analysersampleid, $rangeindex);
+
+                    // Init the sample if it is still empty.
+                    if (!isset($dataset[$uniquesampleid])) {
+                        $dataset[$uniquesampleid] = array();
+                    }
+
+                    // Append the calculated indicator features at the end of the sample.
+                    $dataset[$uniquesampleid] = array_merge($dataset[$uniquesampleid], $calculatedvalues);
+                }
+            }
+        }
+
+        return $dataset;
+    }
+
+    /**
+     * Adds time range indicators and the target to each sample.
+     *
+     * This will identify the sample as belonging to a specific range.
+     *
+     * @return void
+     */
+    protected function fill_dataset(&$dataset, $calculatedtarget = false) {
+
+        $nranges = count($this->get_all_ranges());
+
+        foreach ($dataset as $uniquesampleid => $unmodified) {
+
+            list($analysersampleid, $rangeindex) = $this->infer_sample_info($uniquesampleid);
+
+            // No need to add range features if this time splitting method only defines one time range.
+            if ($nranges > 1) {
+
+                // 1 column for each range.
+                $timeindicators = array_fill(0, $nranges, 0);
+
+                $timeindicators[$rangeindex] = 1;
+
+                $dataset[$uniquesampleid] = array_merge($timeindicators, $dataset[$uniquesampleid]);
+            }
+
+            if ($calculatedtarget) {
+                // Add this sampleid's calculated target and the end.
+                $dataset[$uniquesampleid][] = $calculatedtarget[$analysersampleid];
+
+            } else {
+                // Add this sampleid, it will be used to identify the prediction that comes back from
+                // the predictions processor.
+                array_unshift($dataset[$uniquesampleid], $uniquesampleid);
+            }
+        }
+    }
+
+    /**
+     * Adds dataset context info.
+     *
+     * The final dataset document will look like this:
+     * ----------------------------------------------------
+     * metadata1,metadata2,metadata3,.....
+     * value1, value2, value3,.....
+     *
+     * indicator1,indicator2,indicator3,indicator4,.....
+     * stud1value1,stud1value2,stud1value3,stud1value4,.....
+     * stud2value1,stud2value2,stud2value3,stud2value4,.....
+     * .....
+     * ----------------------------------------------------
+     *
+     * @return void
+     */
+    protected function add_metadata(&$dataset, $indicators, $target = false) {
+
+        $metadata = array(
+            'timesplitting' => $this->get_id(),
+            // If no target the first column is the sampleid, if target the last column is the target.
+            'nfeatures' => count(current($dataset)) - 1
+        );
+        if ($target) {
+            $metadata['targetclasses'] = json_encode($target::get_classes());
+            $metadata['targettype'] = ($target->is_linear()) ? 'linear' : 'discrete';
+        }
+
+        // The first 2 samples will be used to store metadata about the dataset.
+        $metadatacolumns = [];
+        $metadatavalues = [];
+        foreach ($metadata as $key => $value) {
+            $metadatacolumns[] = $key;
+            $metadatavalues[] = $value;
+        }
+
+        $headers = $this->get_headers($indicators, $target);
+
+        // This will also reset samples' dataset keys.
+        array_unshift($dataset, $metadatacolumns, $metadatavalues, $headers);
+    }
+
+    /**
+     * Returns the ranges used by this time splitting method.
+     *
+     * @return array
+     */
+    public function get_all_ranges() {
+        return $this->ranges;
+    }
+
+    public function append_rangeindex($sampleid, $rangeindex) {
+        return $sampleid . '-' . $rangeindex;
+    }
+
+    public function infer_sample_info($sampleid) {
+        return explode('-', $sampleid);
+    }
+
+    protected function get_headers($indicators, $target = false) {
+        // 3th column will contain the indicator ids.
+        $headers = array();
+
+        if (!$target) {
+            // The first column is the sampleid.
+            $headers[] = 'sampleid';
+        }
+
+        // We always have 1 column for each time splitting method range, it does not depend on how
+        // many ranges we calculated.
+        $ranges = $this->get_all_ranges();
+        if (count($ranges) > 1) {
+            foreach ($ranges as $rangeindex => $range) {
+                $headers[] = 'range/' . $rangeindex;
+            }
+        }
+
+        // Model indicators.
+        foreach ($indicators as $indicator) {
+            $headers = array_merge($headers, $indicator::get_feature_headers());
+        }
+
+        // The target as well.
+        if ($target) {
+            $headers[] = get_class($target);
+        }
+
+        return $headers;
+    }
+
+    /**
+     * Validates the time splitting method ranges.
+     *
+     * @throw \coding_exception
+     * @return void
+     */
+    protected function validate_ranges() {
+        foreach ($this->ranges as $key => $range) {
+            if (!isset($this->ranges[$key]['start']) || !isset($this->ranges[$key]['end'])) {
+                throw new \coding_exception($this->get_id() . ' time splitting method "' . $key .
+                    '" range is not fully defined. We need a start timestamp and an end timestamp.');
+            }
+        }
+    }
+}
diff --git a/analytics/classes/local/time_splitting/deciles.php b/analytics/classes/local/time_splitting/deciles.php
new file mode 100644 (file)
index 0000000..5237622
--- /dev/null
@@ -0,0 +1,55 @@
+<?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/>.
+
+/**
+ * 10 parts time splitting method.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\time_splitting;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * 10 parts time splitting method.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class deciles extends base {
+
+    public function get_name() {
+        return get_string('timesplitting:deciles', 'analytics');
+    }
+
+    protected function define_ranges() {
+        $rangeduration = floor(($this->analysable->get_end() - $this->analysable->get_start()) / 10);
+
+        $ranges = array();
+        for ($i = 0; $i < 10; $i++) {
+            $ranges[] = array(
+                'start' => $this->analysable->get_start() + ($rangeduration * $i),
+                'end' => $this->analysable->get_start() + ($rangeduration * ($i + 1))
+            );
+        }
+
+        return $ranges;
+    }
+}
diff --git a/analytics/classes/local/time_splitting/deciles_accum.php b/analytics/classes/local/time_splitting/deciles_accum.php
new file mode 100644 (file)
index 0000000..cea73f9
--- /dev/null
@@ -0,0 +1,55 @@
+<?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/>.
+
+/**
+ * Range processor splitting the course in ten parts and accumulating data.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\time_splitting;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Range processor splitting the course in ten parts and accumulating data.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class deciles_accum extends base {
+
+    public function get_name() {
+        return get_string('timesplitting:decilesaccum', 'analytics');
+    }
+
+    protected function define_ranges() {
+        $rangeduration = floor(($this->analysable->get_end() - $this->analysable->get_start()) / 10);
+
+        $ranges = array();
+        for ($i = 0; $i < 10; $i++) {
+            $ranges[] = array(
+                'start' => $this->analysable->get_start(),
+                'end' => $this->analysable->get_start() + ($rangeduration * ($i + 1))
+            );
+        }
+
+        return $ranges;
+    }
+}
diff --git a/analytics/classes/local/time_splitting/no_splitting.php b/analytics/classes/local/time_splitting/no_splitting.php
new file mode 100644 (file)
index 0000000..d09aab0
--- /dev/null
@@ -0,0 +1,49 @@
+<?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/>.
+
+/**
+ * No time splitting method.
+ *
+ * Used when time is not a factor to consider into the equation.
+ *
+ * @package   core_analytics
+ * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\time_splitting;
+
+defined('MOODLE_INTERNAL') || die();
+
+class no_splitting extends base {
+
+    public function get_name() {
+        return get_string('timesplitting:nosplitting', 'analytics');
+    }
+
+    public function ready_to_predict($range) {
+        return true;
+    }
+
+    protected function define_ranges() {
+        return [
+            [
+                'start' => 0,
+                'end' => \core_analytics\analysable::MAX_TIME
+            ]
+        ];
+    }
+}
diff --git a/analytics/classes/local/time_splitting/quarters.php b/analytics/classes/local/time_splitting/quarters.php
new file mode 100644 (file)
index 0000000..4f02fb6
--- /dev/null
@@ -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/>.
+
+/**
+ * 4 quarters time splitting method.
+ *
+ * @package   core_analytics
+ * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\time_splitting;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * 4 quarters time splitting method.
+ *
+ * @package   core_analytics
+ * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class quarters extends base {
+
+    public function get_name() {
+        return get_string('timesplitting:quarters', 'analytics');
+    }
+
+    protected function define_ranges() {
+        $duration = floor(($this->analysable->get_end() - $this->analysable->get_start()) / 4);
+        return [
+            [
+                'start' => $this->analysable->get_start(),
+                'end' => $this->analysable->get_start() + $duration
+            ], [
+                'start' => $this->analysable->get_start() + $duration,
+                'end' => $this->analysable->get_start() + ($duration * 2)
+            ], [
+                'start' => $this->analysable->get_start() + ($duration * 2),
+                'end' => $this->analysable->get_start() + ($duration * 3)
+            ], [
+                'start' => $this->analysable->get_start() + ($duration * 3),
+                'end' => $this->analysable->get_start() + ($duration * 4)
+            ]
+        ];
+    }
+}
diff --git a/analytics/classes/local/time_splitting/quarters_accum.php b/analytics/classes/local/time_splitting/quarters_accum.php
new file mode 100644 (file)
index 0000000..067fd44
--- /dev/null
@@ -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/>.
+
+/**
+ * Range processor splitting the course in quarters and accumulating data.
+ *
+ * @package   core_analytics
+ * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\time_splitting;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Range processor splitting the course in quarters and accumulating data.
+ *
+ * @package   core_analytics
+ * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class quarters_accum extends base {
+
+    public function get_name() {
+        return get_string('timesplitting:quartersaccum', 'analytics');
+    }
+
+    protected function define_ranges() {
+        $duration = floor(($this->analysable->get_end() - $this->analysable->get_start()) / 4);
+        return [
+            [
+                'start' => $this->analysable->get_start(),
+                'end' => $this->analysable->get_start() + $duration
+            ], [
+                'start' => $this->analysable->get_start(),
+                'end' => $this->analysable->get_start() + ($duration * 2)
+            ], [
+                'start' => $this->analysable->get_start(),
+                'end' => $this->analysable->get_start() + ($duration * 3)
+            ], [
+                'start' => $this->analysable->get_start(),
+                'end' => $this->analysable->get_start() + ($duration * 4)
+            ]
+        ];
+    }
+}
diff --git a/analytics/classes/local/time_splitting/single_range.php b/analytics/classes/local/time_splitting/single_range.php
new file mode 100644 (file)
index 0000000..b8e7ba5
--- /dev/null
@@ -0,0 +1,43 @@
+<?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/>.
+
+/**
+ * Single time splitting method.
+ *
+ * @package   core_analytics
+ * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\time_splitting;
+
+defined('MOODLE_INTERNAL') || die();
+
+class single_range extends base {
+
+    public function get_name() {
+        return get_string('timesplitting:singlerange', 'analytics');
+    }
+
+    protected function define_ranges() {
+        return [
+            [
+                'start' => $this->analysable->get_start(),
+                'end' => $this->analysable->get_end()
+            ]
+        ];
+    }
+}
diff --git a/analytics/classes/local/time_splitting/weekly.php b/analytics/classes/local/time_splitting/weekly.php
new file mode 100644 (file)
index 0000000..50785b8
--- /dev/null
@@ -0,0 +1,79 @@
+<?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/>.
+
+/**
+ * Weekly time splitting method.
+ *
+ * @package   core_analytics
+ * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\time_splitting;
+
+defined('MOODLE_INTERNAL') || die();
+
+abstract class weekly extends base {
+
+    public function get_name() {
+        return get_string('timesplitting:weekly', 'analytics');
+    }
+
+    public function is_valid_analysable(\core_analytics\analysable $analysable) {
+        $diff = $analysable->get_end() - $analysable->get_start();
+        $nweeks = round($diff / WEEKSECS);
+        if ($nweeks > 520) {
+            // More than 10 years...
+            return false;
+        }
+        return parent::is_valid_analysable($analysable);
+    }
+
+    protected function define_ranges() {
+
+        $ranges = array();
+
+        // It is more important to work with a proper end date than the start date.
+        $i = 0;
+        do {
+
+            $dt = new \DateTime();
+            $dt->setTimestamp($this->analysable->get_end());
+            $dt->modify('-' . $i . ' weeks');
+            $rangeend = $dt->getTimestamp();
+
+            $dt->modify('-1 weeks');
+            $rangestart = $dt->getTimestamp();
+
+            $ranges[] = array(
+                'start' => $rangestart,
+                'end' => $rangeend
+            );
+
+            $i++;
+
+        } while ($this->analysable->get_start() < $rangestart);
+
+        $ranges = array_reverse($ranges, false);
+
+        // Is not worth trying to predict during the first weeks.
+        array_shift($ranges);
+        array_shift($ranges);
+
+        return $ranges;
+    }
+
+}
diff --git a/analytics/classes/local/time_splitting/weekly_accum.php b/analytics/classes/local/time_splitting/weekly_accum.php
new file mode 100644 (file)
index 0000000..22d793c
--- /dev/null
@@ -0,0 +1,81 @@
+<?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/>.
+
+/**
+ * Weekly time splitting method.
+ *
+ * @package   core_analytics
+ * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics\local\time_splitting;
+
+defined('MOODLE_INTERNAL') || die();
+
+abstract class weekly_accum extends base {
+
+    public function get_name() {
+        return get_string('timesplitting:weeklyaccum', 'analytics');
+    }
+
+    public function is_valid_analysable(\core_analytics\analysable $analysable) {
+        $diff = $analysable->get_end() - $analysable->get_start();
+        $nweeks = round($diff / WEEKSECS);
+        if ($nweeks > 520) {
+            // More than 10 years...
+            return false;
+        }
+        return parent::is_valid_analysable($analysable);
+    }
+
+    protected function define_ranges() {
+
+        $ranges = array();
+
+        // It is more important to work with a proper end date than start date.
+        $i = 0;
+        do {
+
+            $dt = new \DateTime();
+            $dt->setTimestamp($this->analysable->get_end());
+            $dt->modify('-' . $i . ' weeks');
+            $rangeend = $dt->getTimestamp();
+
+            // Used to calculate when we are done creating new ranges.
+            $dt->modify('-1 weeks');
+            $rangestart = $dt->getTimestamp();
+
+            // Accumulative, always from the course start.
+            $ranges[] = array(
+                'start' => $this->analysable->get_start(),
+                'end' => $rangeend
+            );
+
+            $i++;
+
+        } while ($this->analysable->get_start() < $rangestart);
+
+        $ranges = array_reverse($ranges, false);
+
+        // Is not worth trying to predict during the first weeks.
+        array_shift($ranges);
+        array_shift($ranges);
+
+        return $ranges;
+    }
+
+}
diff --git a/analytics/classes/manager.php b/analytics/classes/manager.php
new file mode 100644 (file)
index 0000000..a709e01
--- /dev/null
@@ -0,0 +1,271 @@
+<?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/>.
+
+/**
+ * Inspire tool manager.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@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();
+
+/**
+ * Inspire tool manager.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class manager {
+
+    /**
+     * @var \core_analytics\predictor[]
+     */
+    protected static $predictionprocessors = null;
+
+    /**
+     * @var \core_analytics\local\indicator\base[]
+     */
+    protected static $allindicators = null;
+
+    /**
+     * @var \core_analytics\local\time_splitting\base[]
+     */
+    protected static $alltimesplittings = null;
+
+    public static function get_all_models($enabled = false, $trained = false, $predictioncontext = false) {
+        global $DB;
+
+        $filters = array();
+        if ($enabled) {
+            $filters['enabled'] = 1;
+        }
+        if ($trained) {
+            $filters['trained'] = 1;
+        }
+        $modelobjs = $DB->get_records('analytics_models', $filters);
+
+        $models = array();
+        foreach ($modelobjs as $modelobj) {
+            $model = new \core_analytics\model($modelobj);
+            if (!$predictioncontext || $model->predictions_exist($predictioncontext)) {
+                $models[$modelobj->id] = $model;
+            }
+        }
+        return $models;
+    }
+
+    /**
+     * Returns the site selected predictions processor.
+     *
+     * @param string $predictionclass
+     * @param bool $checkisready
+     * @return \core_analytics\predictor
+     */
+    public static function get_predictions_processor($predictionclass = false, $checkisready = true) {
+
+        // We want 0 or 1 so we can use it as an array key for caching.
+        $checkisready = intval($checkisready);
+
+        if ($predictionclass === false) {
+            $predictionclass = get_config('analytics', 'predictionsprocessor');
+        }
+
+        if (empty($predictionclass)) {
+            // Use the default one if nothing set.
+            $predictionclass = '\mlbackend_php\processor';
+        }
+
+        if (!class_exists($predictionclass)) {
+            throw new \coding_exception('Invalid predictions processor ' . $predictionclass . '.');
+        }
+
+        $interfaces = class_implements($predictionclass);
+        if (empty($interfaces['core_analytics\predictor'])) {
+            throw new \coding_exception($predictionclass . ' should implement \core_analytics\predictor.');
+        }
+
+        // Return it from the cached list.
+        if (!isset(self::$predictionprocessors[$checkisready][$predictionclass])) {
+
+            $instance = new $predictionclass();
+            if ($checkisready) {
+                $isready = $instance->is_ready();
+                if ($isready !== true) {
+                    throw new \moodle_exception('errorprocessornotready', 'analytics', '', $isready);
+                }
+            }
+            self::$predictionprocessors[$checkisready][$predictionclass] = $instance;
+        }
+
+        return self::$predictionprocessors[$checkisready][$predictionclass];
+    }
+
+    public static function get_all_prediction_processors() {
+
+        $mlbackends = \core_component::get_plugin_list('mlbackend');
+
+        $predictionprocessors = array();
+        foreach ($mlbackends as $mlbackend => $unused) {
+            $classfullpath = '\\mlbackend_' . $mlbackend . '\\processor';
+            $predictionprocessors[$classfullpath] = self::get_predictions_processor($classfullpath, false);
+        }
+        return $predictionprocessors;
+    }
+
+    /**
+     * Get all available time splitting methods.
+     *
+     * @return \core_analytics\time_splitting\base[]
+     */
+    public static function get_all_time_splittings() {
+        if (self::$alltimesplittings !== null) {
+            return self::$alltimesplittings;
+        }
+
+        $classes = self::get_analytics_classes('time_splitting');
+
+        self::$alltimesplittings = [];
+        foreach ($classes as $fullclassname => $classpath) {
+            $instance = self::get_time_splitting($fullclassname);
+            // We need to check that it is a valid time splitting method, it may be an abstract class.
+            if ($instance) {
+                self::$alltimesplittings[$instance->get_id()] = $instance;
+            }
+        }
+
+        return self::$alltimesplittings;
+    }
+
+    /**
+     * Returns the enabled time splitting methods.
+     *
+     * @return \core_analytics\local\time_splitting\base[]
+     */
+    public static function get_enabled_time_splitting_methods() {
+
+        if ($enabledtimesplittings = get_config('analytics', 'timesplittings')) {
+            $enabledtimesplittings = array_flip(explode(',', $enabledtimesplittings));
+        }
+
+        $timesplittings = self::get_all_time_splittings();
+        foreach ($timesplittings as $key => $timesplitting) {
+
+            // We remove the ones that are not enabled. This also respects the default value (all methods enabled).
+            if (!empty($enabledtimesplittings) && !isset($enabledtimesplittings[$key])) {
+                unset($timesplittings[$key]);
+            }
+        }
+        return $timesplittings;
+    }
+
+    /**
+     * Returns a time splitting method by its classname.
+     *
+     * @param string $fullclassname
+     * @return \core_analytics\local\time_splitting\base|false False if it is not valid.
+     */
+    public static function get_time_splitting($fullclassname) {
+        if (!self::is_valid($fullclassname, '\core_analytics\local\time_splitting\base')) {
+            return false;
+        }
+        return new $fullclassname();
+    }
+
+    /**
+     * Return all system indicators.
+     *
+     * @return \core_analytics\local\indicator\base[]
+     */
+    public static function get_all_indicators() {
+        if (self::$allindicators !== null) {
+            return self::$allindicators;
+        }
+
+        $classes = self::get_analytics_classes('indicator');
+
+        self::$allindicators = [];
+        foreach ($classes as $fullclassname => $classpath) {
+            $instance = self::get_indicator($fullclassname);
+            if ($instance) {
+                // Using get_class as get_component_classes_in_namespace returns double escaped fully qualified class names.
+                self::$allindicators['\\' . get_class($instance)] = $instance;
+            }
+        }
+
+        return self::$allindicators;
+    }
+
+    public static function get_target($fullclassname) {
+        if (!self::is_valid($fullclassname, 'core_analytics\local\target\base')) {
+            return false;
+        }
+        return new $fullclassname();
+    }
+
+    /**
+     * Returns an instance of the provided indicator.
+     *
+     * @param string $fullclassname
+     * @return \core_analytics\local\indicator\base|false False if it is not valid.
+     */
+    public static function get_indicator($fullclassname) {
+        if (!self::is_valid($fullclassname, 'core_analytics\local\indicator\base')) {
+            return false;
+        }
+        return new $fullclassname();
+    }
+
+    /**
+     * Returns whether a time splitting method is valid or not.
+     *
+     * @param string $fullclassname
+     * @return bool
+     */
+    public static function is_valid($fullclassname, $baseclass) {
+        if (is_subclass_of($fullclassname, $baseclass)) {
+            if ((new \ReflectionClass($fullclassname))->isInstantiable()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Returns the provided element classes in the site.
+     *
+     * @param string $element
+     * @return string[] Array keys are the FQCN and the values the class path.
+     */
+    private static function get_analytics_classes($element) {
+
+        // Just in case...
+        $element = clean_param($element, PARAM_ALPHAEXT);
+
+        $classes = \core_component::get_component_classes_in_namespace('core_analytics', 'local\\' . $element);
+        foreach (\core_component::get_plugin_types() as $type => $unusedplugintypepath) {
+            foreach (\core_component::get_plugin_list($type) as $pluginname => $unusedpluginpath) {
+                $frankenstyle = $type . '_' . $pluginname;
+                $classes += \core_component::get_component_classes_in_namespace($frankenstyle, 'analytics\\' . $element);
+            }
+        }
+        return $classes;
+    }
+}
diff --git a/analytics/classes/model.php b/analytics/classes/model.php
new file mode 100644 (file)
index 0000000..4ff9629
--- /dev/null
@@ -0,0 +1,933 @@
+<?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/>.
+
+/**
+ * Inspire tool model representation.
+ *
+ * @package   core_analytics
+ * @copyright 2016 David Monllao {@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();
+
+/**
+ * Inspire tool model representation.
+ *
+ * @package   core_analytics
+ * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class model {
+
+    const OK = 0;
+    const GENERAL_ERROR = 1;
+    const NO_DATASET = 2;
+
+    const EVALUATE_LOW_SCORE = 4;
+    const EVALUATE_NOT_ENOUGH_DATA = 8;
+
+    const ANALYSE_INPROGRESS = 2;
+    const ANALYSE_REJECTED_RANGE_PROCESSOR = 4;
+    const ANALYSABLE_STATUS_INVALID_FOR_RANGEPROCESSORS = 8;
+    const ANALYSABLE_STATUS_INVALID_FOR_TARGET = 16;
+
+    const MIN_SCORE = 0.7;
+    const ACCEPTED_DEVIATION = 0.05;
+    const EVALUATION_ITERATIONS = 10;
+
+    /**
+     * @var \stdClass
+     */
+    protected $model = null;
+
+    /**
+     * @var \core_analytics\local\analyser\base
+     */
+    protected $analyser = null;
+
+    /**
+     * @var \core_analytics\local\target\base
+     */
+    protected $target = null;
+
+    /**
+     * @var \core_analytics\local\indicator\base[]
+     */
+    protected $indicators = null;
+
+    /**
+     * Unique Model id created from site info and last model modification.
+     *
+     * @var string
+     */
+    protected $uniqueid = null;
+
+    /**
+     * __construct
+     *
+     * @param int|stdClass $model
+     * @return void
+     */
+    public function __construct($model) {
+        global $DB;
+
+        if (is_scalar($model)) {
+            $model = $DB->get_record('analytics_models', array('id' => $model));
+        }
+        $this->model = $model;
+    }
+
+    /**
+     * get_id
+     *
+     * @return int
+     */
+    public function get_id() {
+        return $this->model->id;
+    }
+
+    /**
+     * get_model_obj
+     *
+     * @return \stdClass
+     */
+    public function get_model_obj() {
+        return $this->model;
+    }
+
+    /**
+     * get_target
+     *
+     * @return \core_analytics\local\target\base
+     */
+    public function get_target() {
+        if ($this->target !== null) {
+            return $this->target;
+        }
+        $instance = \core_analytics\manager::get_target($this->model->target);
+        $this->target = $instance;
+
+        return $this->target;
+    }
+
+    /**
+     * get_indicators
+     *
+     * @return \core_analytics\local\indicator\base[]
+     */
+    public function get_indicators() {
+        if ($this->indicators !== null) {
+            return $this->indicators;
+        }
+
+        $fullclassnames = json_decode($this->model->indicators);
+
+        if (!is_array($fullclassnames)) {
+            throw new \coding_exception('Model ' . $this->model->id . ' indicators can not be read');
+        }
+
+        $this->indicators = array();
+        foreach ($fullclassnames as $fullclassname) {
+            $instance = \core_analytics\manager::get_indicator($fullclassname);
+            if ($instance) {
+                $this->indicators[$fullclassname] = $instance;
+            } else {
+                debugging('Can\'t load ' . $fullclassname . ' indicator', DEBUG_DEVELOPER);
+            }
+        }
+
+        return $this->indicators;
+    }
+
+    /**
+     * Returns the list of indicators that could potentially be used by the model target.
+     *
+     * It includes the indicators that are part of the model.
+     *
+     * @return \core_analytics\local\indicator\base
+     */
+    public function get_potential_indicators() {
+
+        $indicators = \core_analytics\manager::get_all_indicators();
+
+        if (empty($this->analyser)) {
+            $this->init_analyser(array('evaluation' => true));
+        }
+
+        foreach ($indicators as $classname => $indicator) {
+            if ($this->analyser->check_indicator_requirements($indicator) !== true) {
+                unset($indicators[$classname]);
+            }
+        }
+        return $indicators;
+    }
+
+    /**
+     * get_analyser
+     *
+     * @return \core_analytics\local\analyser\base
+     */
+    public function get_analyser() {
+        if ($this->analyser !== null) {
+            return $this->analyser;
+        }
+
+        // Default initialisation with no options.
+        $this->init_analyser();
+
+        return $this->analyser;
+    }
+
+    /**
+     * init_analyser
+     *
+     * @param array $options
+     * @return void
+     */
+    protected function init_analyser($options = array()) {
+
+        $target = $this->get_target();
+        $indicators = $this->get_indicators();
+
+        if (empty($target)) {
+            throw new \moodle_exception('errornotarget', 'analytics');
+        }
+
+        if (!empty($options['evaluation'])) {
+            // The evaluation process will run using all available time splitting methods unless one is specified.
+            if (!empty($options['timesplitting'])) {
+                $timesplitting = \core_analytics\manager::get_time_splitting($options['timesplitting']);
+                $timesplittings = array($timesplitting->get_id() => $timesplitting);
+            } else {
+                $timesplittings = \core_analytics\manager::get_enabled_time_splitting_methods();
+            }
+        } else {
+
+            if (empty($this->model->timesplitting)) {
+                throw new \moodle_exception('invalidtimesplitting', 'analytics', '', $this->model->id);
+            }
+
+            // Returned as an array as all actions (evaluation, training and prediction) go through the same process.
+            $timesplittings = array($this->model->timesplitting => $this->get_time_splitting());
+        }
+
+        if (empty($timesplittings)) {
+            throw new \moodle_exception('errornotimesplittings', 'analytics');
+        }
+
+        $classname = $target->get_analyser_class();
+        if (!class_exists($classname)) {
+            throw \coding_exception($classname . ' class does not exists');
+        }
+
+        // Returns a \core_analytics\local\analyser\base class.
+        $this->analyser = new $classname($this->model->id, $target, $indicators, $timesplittings, $options);
+    }
+
+    /**
+     * get_time_splitting
+     *
+     * @return \core_analytics\local\time_splitting\base
+     */
+    public function get_time_splitting() {
+        if (empty($this->model->timesplitting)) {
+            return false;
+        }
+        return \core_analytics\manager::get_time_splitting($this->model->timesplitting);
+    }
+
+    /**
+     * create
+     *
+     * @param \core_analytics\local\target\base $target
+     * @param \core_analytics\local\indicator\base[] $indicators
+     * @return \core_analytics\model
+     */
+    public static function create(\core_analytics\local\target\base $target, array $indicators) {
+        global $USER, $DB;
+
+        $indicatorclasses = self::indicator_classes($indicators);
+
+        $now = time();
+
+        $modelobj = new \stdClass();
+        $modelobj->target = '\\' . get_class($target);
+        $modelobj->indicators = json_encode($indicatorclasses);
+        $modelobj->version = $now;
+        $modelobj->timecreated = $now;
+        $modelobj->timemodified = $now;
+        $modelobj->usermodified = $USER->id;
+
+        $id = $DB->insert_record('analytics_models', $modelobj);
+
+        // Get db defaults.
+        $modelobj = $DB->get_record('analytics_models', array('id' => $id), '*', MUST_EXIST);
+
+        return new static($modelobj);
+    }
+
+    public function update($enabled, $indicators, $timesplitting = '') {
+        global $USER, $DB;
+
+        $now = time();
+
+        $indicatorclasses = self::indicator_classes($indicators);
+
+        $indicatorsstr = json_encode($indicatorclasses);
+        if ($this->model->timesplitting !== $timesplitting ||
+                $this->model->indicators !== $indicatorsstr) {
+            // We update the version of the model so different time splittings are not mixed up.
+            $this->model->version = $now;
+
+            // Delete generated predictions.
+            $this->clear_model();
+
+            // Purge all generated files.
+            \core_analytics\dataset_manager::clear_model_files($this->model->id);
+
+            // Reset trained flag.
+            $this->model->trained = 0;
+        }
+        $this->model->enabled = $enabled;
+        $this->model->indicators = $indicatorsstr;
+        $this->model->timesplitting = $timesplitting;
+        $this->model->timemodified = $now;
+        $this->model->usermodified = $USER->id;
+
+        $DB->update_record('analytics_models', $this->model);
+
+        // It needs to be reset (just in case, we may already used it).
+        $this->uniqueid = null;
+    }
+
+    /**
+     * Evaluates the model datasets.
+     *
+     * Model datasets should already be available in Moodle's filesystem.
+     *
+     * @param array $options
+     * @return \stdClass[]
+     */
+    public function evaluate($options = array()) {
+
+        // Increase memory limit.
+        $this->increase_memory();
+
+        $options['evaluation'] = true;
+        $this->init_analyser($options);
+
+        if (empty($this->get_indicators())) {
+            throw new \moodle_exception('errornoindicators', 'analytics');
+        }
+
+        // Before get_labelled_data call so we get an early exception if it is not ready.
+        $predictor = \core_analytics\manager::get_predictions_processor();
+
+        $datasets = $this->get_analyser()->get_labelled_data();
+
+        // No datasets generated.
+        if (empty($datasets)) {
+            $result = new \stdClass();
+            $result->status = self::NO_DATASET;
+            $result->info = $this->get_analyser()->get_logs();
+            return array($result);
+        }
+
+        if (!PHPUNIT_TEST && CLI_SCRIPT) {
+            echo PHP_EOL . get_string('processingsitecontents', 'analytics') . PHP_EOL;
+        }
+
+        $results = array();
+        foreach ($datasets as $timesplittingid => $dataset) {
+
+            $timesplitting = \core_analytics\manager::get_time_splitting($timesplittingid);
+
+            $result = new \stdClass();
+
+            $dashestimesplittingid = str_replace('\\', '', $timesplittingid);
+            $outputdir = $this->get_output_dir(array('evaluation', $dashestimesplittingid));
+
+            // Evaluate the dataset, the deviation we accept in the results depends on the amount of iterations.
+            $predictorresult = $predictor->evaluate($this->model->id, self::ACCEPTED_DEVIATION,
+                self::EVALUATION_ITERATIONS, $dataset, $outputdir);
+
+            $result->status = $predictorresult->status;
+            $result->info = $predictorresult->info;
+
+            if (isset($predictorresult->score)) {
+                $result->score = $predictorresult->score;
+            } else {
+                // Prediction processors may return an error, default to 0 score in that case.
+                $result->score = 0;
+            }
+
+            $dir = false;
+            if (!empty($predictorresult->dir)) {
+                $dir = $predictorresult->dir;
+            }
+
+            $result->logid = $this->log_result($timesplitting->get_id(), $result->score, $dir, $result->info);
+
+            $results[$timesplitting->get_id()] = $result;
+        }
+
+        return $results;
+    }
+
+    /**
+     * train
+     *
+     * @return \stdClass
+     */
+    public function train() {
+        global $DB;
+
+        // Increase memory limit.
+        $this->increase_memory();
+
+        if ($this->model->enabled == false || empty($this->model->timesplitting)) {
+            throw new \moodle_exception('invalidtimesplitting', 'analytics', '', $this->model->id);
+        }
+
+        if (empty($this->get_indicators())) {
+            throw new \moodle_exception('errornoindicators', 'analytics');
+        }
+
+        // Before get_labelled_data call so we get an early exception if it is not writable.
+        $outputdir = $this->get_output_dir(array('execution'));
+
+        // Before get_labelled_data call so we get an early exception if it is not ready.
+        $predictor = \core_analytics\manager::get_predictions_processor();
+
+        $datasets = $this->get_analyser()->get_labelled_data();
+
+        // No training if no files have been provided.
+        if (empty($datasets) || empty($datasets[$this->model->timesplitting])) {
+
+            $result = new \stdClass();
+            $result->status = self::NO_DATASET;
+            $result->info = $this->get_analyser()->get_logs();
+            return $result;
+        }
+        $samplesfile = $datasets[$this->model->timesplitting];
+
+        // Train using the dataset.
+        $predictorresult = $predictor->train($this->get_unique_id(), $samplesfile, $outputdir);
+
+        $result = new \stdClass();
+        $result->status = $predictorresult->status;
+        $result->info = $predictorresult->info;
+
+        $this->flag_file_as_used($samplesfile, 'trained');
+
+        // Mark the model as trained if it wasn't.
+        if ($this->model->trained == false) {
+            $this->mark_as_trained();
+        }
+
+        return $result;
+    }
+
+    /**
+     * predict
+     *
+     * @return \stdClass
+     */
+    public function predict() {
+        global $DB;
+
+        // Increase memory limit.
+        $this->increase_memory();
+
+        if ($this->model->enabled == false || empty($this->model->timesplitting)) {
+            throw new \moodle_exception('invalidtimesplitting', 'analytics', '', $this->model->id);
+        }
+
+        if (empty($this->get_indicators())) {
+            throw new \moodle_exception('errornoindicators', 'analytics');
+        }
+
+        // Before get_unlabelled_data call so we get an early exception if it is not writable.
+        $outputdir = $this->get_output_dir(array('execution'));
+
+        // Before get_unlabelled_data call so we get an early exception if it is not ready.
+        $predictor = \core_analytics\manager::get_predictions_processor();
+
+        $samplesdata = $this->get_analyser()->get_unlabelled_data();
+
+        // Get the prediction samples file.
+        if (empty($samplesdata) || empty($samplesdata[$this->model->timesplitting])) {
+
+            $result = new \stdClass();
+            $result->status = self::NO_DATASET;
+            $result->info = $this->get_analyser()->get_logs();
+            return $result;
+        }
+        $samplesfile = $samplesdata[$this->model->timesplitting];
+
+        // We need to throw an exception if we are trying to predict stuff that was already predicted.
+        $params = array('modelid' => $this->model->id, 'fileid' => $samplesfile->get_id(), 'action' => 'predicted');
+        if ($predicted = $DB->get_record('analytics_used_files', $params)) {
+            throw new \moodle_exception('erroralreadypredict', 'analytics', '', $samplesfile->get_id());
+        }
+
+        $predictorresult = $predictor->predict($this->get_unique_id(), $samplesfile, $outputdir);
+
+        $result = new \stdClass();
+        $result->status = $predictorresult->status;
+        $result->info = $predictorresult->info;
+
+        $calculations = \core_analytics\dataset_manager::get_structured_data($samplesfile);
+
+        // Here we will store all predictions' contexts, this will be used to limit which users will see those predictions.
+        $samplecontexts = array();
+
+        if ($predictorresult) {
+            $result->predictions = $predictorresult->predictions;
+            foreach ($result->predictions as $sampleinfo) {
+
+                // We parse each prediction
+                switch (count($sampleinfo)) {
+                    case 1:
+                        // For whatever reason the predictions processor could not process this sample, we
+                        // skip it and do nothing with it.
+                        debugging($this->model->id . ' model predictions processor could not process the sample with id ' .
+                            $sampleinfo[0], DEBUG_DEVELOPER);
+                        continue;
+                    case 2:
+                        // Prediction processors that do not return a prediction score will have the maximum prediction
+                        // score.
+                        list($uniquesampleid, $prediction) = $sampleinfo;
+                        $predictionscore = 1;
+                        break;
+                    case 3:
+                        list($uniquesampleid, $prediction, $predictionscore) = $sampleinfo;
+                        break;
+                    default:
+                        break;
+                }
+
+                if ($this->get_target()->triggers_callback($prediction, $predictionscore)) {
+
+                    // The unique sample id contains both the sampleid and the rangeindex.
+                    list($sampleid, $rangeindex) = $this->get_time_splitting()->infer_sample_info($uniquesampleid);
+
+                    // Store the predicted values.
+                    $samplecontext = $this->save_prediction($sampleid, $rangeindex, $prediction, $predictionscore,
+                        json_encode($calculations[$uniquesampleid]));
+
+                    // Also store all samples context to later generate insights or whatever action the target wants to perform.
+                    $samplecontexts[$samplecontext->id] = $samplecontext;
+
+                    $this->get_target()->prediction_callback($this->model->id, $sampleid, $rangeindex, $samplecontext,
+                        $prediction, $predictionscore);
+                }
+            }
+        }
+
+        if (!empty($samplecontexts)) {
+            // Notify the target that all predictions have been processed.
+            $this->get_target()->generate_insights($this->model->id, $samplecontexts);
+
+            // Aggressive invalidation, the cost of filling up the cache is not high.
+            $cache = \cache::make('core', 'modelswithpredictions');
+            foreach ($samplecontexts as $context) {
+                $cache->delete($context->id);
+            }
+        }
+
+        $this->flag_file_as_used($samplesfile, 'predicted');
+
+        return $result;
+    }
+
+    /**
+     * save_prediction
+     *
+     * @param int $sampleid
+     * @param int $rangeindex
+     * @param int $prediction
+     * @param float $predictionscore
+     * @param string $calculations
+     * @return \context
+     */
+    protected function save_prediction($sampleid, $rangeindex, $prediction, $predictionscore, $calculations) {
+        global $DB;
+
+        $context = $this->get_analyser()->sample_access_context($sampleid);
+
+        $record = new \stdClass();
+        $record->modelid = $this->model->id;
+        $record->contextid = $context->id;
+        $record->sampleid = $sampleid;
+        $record->rangeindex = $rangeindex;
+        $record->prediction = $prediction;
+        $record->predictionscore = $predictionscore;
+        $record->calculations = $calculations;
+        $record->timecreated = time();
+        $DB->insert_record('analytics_predictions', $record);
+
+        return $context;
+    }
+
+    /**
+     * enable
+     *
+     * @param string $timesplittingid
+     * @return void
+     */
+    public function enable($timesplittingid = false) {
+        global $DB;
+
+        $now = time();
+
+        if ($timesplittingid && $timesplittingid !== $this->model->timesplitting) {
+
+            if (!\core_analytics\manager::is_valid($timesplittingid, '\core_analytics\local\time_splitting\base')) {
+                throw new \moodle_exception('errorinvalidtimesplitting', 'analytics');
+            }
+
+            if (substr($timesplittingid, 0, 1) !== '\\') {
+                throw new \moodle_exception('errorinvalidtimesplitting', 'analytics');
+            }
+
+            $this->model->timesplitting = $timesplittingid;
+            $this->model->version = $now;
+        }
+        $this->model->enabled = 1;
+        $this->model->timemodified = $now;
+
+        // We don't always update timemodified intentionally as we reserve it for target, indicators or timesplitting updates.
+        $DB->update_record('analytics_models', $this->model);
+
+        // It needs to be reset (just in case, we may already used it).
+        $this->uniqueid = null;
+    }
+
+    /**
+     * is_enabled
+     *
+     * @return bool
+     */
+    public function is_enabled() {
+        return (bool)$this->model->enabled;
+    }
+
+    /**
+     * is_trained
+     *
+     * @return bool
+     */
+    public function is_trained() {
+        return (bool)$this->model->trained;
+    }
+
+    /**
+     * mark_as_trained
+     *
+     * @return void
+     */
+    public function mark_as_trained() {
+        global $DB;
+
+        $this->model->trained = 1;
+        $DB->update_record('analytics_models', $this->model);
+    }
+
+    /**
+     * get_predictions_contexts
+     *
+     * @return \stdClass[]
+     */
+    public function get_predictions_contexts() {
+        global $DB;
+
+        $sql = "SELECT DISTINCT contextid FROM {analytics_predictions} WHERE modelid = ?";
+        return $DB->get_records_sql($sql, array($this->model->id));
+    }
+
+    /**
+     * Whether predictions exist for this context.
+     *
+     * @param \context $context
+     * @return bool
+     */
+    public function predictions_exist(\context $context) {
+        global $DB;
+
+        // Filters out previous predictions keeping only the last time range one.
+        $select = "modelid = :modelid AND contextid = :contextid";
+        $params = array($this->model->id, $context->id);
+        return $DB->record_exists_select('analytics_predictions', $select, $params);
+    }
+
+    /**
+     * Gets the predictions for this context.
+     *
+     * @param \context $context
+     * @return \core_analytics\prediction[]
+     */
+    public function get_predictions(\context $context) {
+        global $DB;
+
+        // Filters out previous predictions keeping only the last time range one.
+        $sql = "SELECT tip.*
+                  FROM {analytics_predictions} tip
+                  JOIN (
+                    SELECT sampleid, max(rangeindex) AS rangeindex
+                      FROM {analytics_predictions}
+                     WHERE modelid = ? and contextid = ?
+                    GROUP BY sampleid
+                  ) tipsub
+                  ON tip.sampleid = tipsub.sampleid AND tip.rangeindex = tipsub.rangeindex
+                 WHERE tip.modelid = ? and tip.contextid = ?";
+        $params = array($this->model->id, $context->id, $this->model->id, $context->id);
+        if (!$predictions = $DB->get_records_sql($sql, $params)) {
+            return array();
+        }
+
+        // Get predicted samples' ids.
+        $sampleids = array_map(function($prediction) {
+            return $prediction->sampleid;
+        }, $predictions);
+
+        list($unused, $samplesdata) = $this->get_analyser()->get_samples($sampleids);
+
+        // Add samples data as part of each prediction.
+        foreach ($predictions as $predictionid => $predictiondata) {
+
+            $sampleid = $predictiondata->sampleid;
+
+            // Filter out predictions which samples are not available anymore.
+            if (empty($samplesdata[$sampleid])) {
+                unset($predictions[$predictionid]);
+                continue;
+            }
+
+            // Replace stdClass object by \core_analytics\prediction objects.
+            $prediction = new \core_analytics\prediction($predictiondata, $samplesdata[$sampleid]);
+
+            $predictions[$predictionid] = $prediction;
+        }
+
+        return $predictions;
+    }
+
+    /**
+     * prediction_sample_data
+     *
+     * @param \stdClass $predictionobj
+     * @return array
+     */
+    public function prediction_sample_data($predictionobj) {
+
+        list($unused, $samplesdata) = $this->get_analyser()->get_samples(array($predictionobj->sampleid));
+
+        if (empty($samplesdata[$predictionobj->sampleid])) {
+            throw new \moodle_exception('errorsamplenotavailable', 'analytics');
+        }
+
+        return $samplesdata[$predictionobj->sampleid];
+    }
+
+    /**
+     * prediction_sample_description
+     *
+     * @param \core_analytics\prediction $prediction
+     * @return array 2 elements: list(string, \renderable)
+     */
+    public function prediction_sample_description(\core_analytics\prediction $prediction) {
+        return $this->get_analyser()->sample_description($prediction->get_prediction_data()->sampleid,
+            $prediction->get_prediction_data()->contextid, $prediction->get_sample_data());
+    }
+
+    /**
+     * Returns the output directory for prediction processors.
+     *
+     * Directory structure as follows:
+     * - Evaluation runs:
+     *   models/$model->id/$model->version/evaluation/$model->timesplitting
+     * - Training  & prediction runs:
+     *   models/$model->id/$model->version/execution
+     *
+     * @param array $subdirs
+     * @return string
+     */
+    protected function get_output_dir($subdirs = array()) {
+        global $CFG;
+
+        $subdirstr = '';
+        foreach ($subdirs as $subdir) {
+            $subdirstr .= DIRECTORY_SEPARATOR . $subdir;
+        }
+
+        $outputdir = get_config('analytics', 'modeloutputdir');
+        if (empty($outputdir)) {
+            // Apply default value.
+            $outputdir = rtrim($CFG->dataroot, '/') . DIRECTORY_SEPARATOR . 'models';
+        }
+
+        // Append model id and version + subdirs.
+        $outputdir .= DIRECTORY_SEPARATOR . $this->model->id . DIRECTORY_SEPARATOR . $this->model->version . $subdirstr;
+
+        make_writable_directory($outputdir);
+
+        return $outputdir;
+    }
+
+    /**
+     * get_unique_id
+     *
+     * @return string
+     */
+    public function get_unique_id() {
+        global $CFG;
+
+        if (!is_null($this->uniqueid)) {
+            return $this->uniqueid;
+        }
+
+        // Generate a unique id for this site, this model and this time splitting method, considering the last time
+        // that the model target and indicators were updated.
+        $ids = array($CFG->wwwroot, $CFG->dirroot, $CFG->prefix, $this->model->id, $this->model->version);
+        $this->uniqueid = sha1(implode('$$', $ids));
+
+        return $this->uniqueid;
+    }
+
+    /**
+     * Exports the model data.
+     *
+     * @return \stdClass
+     */
+    public function export() {
+        $data = clone $this->model;
+        $data->target = $this->get_target()->get_name();
+
+        if ($timesplitting = $this->get_time_splitting()) {
+            $data->timesplitting = $timesplitting->get_name();
+        }
+
+        $data->indicators = array();
+        foreach ($this->get_indicators() as $indicator) {
+            $data->indicators[] = $indicator->get_name();
+        }
+        return $data;
+    }
+
+    /**
+     * flag_file_as_used
+     *
+     * @param \stored_file $file
+     * @param string $action
+     * @return void
+     */
+    protected function flag_file_as_used(\stored_file $file, $action) {
+        global $DB;
+
+        $usedfile = new \stdClass();
+        $usedfile->modelid = $this->model->id;
+        $usedfile->fileid = $file->get_id();
+        $usedfile->action = $action;
+        $usedfile->time = time();
+        $DB->insert_record('analytics_used_files', $usedfile);
+    }
+
+    /**
+     * log_result
+     *
+     * @param string $timesplittingid
+     * @param float $score
+     * @param string $dir
+     * @param array $info
+     * @return int The inserted log id
+     */
+    protected function log_result($timesplittingid, $score, $dir = false, $info = false) {
+        global $DB, $USER;
+
+        $log = new \stdClass();
+        $log->modelid = $this->get_id();
+        $log->version = $this->model->version;
+        $log->target = $this->model->target;
+        $log->indicators = $this->model->indicators;
+        $log->timesplitting = $timesplittingid;
+        $log->dir = $dir;
+        if ($info) {
+            // Ensure it is not an associative array.
+            $log->info = json_encode(array_values($info));
+        }
+        $log->score = $score;
+        $log->timecreated = time();
+        $log->usermodified = $USER->id;
+
+        return $DB->insert_record('analytics_models_log', $log);
+    }
+
+    /**
+     * Utility method to return indicator class names from a list of indicator objects
+     *
+     * @param \core_analytics\local\indicator\base[] $indicators
+     * @return string[]
+     */
+    private static function indicator_classes($indicators) {
+
+        // What we want to check and store are the indicator classes not the keys.
+        $indicatorclasses = array();
+        foreach ($indicators as $indicator) {
+            if (!\core_analytics\manager::is_valid($indicator, '\core_analytics\local\indicator\base')) {
+                if (!is_object($indicator) && !is_scalar($indicator)) {
+                    $indicator = strval($indicator);
+                } else if (is_object($indicator)) {
+                    $indicator = get_class($indicator);
+                }
+                throw new \moodle_exception('errorinvalidindicator', 'analytics', '', $indicator);
+            }
+            $indicatorclasses[] = '\\' . get_class($indicator);
+        }
+
+        return $indicatorclasses;
+    }
+
+    /**
+     * Clears the model training and prediction data.
+     *
+     * Executed after updating model critical elements like the time splitting method
+     * or the indicators.
+     *
+     * @return void
+     */
+    private function clear_model() {
+        global $DB;
+
+        $DB->delete_records('analytics_predict_ranges', array('modelid' => $this->model->id));
+        $DB->delete_records('analytics_predictions', array('modelid' => $this->model->id));
+        $DB->delete_records('analytics_train_samples', array('modelid' => $this->model->id));
+        $DB->delete_records('analytics_used_files', array('modelid' => $this->model->id));
+
+        $cache = \cache::make('core', 'modelswithpredictions');
+        $result = $cache->purge();
+    }
+
+    private function increase_memory() {
+        if (ini_get('memory_limit') != -1) {
+            raise_memory_limit(MEMORY_HUGE);
+        }
+    }
+
+}
diff --git a/analytics/classes/prediction.php b/analytics/classes/prediction.php
new file mode 100644 (file)
index 0000000..d81c465
--- /dev/null
@@ -0,0 +1,104 @@
+<?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/>.
+
+/**
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@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();
+
+/**
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class prediction {
+
+    private $prediction;
+
+    private $sampledata;
+
+    private $calculations = array();
+
+    public function __construct($prediction, $sampledata) {
+        global $DB;
+
+        if (is_scalar($prediction)) {
+            $prediction = $DB->get_record('analytics_predictions', array('id' => $prediction), '*', MUST_EXIST);
+        }
+        $this->prediction = $prediction;
+
+        $this->sampledata = $sampledata;
+
+        $this->format_calculations();
+    }
+
+    public function get_prediction_data() {
+        return $this->prediction;
+    }
+
+    public function get_sample_data() {
+        return $this->sampledata;
+    }
+
+    public function get_calculations() {
+        return $this->calculations;
+    }
+
+    private function format_calculations() {
+
+        $calculations = json_decode($this->prediction->calculations, true);
+
+        foreach ($calculations as $featurename => $value) {
+
+            list($indicatorclass, $subtype) = $this->parse_feature_name($featurename);
+
+            if ($indicatorclass === 'range') {
+                // Time range indicators don't belong to any indicator class, we don't store them.
+                continue;
+            } else if (!\core_analytics\manager::is_valid($indicatorclass, '\core_analytics\local\indicator\base')) {
+                throw new \moodle_exception('errorpredictionformat', 'analytics');
+            }
+
+            $this->calculations[$featurename] = new \stdClass();
+            $this->calculations[$featurename]->subtype = $subtype;
+            $this->calculations[$featurename]->indicator = \core_analytics\manager::get_indicator($indicatorclass);
+            $this->calculations[$featurename]->value = $value;
+        }
+    }
+
+    private function parse_feature_name($featurename) {
+
+        $indicatorclass = $featurename;
+        $subtype = false;
+
+        // Some indicator result in more than 1 feature, we need to see which feature are we dealing with.
+        $separatorpos = strpos($featurename, '/');
+        if ($separatorpos !== false) {
+   &nb