Part of MDL-57791 epic.
--- /dev/null
+<?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));
+
+ }
+}
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<?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();
+}
--- /dev/null
+<?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.');
+ }
+}
--- /dev/null
+<?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);
+ }
+}
--- /dev/null
+<?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;
+ }
+
+}
--- /dev/null
+<?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');
+ }
+}
--- /dev/null
+<?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);
+ }
+ }
+}
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<?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);
+ }
+}
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<?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']));
+ }
+
+}
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<?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();
+
+ }
+}
--- /dev/null
+<?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();
+ }
+}
--- /dev/null
+<?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();
+ }
+}
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<?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';
+ }
+}
--- /dev/null
+<?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);
+ }
+}
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<?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';
+ }
+}
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<?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';
+ }
+}
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<?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';
+ }
+}
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<?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';
+ }
+}
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<?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);
+ }
+}
--- /dev/null
+<?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';
+ }
+}
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<?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';
+ }
+}
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<?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';
+ }
+}
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<?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';
+ }
+}
--- /dev/null
+<?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;
+ }
+
+}
--- /dev/null
+<?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';
+ }
+}
--- /dev/null
+<?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;
+ }
+
+}
--- /dev/null
+<?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';
+ }
+}
--- /dev/null
+<?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;
+ }
+
+}
--- /dev/null
+<?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';
+ }
+}
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<?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');
+ }
+
+}
--- /dev/null
+<?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';
+ }
+}
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<?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';
+ }
+}
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<?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';
+ }
+}
--- /dev/null
+<?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);
+ }
+}
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<?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';
+ }
+}
--- /dev/null
+<?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');
+ }
+}
--- /dev/null
+<?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';
+ }
+}
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<?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';
+ }
+}
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<?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';
+ }
+}
--- /dev/null
+<?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);
+ }
+}
--- /dev/null
+<?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();
+ }
+}
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<?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';
+ }
+}
--- /dev/null
+<?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');
+ }
+}
--- /dev/null
+<?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';
+ }
+}
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<?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')
+ );
+ }
+
+}
--- /dev/null
+<?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');
+ }
+}
--- /dev/null
+<?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');
+ }
+}
--- /dev/null
+<?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.');
+ }
+ }
+ }
+}
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<?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
+ ]
+ ];
+ }
+}
--- /dev/null
+<?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)
+ ]
+ ];
+ }
+}
--- /dev/null
+<?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)
+ ]
+ ];
+ }
+}
--- /dev/null
+<?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()
+ ]
+ ];
+ }
+}
--- /dev/null
+<?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;
+ }
+
+}
--- /dev/null
+<?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;
+ }
+
+}
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<?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);
+ }
+ }
+
+}
--- /dev/null
+<?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