lib/editor/tinymce/plugins/pdw/tinymce/
lib/editor/tinymce/plugins/spellchecker/rpc.php
lib/editor/tinymce/tiny_mce/
+lib/mlbackend/php/phpml/
lib/adodb/
lib/bennu/
lib/evalmath/
lib/editor/tinymce/plugins/pdw/tinymce/
lib/editor/tinymce/plugins/spellchecker/rpc.php
lib/editor/tinymce/tiny_mce/
+lib/mlbackend/php/phpml/
lib/adodb/
lib/bennu/
lib/evalmath/
{
+ "plugins": [
+ "stylelint-csstree-validator"
+ ],
"rules": {
+ "csstree/validator": true,
"at-rule-empty-line-before": [ "always",
- {"except": [ "blockless-group"], ignore: ["after-comment", "all-nested"]}
+ {"except": [ "blockless-after-blockless"], ignore: ["after-comment", "inside-block"]}
],
"at-rule-name-case": "lower",
"at-rule-name-space-after": "always-single-line",
"at-rule-no-unknown": null, # Enabled for non-scss in grunt.
"at-rule-semicolon-newline-after": "always",
+ "at-rule-semicolon-space-before": "never",
"block-closing-brace-newline-after": "always",
- "block-closing-brace-newline-before": "always-multi-line",
+ "block-closing-brace-newline-before": "always",
"block-closing-brace-space-before": "always-single-line",
"block-no-empty": true,
- "block-no-single-line": true,
- "block-opening-brace-newline-after": "always-multi-line",
+ "block-opening-brace-newline-after": "always",
"block-opening-brace-space-after": "always-single-line",
"block-opening-brace-space-before": "always",
"color-hex-case": ["lower", { "severity": "warning" }],
"declaration-bang-space-after": "never",
"declaration-bang-space-before": "always",
"declaration-block-no-duplicate-properties": true,
- "declaration-block-no-ignored-properties": true,
"declaration-block-no-shorthand-property-overrides": true,
"declaration-block-semicolon-newline-after": "always-multi-line",
"declaration-block-semicolon-space-after": "always-single-line",
"declaration-colon-space-after": "always-single-line",
"declaration-colon-space-before": "never",
"declaration-no-important": true,
+ "font-family-no-duplicate-names": true,
"function-calc-no-unspaced-operator": true,
"function-comma-newline-after": "always-multi-line",
"function-comma-space-after": "always-single-line",
"function-name-case": "lower",
"function-parentheses-newline-inside": "always-multi-line",
"function-parentheses-space-inside": "never-single-line",
- "function-url-data-uris": never,
+ "function-url-scheme-blacklist": ["data"],
"function-whitespace-after": "always",
"indentation": 4,
"keyframe-declaration-no-important": true,
"max-line-length": [132, { "severity": "warning" }],
"media-feature-colon-space-after": "always",
"media-feature-colon-space-before": "never",
- "media-feature-no-missing-punctuation": true,
"media-feature-parentheses-space-inside": "never",
"media-feature-range-operator-space-after": "always",
"media-feature-range-operator-space-before": "always",
"media-query-list-comma-newline-after": "always-multi-line",
"media-query-list-comma-space-after": "always-single-line",
"media-query-list-comma-space-before": "never",
- "no-browser-hacks": null, # Enabled for non-scss in grunt.
"no-empty-source": true,
"no-eol-whitespace": true,
"no-extra-semicolons": [true, { "severity": "warning" }],
"selector-pseudo-class-parentheses-space-inside": "never",
"selector-pseudo-element-case": "lower",
"selector-pseudo-element-no-unknown": true,
- "selector-root-no-composition": true,
"selector-type-case": "lower",
"selector-type-no-unknown": true,
"string-no-newline": true,
- "time-no-imperceptible": true,
+ "time-min-milliseconds": 100,
"unit-blacklist": ["pt"],
"unit-case": "lower",
"unit-no-unknown": true,
rules: {
// These rules have to be disabled in .stylelintrc for scss compat.
"at-rule-no-unknown": true,
- "no-browser-hacks": [true, {"severity": "warning"}]
}
}
},
rules: {
// These rules have to be disabled in .stylelintrc for scss compat.
"at-rule-no-unknown": true,
- "no-browser-hacks": [true, {"severity": "warning"}]
}
}
}
--- /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)
+ );
+
+ // Log store.
+ $logmanager = get_log_manager();
+ $readers = $logmanager->get_readers('core\log\sql_reader');
+ $options = array();
+ $defaultreader = null;
+ foreach ($readers as $plugin => $reader) {
+ if (!$reader->is_logging()) {
+ continue;
+ }
+ if (!isset($defaultreader)) {
+ // The top one as default reader.
+ $defaultreader = $plugin;
+ }
+ $options[$plugin] = $reader->get_name();
+ }
+
+ if (empty($defaultreader)) {
+ // We fall here during initial site installation because log stores are not
+ // enabled until admin/tool/log/db/install.php is executed and get_readers
+ // return nothing.
+
+ if ($enabledlogstores = get_config('tool_log', 'enabled_stores')) {
+ $enabledlogstores = explode(',', $enabledlogstores);
+ $defaultreader = reset($enabledlogstores);
+
+ // No need to set the correct name, just the value, this will not be displayed.
+ $options[$defaultreader] = $defaultreader;
+ }
+ }
+ $settings->add(new admin_setting_configselect('analytics/logstore',
+ new lang_string('analyticslogstore', 'analytics'), new lang_string('analyticslogstore_help', 'analytics'),
+ $defaultreader, $options));
+
+ // Enable/disable time splitting methods.
+ $alltimesplittings = \core_analytics\manager::get_all_time_splittings();
+
+ $timesplittingoptions = array();
+ $timesplittingdefaults = array('\core\analytics\time_splitting\quarters_accum',
+ '\core\analytics\time_splitting\quarters', '\core\analytics\time_splitting\no_splitting');
+ 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';
+ if (empty(get_config('analytics', 'modeloutputdir')) && !file_exists($defaultmodeloutputdir) &&
+ is_writable($defaultmodeloutputdir)) {
+ // Automatically create the dir for them so users don't see the invalid value red cross.
+ mkdir($defaultmodeloutputdir, $CFG->directorypermissions, true);
+ }
+ $settings->add(new admin_setting_configdirectory('analytics/modeloutputdir', new lang_string('modeloutputdir', 'analytics'),
+ new lang_string('modeloutputdirinfo', 'analytics'), $defaultmodeloutputdir));
+ }
+}
new lang_string('passwordchangetokendeletion', 'admin'),
new lang_string('passwordchangetokendeletion_desc', 'admin'), 0));
+ $temp->add(new admin_setting_configduration('tokenduration',
+ new lang_string('tokenduration', 'admin'),
+ new lang_string('tokenduration_desc', 'admin'), 12 * WEEKSECS, WEEKSECS));
+
$temp->add(new admin_setting_configcheckbox('groupenrolmentkeypolicy', new lang_string('groupenrolmentkeypolicy', 'admin'), new lang_string('groupenrolmentkeypolicy_desc', 'admin'), 1));
$temp->add(new admin_setting_configcheckbox('disableuserimages', new lang_string('disableuserimages', 'admin'), new lang_string('configdisableuserimages', 'admin'), 0));
$temp->add(new admin_setting_configcheckbox('emailchangeconfirmation', new lang_string('emailchangeconfirmation', 'admin'), new lang_string('configemailchangeconfirmation', 'admin'), 1));
'phone2' => new lang_string('phone2'),
'department' => new lang_string('department'),
'institution' => new lang_string('institution'),
+ 'city' => new lang_string('city'),
+ 'country' => new lang_string('country'),
)));
$setting = new admin_setting_configtext('fullnamedisplay', new lang_string('fullnamedisplay', 'admin'),
new lang_string('configfullnamedisplay', 'admin'), 'language', PARAM_TEXT, 50);
--- /dev/null
+// 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/>.
+
+/**
+ * Shows a dialogue with info about this logs.
+ *
+ * @module tool_analytics/log_info
+ * @class log_info
+ * @package tool_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(['jquery', 'core/str', 'core/modal_factory', 'core/notification'], function($, str, ModalFactory, Notification) {
+
+ return /** @alias module:tool_analytics/log_info */ {
+
+ /**
+ * Prepares a modal info for a log's results.
+ *
+ * @method loadInfo
+ * @param {int} id
+ * @param {string[]} info
+ */
+ loadInfo: function(id, info) {
+
+ var link = $('[data-model-log-id="' + id + '"]');
+ str.get_string('loginfo', 'tool_analytics').then(function(langString) {
+
+ var bodyInfo = $("<ul>");
+ info.forEach(function(item) {
+ bodyInfo.append('<li>' + item + '</li>');
+ });
+ bodyInfo.append("</ul>");
+
+ return ModalFactory.create({
+ title: langString,
+ body: bodyInfo.html(),
+ large: true,
+ }, link);
+
+ }).catch(Notification.exception);
+ }
+ };
+});
--- /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/>.
+
+/**
+ * Model edit form.
+ *
+ * @package tool_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_analytics\output\form;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot.'/lib/formslib.php');
+
+/**
+ * Model edit form.
+ *
+ * @package tool_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class edit_model extends \moodleform {
+
+ /**
+ * Form definition
+ */
+ public function definition() {
+ global $OUTPUT;
+
+ $mform = $this->_form;
+
+ if ($this->_customdata['model']->get_model_obj()->trained == 1) {
+ $message = get_string('edittrainedwarning', 'tool_analytics');
+ $mform->addElement('html', $OUTPUT->notification($message, \core\output\notification::NOTIFY_WARNING));
+ }
+
+ $mform->addElement('advcheckbox', 'enabled', get_string('enabled', 'tool_analytics'));
+
+ $indicators = array();
+ foreach ($this->_customdata['indicators'] as $classname => $indicator) {
+ $optionname = \tool_analytics\output\helper::class_to_option($classname);
+ $indicators[$optionname] = $indicator->get_name();
+ }
+ $options = array(
+ 'multiple' => true
+ );
+ $mform->addElement('autocomplete', 'indicators', get_string('indicators', 'tool_analytics'), $indicators, $options);
+ $mform->setType('indicators', PARAM_ALPHANUMEXT);
+
+ $timesplittings = array('' => '');
+ foreach ($this->_customdata['timesplittings'] as $classname => $timesplitting) {
+ $optionname = \tool_analytics\output\helper::class_to_option($classname);
+ $timesplittings[$optionname] = $timesplitting->get_name();
+ }
+
+ $mform->addElement('select', 'timesplitting', get_string('timesplittingmethod', 'analytics'), $timesplittings);
+ $mform->addHelpButton('timesplitting', 'timesplittingmethod', 'analytics');
+
+ $mform->addElement('hidden', 'id', $this->_customdata['id']);
+ $mform->setType('id', PARAM_INT);
+
+ $mform->addElement('hidden', 'action', 'edit');
+ $mform->setType('action', PARAM_ALPHANUMEXT);
+
+ $this->add_action_buttons();
+ }
+
+ /**
+ * Form validation
+ *
+ * @param array $data data from the form.
+ * @param array $files files uploaded.
+ *
+ * @return array of errors.
+ */
+ public function validation($data, $files) {
+ $errors = parent::validation($data, $files);
+
+ if (!empty($data['timesplitting'])) {
+ $realtimesplitting = \tool_analytics\output\helper::option_to_class($data['timesplitting']);
+ if (\core_analytics\manager::is_valid($realtimesplitting, '\core_analytics\local\time_splitting\base') === false) {
+ $errors['timesplitting'] = get_string('errorinvalidtimesplitting', 'analytics');
+ }
+ }
+
+ if (empty($data['indicators'])) {
+ $errors['indicators'] = get_string('errornoindicators', 'analytics');
+ } else {
+ foreach ($data['indicators'] as $indicator) {
+ $realindicatorname = \tool_analytics\output\helper::option_to_class($indicator);
+ if (\core_analytics\manager::is_valid($realindicatorname, '\core_analytics\local\indicator\base') === false) {
+ $errors['indicators'] = get_string('errorinvalidindicator', 'analytics', $realindicatorname);
+ }
+ }
+ }
+
+ if (!empty($data['enabled']) && empty($data['timesplitting'])) {
+ $errors['enabled'] = get_string('errorcantenablenotimesplitting', 'tool_analytics');
+ }
+
+ return $errors;
+ }
+}
--- /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/>.
+
+/**
+ * Typical crappy helper class with tiny functions.
+ *
+ * @package tool_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_analytics\output;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Helper class with general purpose tiny functions.
+ *
+ * @package tool_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class helper {
+
+ /**
+ * Converts a class full name to a select option key
+ *
+ * @param string $class
+ * @return string
+ */
+ public static function class_to_option($class) {
+ // Form field is PARAM_ALPHANUMEXT and we are sending fully qualified class names
+ // as option names, but replacing the backslash for a string that is really unlikely
+ // to ever be part of a class name.
+ return str_replace('\\', '2015102400ouuu', $class);
+ }
+
+ /**
+ * option_to_class
+ *
+ * @param string $option
+ * @return string
+ */
+ public static function option_to_class($option) {
+ // Really unlikely but yeah, I'm a bad booyyy.
+ return str_replace('2015102400ouuu', '\\', $option);
+ }
+}
--- /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/>.
+
+/**
+ * Model logs table class.
+ *
+ * @package tool_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_analytics\output;
+
+defined('MOODLE_INTERNAL') || die;
+require_once($CFG->libdir . '/tablelib.php');
+
+/**
+ * Model logs table class.
+ *
+ * @package tool_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class model_logs extends \table_sql {
+
+ /**
+ * @var \core_analytics\model
+ */
+ protected $model = null;
+
+ /**
+ * Sets up the table_log parameters.
+ *
+ * @param string $uniqueid unique id of form.
+ * @param \core_analytics\model $model
+ */
+ public function __construct($uniqueid, $model) {
+ global $PAGE;
+
+ parent::__construct($uniqueid);
+
+ $this->model = $model;
+
+ $this->set_attribute('class', 'modellog generaltable generalbox');
+ $this->set_attribute('aria-live', 'polite');
+
+ $this->define_columns(array('time', 'version', 'indicators', 'timesplitting', 'accuracy', 'info', 'usermodified'));
+ $this->define_headers(array(
+ get_string('time'),
+ get_string('version'),
+ get_string('indicators', 'tool_analytics'),
+ get_string('timesplittingmethod', 'analytics'),
+ get_string('accuracy', 'tool_analytics'),
+ get_string('info', 'tool_analytics'),
+ get_string('fullnameuser'),
+ ));
+ $this->pageable(true);
+ $this->collapsible(false);
+ $this->sortable(false);
+ $this->is_downloadable(false);
+
+ $this->define_baseurl($PAGE->url);
+ }
+
+ /**
+ * Generate the version column.
+ *
+ * @param \stdClass $log log data.
+ * @return string HTML for the version column
+ */
+ public function col_version($log) {
+ $recenttimestr = get_string('strftimerecent', 'core_langconfig');
+ return userdate($log->version, $recenttimestr);
+ }
+
+ /**
+ * Generate the time column.
+ *
+ * @param \stdClass $log log data.
+ * @return string HTML for the time column
+ */
+ public function col_time($log) {
+ $recenttimestr = get_string('strftimerecent', 'core_langconfig');
+ return userdate($log->timecreated, $recenttimestr);
+ }
+
+ /**
+ * Generate the indicators column.
+ *
+ * @param \stdClass $log log data.
+ * @return string HTML for the indicators column
+ */
+ public function col_indicators($log) {
+ $indicatorclasses = json_decode($log->indicators);
+ $indicators = array();
+ foreach ($indicatorclasses as $indicatorclass) {
+ $indicator = \core_analytics\manager::get_indicator($indicatorclass);
+ if ($indicator) {
+ $indicators[] = $indicator->get_name();
+ } else {
+ debugging('Can\'t load ' . $indicatorclass . ' indicator', DEBUG_DEVELOPER);
+ }
+ }
+ return '<ul><li>' . implode('</li><li>', $indicators) . '</li></ul>';
+ }
+
+ /**
+ * Generate the context column.
+ *
+ * @param \stdClass $log log data.
+ * @return string HTML for the context column
+ */
+ public function col_timesplitting($log) {
+ $timesplitting = \core_analytics\manager::get_time_splitting($log->timesplitting);
+ return $timesplitting->get_name();
+ }
+
+ /**
+ * Generate the accuracy column.
+ *
+ * @param \stdClass $log log data.
+ * @return string HTML for the accuracy column
+ */
+ public function col_accuracy($log) {
+ return strval(round($log->score * 100, 2)) . '%';
+ }
+
+ /**
+ * Generate the info column.
+ *
+ * @param \stdClass $log log data.
+ * @return string HTML for the score column
+ */
+ public function col_info($log) {
+ global $PAGE;
+
+ if (empty($log->info) && empty($log->dir)) {
+ return '';
+ }
+
+ $info = array();
+ if (!empty($log->info)) {
+ $info = json_decode($log->info);
+ }
+ if (!empty($log->dir)) {
+ $info[] = get_string('predictorresultsin', 'tool_analytics', $log->dir);
+ }
+ $PAGE->requires->js_call_amd('tool_analytics/log_info', 'loadInfo', array($log->id, $info));
+ return \html_writer::link('#', get_string('view'), array('data-model-log-id' => $log->id));
+ }
+
+ /**
+ * Generate the usermodified column.
+ *
+ * @param \stdClass $log log data.
+ * @return string HTML for the usermodified column
+ */
+ public function col_usermodified($log) {
+ $user = \core_user::get_user($log->usermodified);
+ return fullname($user);
+ }
+
+ /**
+ * Query the logs table. Store results in the object for use by build_table.
+ *
+ * @param int $pagesize size of page for paginated displayed table.
+ * @param bool $useinitialsbar do you want to use the initials bar.
+ */
+ public function query_db($pagesize, $useinitialsbar = true) {
+ $total = count($this->model->get_logs());
+ $this->pagesize($pagesize, $total);
+ $this->rawdata = $this->model->get_logs($this->get_page_start(), $this->get_page_size());
+
+ // Set initial bars.
+ if ($useinitialsbar) {
+ $this->initialbars($total > $pagesize);
+ }
+ }
+}
--- /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 models list page.
+ *
+ * @package tool_analytics
+ * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_analytics\output;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Shows tool_analytics models list.
+ *
+ * @package tool_analytics
+ * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class models_list implements \renderable, \templatable {
+
+ /**
+ * models
+ *
+ * @var \core_analytics\model[]
+ */
+ protected $models = array();
+
+ /**
+ * __construct
+ *
+ * @param \core_analytics\model[] $models
+ * @return void
+ */
+ public function __construct($models) {
+ $this->models = $models;
+ }
+
+ /**
+ * Exports the data.
+ *
+ * @param \renderer_base $output
+ * @return \stdClass
+ */
+ public function export_for_template(\renderer_base $output) {
+
+ $data = new \stdClass();
+
+ $data->models = array();
+ foreach ($this->models as $model) {
+ $modeldata = $model->export();
+
+ // Model predictions list.
+ if ($model->uses_insights()) {
+ $predictioncontexts = $model->get_predictions_contexts();
+ if ($predictioncontexts) {
+
+ foreach ($predictioncontexts as $contextid => $unused) {
+ // We prepare this to be used as single_select template options.
+ $context = \context::instance_by_id($contextid);
+
+ // Special name for system level predictions as showing "System is not visually nice".
+ if ($contextid == SYSCONTEXTID) {
+ $contextname = get_string('allpredictions', 'tool_analytics');
+ } else {
+ $contextname = shorten_text($context->get_context_name(true, true), 90);
+ }
+ $predictioncontexts[$contextid] = $contextname;
+ }
+ \core_collator::asort($predictioncontexts);
+
+ if (!empty($predictioncontexts)) {
+ $url = new \moodle_url('/report/insights/insights.php', array('modelid' => $model->get_id()));
+ $singleselect = new \single_select($url, 'contextid', $predictioncontexts);
+ $modeldata->insights = $singleselect->export_for_template($output);
+ }
+ }
+
+ if (empty($modeldata->insights)) {
+ if ($model->any_prediction_obtained()) {
+ $modeldata->noinsights = get_string('noinsights', 'analytics');
+ } else {
+ $modeldata->noinsights = get_string('nopredictionsyet', 'analytics');
+ }
+ }
+
+ } else {
+ $modeldata->noinsights = get_string('noinsightsmodel', 'analytics');
+ }
+
+ // Actions.
+ $actionsmenu = new \action_menu();
+ $actionsmenu->set_menu_trigger(get_string('actions'));
+ $actionsmenu->set_owner_selector('model-actions-' . $model->get_id());
+ $actionsmenu->set_alignment(\action_menu::TL, \action_menu::BL);
+
+ // Edit model.
+ if (!$model->is_static()) {
+ $url = new \moodle_url('model.php', array('action' => 'edit', 'id' => $model->get_id()));
+ $icon = new \action_menu_link_secondary($url, new \pix_icon('t/edit', get_string('edit')), get_string('edit'));
+ $actionsmenu->add($icon);
+ }
+
+ // Enable / disable.
+ if ($model->is_enabled()) {
+ $action = 'disable';
+ $text = get_string('disable');
+ $icontype = 't/block';
+ } else {
+ $action = 'enable';
+ $text = get_string('enable');
+ $icontype = 'i/checked';
+ }
+ $url = new \moodle_url('model.php', array('action' => $action, 'id' => $model->get_id()));
+ $icon = new \action_menu_link_secondary($url, new \pix_icon($icontype, $text), $text);
+ $actionsmenu->add($icon);
+
+ // Evaluate machine-learning-based models.
+ if ($model->get_indicators() && !$model->is_static()) {
+ $url = new \moodle_url('model.php', array('action' => 'evaluate', 'id' => $model->get_id()));
+ $icon = new \action_menu_link_secondary($url, new \pix_icon('i/calc', get_string('evaluate', 'tool_analytics')),
+ get_string('evaluate', 'tool_analytics'));
+ $actionsmenu->add($icon);
+ }
+
+ if ($modeldata->enabled && !empty($modeldata->timesplitting)) {
+ $url = new \moodle_url('model.php', array('action' => 'getpredictions', 'id' => $model->get_id()));
+ $icon = new \action_menu_link_secondary($url, new \pix_icon('i/notifications',
+ get_string('getpredictions', 'tool_analytics')), get_string('getpredictions', 'tool_analytics'));
+ $actionsmenu->add($icon);
+ }
+
+ // Machine-learning-based models evaluation log.
+ if (!$model->is_static()) {
+ $url = new \moodle_url('model.php', array('action' => 'log', 'id' => $model->get_id()));
+ $icon = new \action_menu_link_secondary($url, new \pix_icon('i/report', get_string('viewlog', 'tool_analytics')),
+ get_string('viewlog', 'tool_analytics'));
+ $actionsmenu->add($icon);
+ }
+
+ $modeldata->actions = $actionsmenu->export_for_template($output);
+
+ $data->models[] = $modeldata;
+ }
+
+ $data->warnings = array(
+ (object)array('message' => get_string('bettercli', 'tool_analytics'), 'closebutton' => true)
+ );
+
+ 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/>.
+
+/**
+ * Renderer.
+ *
+ * @package tool_analytics
+ * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_analytics\output;
+
+defined('MOODLE_INTERNAL') || die();
+
+use plugin_renderer_base;
+use templatable;
+use renderable;
+
+/**
+ * Renderer class.
+ *
+ * @package tool_analytics
+ * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class renderer extends plugin_renderer_base {
+
+ /**
+ * Defer to template.
+ *
+ * @param \tool_analytics\output\models_list $modelslist
+ * @return string HTML
+ */
+ protected function render_models_list(\tool_analytics\output\models_list $modelslist) {
+ $data = $modelslist->export_for_template($this);
+ return parent::render_from_template('tool_analytics/models_list', $data);
+ }
+
+ /**
+ * Renders a table.
+ *
+ * @param \table_sql $table
+ * @return string HTML
+ */
+ public function render_table(\table_sql $table) {
+
+ ob_start();
+ $table->out(10, true);
+ $output = ob_get_contents();
+ ob_end_clean();
+
+ return $output;
+ }
+
+ /**
+ * Web interface evaluate results.
+ *
+ * @param \stdClass[] $results
+ * @param string[] $logs
+ * @return string HTML
+ */
+ public function render_evaluate_results($results, $logs = array()) {
+ global $OUTPUT;
+
+ $output = '';
+
+ foreach ($results as $timesplittingid => $result) {
+
+ if (!CLI_SCRIPT) {
+ $output .= $OUTPUT->box_start('generalbox m-b-3');
+ }
+
+ // Check that the array key is a string, not all results depend on time splitting methods (e.g. general errors).
+ if (!is_numeric($timesplittingid)) {
+ $timesplitting = \core_analytics\manager::get_time_splitting($timesplittingid);
+ $langstrdata = (object)array('name' => $timesplitting->get_name(), 'id' => $timesplittingid);
+
+ if (CLI_SCRIPT) {
+ $output .= $OUTPUT->heading(get_string('getpredictionsresultscli', 'tool_analytics', $langstrdata), 3);
+ } else {
+ $output .= $OUTPUT->heading(get_string('getpredictionsresults', 'tool_analytics', $langstrdata), 3);
+ }
+ }
+
+ if ($result->status == 0) {
+ $output .= $OUTPUT->notification(get_string('goodmodel', 'tool_analytics'),
+ \core\output\notification::NOTIFY_SUCCESS);
+ } else if ($result->status === \core_analytics\model::NO_DATASET) {
+ $output .= $OUTPUT->notification(get_string('nodatatoevaluate', 'tool_analytics'),
+ \core\output\notification::NOTIFY_WARNING);
+ }
+
+ if (isset($result->score)) {
+ // Score.
+ $output .= $OUTPUT->heading(get_string('accuracy', 'tool_analytics') . ': ' .
+ round(floatval($result->score), 4) * 100 . '%', 4);
+ }
+
+ if (!empty($result->info)) {
+ foreach ($result->info as $message) {
+ $output .= $OUTPUT->notification($message, \core\output\notification::NOTIFY_WARNING);
+ }
+ }
+
+ if (!CLI_SCRIPT) {
+ $output .= $OUTPUT->box_end();
+ }
+ }
+
+ // Info logged during evaluation.
+ if (!empty($logs) && debugging()) {
+ $output .= $OUTPUT->heading(get_string('extrainfo', 'tool_analytics'), 3);
+ foreach ($logs as $log) {
+ $output .= $OUTPUT->notification($log, \core\output\notification::NOTIFY_WARNING);
+ }
+ }
+
+ if (!CLI_SCRIPT) {
+ $output .= $OUTPUT->single_button(new \moodle_url('/admin/tool/analytics/index.php'), get_string('continue'));
+ }
+
+ return $output;
+ }
+
+
+ /**
+ * Web interface training & prediction results.
+ *
+ * @param \stdClass|false $trainresults
+ * @param string[] $trainlogs
+ * @param \stdClass|false $predictresults
+ * @param string[] $predictlogs
+ * @return string HTML
+ */
+ public function render_get_predictions_results($trainresults = false, $trainlogs = array(), $predictresults = false, $predictlogs = array()) {
+ global $OUTPUT;
+
+ $output = '';
+
+ if ($trainresults || (!empty($trainlogs) && debugging())) {
+ $output .= $OUTPUT->heading(get_string('trainingresults', 'tool_analytics'), 3);
+ }
+
+ if ($trainresults) {
+ if ($trainresults->status == 0) {
+ $output .= $OUTPUT->notification(get_string('trainingprocessfinished', 'tool_analytics'),
+ \core\output\notification::NOTIFY_SUCCESS);
+ } else if ($trainresults->status === \core_analytics\model::NO_DATASET) {
+ $output .= $OUTPUT->notification(get_string('nodatatotrain', 'tool_analytics'),
+ \core\output\notification::NOTIFY_WARNING);
+ } else {
+ $output .= $OUTPUT->notification(get_string('generalerror', 'analytics', $trainresults->status),
+ \core\output\notification::NOTIFY_ERROR);
+ }
+ }
+
+ if (!empty($trainlogs) && debugging()) {
+ $output .= $OUTPUT->heading(get_string('extrainfo', 'tool_analytics'), 4);
+ foreach ($trainlogs as $log) {
+ $output .= $OUTPUT->notification($log, \core\output\notification::NOTIFY_WARNING);
+ }
+ }
+
+ if ($predictresults || (!empty($predictlogs) && debugging())) {
+ $output .= $OUTPUT->heading(get_string('predictionresults', 'tool_analytics'), 3, 'main m-t-3');
+ }
+
+ if ($predictresults) {
+ if ($predictresults->status == 0) {
+ $output .= $OUTPUT->notification(get_string('predictionprocessfinished', 'tool_analytics'),
+ \core\output\notification::NOTIFY_SUCCESS);
+ } else if ($predictresults->status === \core_analytics\model::NO_DATASET) {
+ $output .= $OUTPUT->notification(get_string('nodatatopredict', 'tool_analytics'),
+ \core\output\notification::NOTIFY_WARNING);
+ } else {
+ $output .= $OUTPUT->notification(get_string('generalerror', 'analytics', $predictresults->status),
+ \core\output\notification::NOTIFY_ERROR);
+ }
+ }
+
+ if (!empty($predictlogs) && debugging()) {
+ $output .= $OUTPUT->heading(get_string('extrainfo', 'tool_analytics'), 4);
+ foreach ($predictlogs as $log) {
+ $output .= $OUTPUT->notification($log, \core\output\notification::NOTIFY_WARNING);
+ }
+ }
+
+ if (!CLI_SCRIPT) {
+ $output .= $OUTPUT->single_button(new \moodle_url('/admin/tool/analytics/index.php'), get_string('continue'));
+ }
+
+ return $output;
+ }
+}
--- /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/>.
+
+/**
+ * Predict system models with new data available.
+ *
+ * @package tool_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_analytics\task;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Predict system models with new data available.
+ *
+ * @package tool_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class predict_models extends \core\task\scheduled_task {
+
+ /**
+ * get_name
+ *
+ * @return string
+ */
+ public function get_name() {
+ return get_string('predictmodels', 'tool_analytics');
+ }
+
+ /**
+ * Executes the prediction task.
+ *
+ * @return void
+ */
+ public function execute() {
+ global $OUTPUT, $PAGE;
+
+ $models = \core_analytics\manager::get_all_models(true, true);
+ if (!$models) {
+ mtrace(get_string('errornoenabledandtrainedmodels', 'tool_analytics'));
+ return;
+ }
+
+ foreach ($models as $model) {
+ $result = $model->predict();
+ if ($result) {
+ echo $OUTPUT->heading(get_string('modelresults', 'tool_analytics', $model->get_target()->get_name()));
+ $renderer = $PAGE->get_renderer('tool_analytics');
+ echo $renderer->render_get_predictions_results(false, array(), $result, $model->get_analyser()->get_logs());
+ }
+ }
+
+ }
+}
--- /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/>.
+
+/**
+ * Train system models with new data available.
+ *
+ * @package tool_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_analytics\task;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Train system models with new data available.
+ *
+ * @package tool_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class train_models extends \core\task\scheduled_task {
+
+ /**
+ * get_name
+ *
+ * @return string
+ */
+ public function get_name() {
+ return get_string('trainmodels', 'tool_analytics');
+ }
+
+ /**
+ * Executes the prediction task.
+ *
+ * @return void
+ */
+ public function execute() {
+ global $OUTPUT, $PAGE;
+
+ $models = \core_analytics\manager::get_all_models(true);
+ if (!$models) {
+ mtrace(get_string('errornoenabledmodels', 'tool_analytics'));
+ return;
+ }
+
+ foreach ($models as $model) {
+
+ if ($model->is_static()) {
+ // Skip models based on assumptions.
+ continue;
+ }
+
+ if (!$model->get_time_splitting()) {
+ // Can not train if there is no time splitting method selected.
+ continue;
+ }
+
+ $result = $model->train();
+ if ($result) {
+ echo $OUTPUT->heading(get_string('modelresults', 'tool_analytics', $model->get_target()->get_name()));
+
+ $renderer = $PAGE->get_renderer('tool_analytics');
+ echo $renderer->render_get_predictions_results($result, $model->get_analyser()->get_logs());
+ }
+ }
+ }
+}
--- /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/>.
+
+/**
+ * Enables the provided model.
+ *
+ * @package tool_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('CLI_SCRIPT', true);
+
+require_once(__DIR__ . '/../../../../config.php');
+require_once($CFG->libdir.'/clilib.php');
+
+$help = "Enables the provided model.
+
+Options:
+--modelid Model id
+--timesplitting Time splitting method full class name
+-h, --help Print out this help
+
+Example:
+\$ php admin/tool/analytics/cli/enable_model.php --modelid=1 --timesplitting=\"\\core\\analytics\\time_splitting\\quarters\"
+";
+
+// Now get cli options.
+list($options, $unrecognized) = cli_get_params(
+ array(
+ 'help' => false,
+ 'modelid' => false,
+ 'timesplitting' => false
+ ),
+ array(
+ 'h' => 'help',
+ )
+);
+
+if ($options['help']) {
+ echo $help;
+ exit(0);
+}
+
+if ($options['modelid'] === false || $options['timesplitting'] === false) {
+ echo $help;
+ exit(0);
+}
+
+// We need admin permissions.
+\core\session\manager::set_user(get_admin());
+
+$model = new \core_analytics\model($options['modelid']);
+
+// Evaluate its suitability to predict accurately.
+$model->enable($options['timesplitting']);
+
+cli_heading(get_string('success'));
+exit(0);
--- /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/>.
+
+/**
+ * Evaluates the provided model.
+ *
+ * @package tool_analytics
+ * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('CLI_SCRIPT', true);
+
+require_once(__DIR__ . '/../../../../config.php');
+require_once($CFG->libdir.'/clilib.php');
+
+$help = "Evaluates the provided model.
+
+Options:
+--modelid Model id
+--non-interactive Not interactive questions
+--timesplitting Restrict the evaluation to 1 single time splitting method (Optional)
+--filter Analyser dependant. e.g. A courseid would evaluate the model using a single course (Optional)
+--reuse-prev-analysed Reuse recently analysed courses instead of analysing the whole site. Set it to false while" .
+ " coding indicators. Defaults to true (Optional)" . "
+-h, --help Print out this help
+
+Example:
+\$ php admin/tool/analytics/cli/evaluate_model.php --modelid=1 --timesplitting='\\core\\analytics\\time_splitting\\quarters' --filter=123,321
+";
+
+// Now get cli options.
+list($options, $unrecognized) = cli_get_params(
+ array(
+ 'help' => false,
+ 'modelid' => false,
+ 'timesplitting' => false,
+ 'reuse-prev-analysed' => true,
+ 'non-interactive' => false,
+ 'filter' => false
+ ),
+ array(
+ 'h' => 'help',
+ )
+);
+
+if ($options['help']) {
+ echo $help;
+ exit(0);
+}
+
+if ($options['modelid'] === false) {
+ echo $help;
+ exit(0);
+}
+
+// Reformat them as an array.
+if ($options['filter'] !== false) {
+ $options['filter'] = explode(',', $options['filter']);
+}
+
+// We need admin permissions.
+\core\session\manager::set_user(get_admin());
+
+$model = new \core_analytics\model($options['modelid']);
+
+mtrace(get_string('analysingsitedata', 'tool_analytics'));
+
+if ($options['reuse-prev-analysed']) {
+ mtrace(get_string('evaluationinbatches', 'tool_analytics'));
+}
+
+$analyseroptions = array(
+ 'filter' => $options['filter'],
+ 'timesplitting' => $options['timesplitting'],
+ 'reuseprevanalysed' => $options['reuse-prev-analysed'],
+);
+// Evaluate its suitability to predict accurately.
+$results = $model->evaluate($analyseroptions);
+
+$renderer = $PAGE->get_renderer('tool_analytics');
+echo $renderer->render_evaluate_results($results, $model->get_analyser()->get_logs());
+
+// Check that we have, at leasa,t 1 valid dataset (not necessarily good) to use.
+foreach ($results as $result) {
+ if ($result->status !== \core_analytics\model::NO_DATASET &&
+ $result->status !== \core_analytics\model::GENERAL_ERROR) {
+ $validdatasets = true;
+ }
+}
+
+if (!empty($validdatasets) && !$model->is_enabled() && $options['non-interactive'] === false) {
+
+ // Select a dataset, train and enable the model.
+ $input = cli_input(get_string('clienablemodel', 'tool_analytics'));
+ while (!\core_analytics\manager::is_valid($input, '\core_analytics\local\time_splitting\base') && $input !== 'none') {
+ mtrace(get_string('errorunexistingtimesplitting', 'analytics'));
+ $input = cli_input(get_string('clienablemodel', 'tool_analytics'));
+ }
+
+ if ($input === 'none') {
+ exit(0);
+ }
+
+ // Refresh the instance to prevent unexpected issues.
+ $model = new \core_analytics\model($modelobj);
+
+ // Set the time splitting method file and enable it.
+ $model->enable($input);
+
+ mtrace(get_string('trainandpredictmodel', 'tool_analytics'));
+
+ // Train the model with the selected time splitting method and start predicting.
+ $model->train();
+ $model->predict();
+}
+
+exit(0);
--- /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/>.
+
+/**
+ * Guesses course start and end dates based on activity logs.
+ *
+ * @package tool_analytics
+ * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('CLI_SCRIPT', true);
+
+require_once(__DIR__ . '/../../../../config.php');
+require_once($CFG->libdir.'/clilib.php');
+
+require_once($CFG->dirroot . '/course/lib.php');
+require_once($CFG->dirroot . '/course/format/weeks/lib.php');
+
+$help = "Guesses course start and end dates based on activity logs.
+
+Options:
+--guessstart Guess the course start date (default to true)
+--guessend Guess the course end date (default to true)
+--guessall Guess all start and end dates, even if they are already set (default to false)
+--update Update the db or just notify the guess (default to false)
+--filter Analyser dependant. e.g. A courseid would evaluate the model using a single course (Optional)
+-h, --help Print out this help
+
+Example:
+\$ php admin/tool/analytics/cli/guess_course_start_and_end_dates.php --update=1 --filter=123,321
+";
+
+// Now get cli options.
+list($options, $unrecognized) = cli_get_params(
+ array(
+ 'help' => false,
+ 'guessstart' => true,
+ 'guessend' => true,
+ 'guessall' => false,
+ 'update' => false,
+ 'filter' => false
+ ),
+ array(
+ 'h' => 'help',
+ )
+);
+
+if ($options['help']) {
+ echo $help;
+ exit(0);
+}
+
+if ($options['guessstart'] === false && $options['guessend'] === false && $options['guessall'] === false) {
+ echo $help;
+ exit(0);
+}
+
+// Reformat them as an array.
+if ($options['filter'] !== false) {
+ $options['filter'] = explode(',', clean_param($options['filter'], PARAM_SEQUENCE));
+}
+
+// We need admin permissions.
+\core\session\manager::set_user(get_admin());
+
+$conditions = array('id != 1');
+if (!$options['guessall']) {
+ if ($options['guessstart']) {
+ $conditions[] = '(startdate is null or startdate = 0)';
+ }
+ if ($options['guessend']) {
+ $conditions[] = '(enddate is null or enddate = 0)';
+ }
+}
+
+$coursessql = '';
+$params = null;
+if ($options['filter']) {
+ list($coursessql, $params) = $DB->get_in_or_equal($options['filter'], SQL_PARAMS_NAMED);
+ $conditions[] = 'id ' . $coursessql;
+}
+
+$courses = $DB->get_recordset_select('course', implode(' AND ', $conditions), $params, 'sortorder ASC');
+foreach ($courses as $course) {
+ tool_analytics_calculate_course_dates($course, $options);
+}
+$courses->close();
+
+
+/**
+ * tool_analytics_calculate_course_dates
+ *
+ * @param stdClass $course
+ * @param array $options CLI options
+ * @return void
+ */
+function tool_analytics_calculate_course_dates($course, $options) {
+ global $DB, $OUTPUT;
+
+ $courseman = new \core_analytics\course($course);
+
+ $notification = $course->shortname . ' (id = ' . $course->id . '): ';
+
+ if ($options['guessstart'] || $options['guessall']) {
+
+ $originalstartdate = $course->startdate;
+
+ $guessedstartdate = $courseman->guess_start();
+ if ($guessedstartdate == $originalstartdate) {
+ if (!$guessedstartdate) {
+ $notification .= PHP_EOL . ' ' . get_string('cantguessstartdate', 'tool_analytics');
+ } else {
+ // No need to update.
+ $notification .= PHP_EOL . ' ' . get_string('samestartdate', 'tool_analytics') . ': ' . userdate($guessedstartdate);
+ }
+ } else if (!$guessedstartdate) {
+ $notification .= PHP_EOL . ' ' . get_string('cantguessstartdate', 'tool_analytics');
+ } else {
+ // Update it to something we guess.
+
+ // We set it to $course even if we don't update because may be needed to guess the end one.
+ $course->startdate = $guessedstartdate;
+ $notification .= PHP_EOL . ' ' . get_string('startdate') . ': ' . userdate($guessedstartdate);
+
+ // Two different course updates because week's end date may be recalculated after setting the start date.
+ if ($options['update']) {
+ update_course($course);
+
+ // Refresh course data as end date may have been updated.
+ $course = $DB->get_record('course', array('id' => $course->id));
+ $courseman = new \core_analytics\course($course);
+ }
+ }
+ }
+
+ if ($options['guessend'] || $options['guessall']) {
+
+ $originalenddate = $course->enddate;
+
+ $format = course_get_format($course);
+ $formatoptions = $format->get_format_options();
+
+ if ($course->format === 'weeks' && $formatoptions['automaticenddate']) {
+ // Special treatment for weeks with automatic end date.
+
+ if ($options['update']) {
+ format_weeks::update_end_date($course->id);
+ $course->enddate = $DB->get_field('course', 'enddate', array('id' => $course->id));
+ $notification .= PHP_EOL . ' ' . get_string('weeksenddateautomaticallyset', 'tool_analytics') . ': ' .
+ userdate($course->enddate);
+ } else {
+ // We can't provide more info without actually updating it in db.
+ $notification .= PHP_EOL . ' ' . get_string('weeksenddatedefault', 'tool_analytics');
+ }
+ } else {
+ $guessedenddate = $courseman->guess_end();
+
+ if ($guessedenddate == $originalenddate) {
+ if (!$guessedenddate) {
+ $notification .= PHP_EOL . ' ' . get_string('cantguessenddate', 'tool_analytics');
+ } else {
+ // No need to update.
+ $notification .= PHP_EOL . ' ' . get_string('sameenddate', 'tool_analytics') . ': ' . userdate($guessedenddate);
+ }
+ } else if (!$guessedenddate) {
+ $notification .= PHP_EOL . ' ' . get_string('cantguessenddate', 'tool_analytics');
+ } else {
+ // Update it to something we guess.
+
+ $course->enddate = $guessedenddate;
+
+ if ($course->enddate > $course->startdate) {
+ $notification .= PHP_EOL . ' ' . get_string('enddate') . ': ' . userdate($course->enddate);
+ } else {
+ $notification .= PHP_EOL . ' ' . get_string('errorendbeforestart', 'analytics', userdate($course->enddate));
+ }
+
+ if ($options['update']) {
+ if ($course->enddate > $course->startdate) {
+ update_course($course);
+ }
+ }
+ }
+ }
+
+ }
+
+ mtrace($notification);
+}
+
+mtrace(get_string('success'));
+
+exit(0);
--- /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/>.
+
+/**
+ * This file defines tasks performed by the tool.
+ *
+ * @package tool_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+// List of tasks.
+$tasks = array(
+ array(
+ 'classname' => 'tool_analytics\task\train_models',
+ 'blocking' => 0,
+ 'minute' => '0',
+ 'hour' => 'R',
+ 'day' => '*',
+ 'dayofweek' => '*',
+ 'month' => '*'
+ ),
+ array(
+ 'classname' => 'tool_analytics\task\predict_models',
+ 'blocking' => 0,
+ 'minute' => '0',
+ 'hour' => 'R',
+ 'day' => '*',
+ 'dayofweek' => '*',
+ 'month' => '*'
+ ),
+);
--- /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 models tool frontend.
+ *
+ * @package tool_analytics
+ * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(__DIR__ . '/../../../config.php');
+require_once($CFG->libdir . '/adminlib.php');
+
+admin_externalpage_setup('analyticmodels', '', null, '', array('pagelayout' => 'report'));
+
+$models = \core_analytics\manager::get_all_models();
+
+echo $OUTPUT->header();
+
+$templatable = new \tool_analytics\output\models_list($models);
+echo $PAGE->get_renderer('tool_analytics')->render($templatable);
+
+echo $OUTPUT->footer();
--- /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/>.
+
+/**
+ * Strings for tool_analytics.
+ *
+ * @package tool_analytics
+ * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['accuracy'] = 'Accuracy';
+$string['allpredictions'] = 'All predictions';
+$string['analysingsitedata'] = 'Analysing the site';
+$string['analyticmodels'] = 'Analytic models';
+$string['bettercli'] = 'Evaluating models and generating predictions may involve heavy processing. It is advised that you run these actions via the command line interface';
+$string['cantguessstartdate'] = 'Can\'t guess the start date';
+$string['cantguessenddate'] = 'Can\'t guess the end date';
+$string['clienablemodel'] = 'You can enable the model by selecting a time splitting method by its id. Note that you can also enable it later using the web interface (\'none\' to exit)';
+$string['editmodel'] = 'Edit "{$a}" model';
+$string['edittrainedwarning'] = 'This model has already been trained, note that changing its indicators or its time splitting method will delete its previous predictions and start generating the new ones';
+$string['enabled'] = 'Enabled';
+$string['errorcantenablenotimesplitting'] = 'You need to select a time splitting method before enabling the model';
+$string['errornoenabledandtrainedmodels'] = 'There are not enabled and trained models to predict';
+$string['errornoenabledmodels'] = 'There are not enabled models to train';
+$string['errornostaticedit'] = 'Models based on assumptions can not be edited';
+$string['errornostaticevaluated'] = 'Models based on assumptions can not be evaluated, they are always 100% correct according to how they were defined';
+$string['errornostaticlog'] = 'Models based on assumptions can not be evaluated, there is no preformance log';
+$string['evaluate'] = 'Evaluate';
+$string['evaluatemodel'] = 'Evaluate model';
+$string['evaluationinbatches'] = 'The site contents are calculated and stored in batches, during evaluation you can stop the process at any moment, the next time you run it it will continue from the point you stopped it.';
+$string['trainandpredictmodel'] = 'Training model and calculating predictions';
+$string['getpredictionsresultscli'] = 'Results using {$a->name} (id: {$a->id}) course duration splitting';
+$string['getpredictionsresults'] = 'Results using {$a->name} course duration splitting';
+$string['extrainfo'] = 'Info';
+$string['generalerror'] = 'Evaluation error. Status code {$a}';
+$string['getpredictions'] = 'Get predictions';
+$string['goodmodel'] = 'This is a good model and it can be used to predict, enable it to start getting predictions.';
+$string['indicators'] = 'Indicators';
+$string['info'] = 'Info';
+$string['insights'] = 'Insights';
+$string['loginfo'] = 'Log extra info';
+$string['modelresults'] = '{$a} results';
+$string['modelslist'] = 'Models list';
+$string['modeltimesplitting'] = 'Time splitting';
+$string['nodatatoevaluate'] = 'There is no data to evaluate the model';
+$string['nodatatopredict'] = 'No new elements to get predictions for';
+$string['nodatatotrain'] = 'There is no new data that can be used for training';
+$string['notdefined'] = 'Not yet defined';
+$string['pluginname'] = 'Analytic models';
+$string['predictionresults'] = 'Prediction results';
+$string['predictmodels'] = 'Predict models';
+$string['predictorresultsin'] = 'Predictor logged information in {$a} directory';
+$string['predictionprocessfinished'] = 'Prediction process finished';
+$string['samestartdate'] = 'Current start date is good';
+$string['sameenddate'] = 'Current end date is good';
+$string['target'] = 'Target';
+$string['trainingprocessfinished'] = 'Training process finished';
+$string['trainingresults'] = 'Training results';
+$string['trainmodels'] = 'Train models';
+$string['viewlog'] = 'Log';
+$string['weeksenddateautomaticallyset'] = 'End date automatically set based on start date and the number of sections';
+$string['weeksenddatedefault'] = 'End date would be automatically calculated from the course start date';
--- /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/>.
+
+/**
+ * Model-related actions.
+ *
+ * @package tool_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(__DIR__ . '/../../../config.php');
+
+$id = required_param('id', PARAM_INT);
+$action = required_param('action', PARAM_ALPHANUMEXT);
+
+$context = context_system::instance();
+
+require_login();
+
+$model = new \core_analytics\model($id);
+\core_analytics\manager::check_can_manage_models();
+
+$params = array('id' => $id, 'action' => $action);
+$url = new \moodle_url('/admin/tool/analytics/model.php', $params);
+
+switch ($action) {
+
+ case 'edit':
+ $title = get_string('editmodel', 'tool_analytics', $model->get_target()->get_name());
+ break;
+ case 'evaluate':
+ $title = get_string('evaluatemodel', 'tool_analytics');
+ break;
+ case 'getpredictions':
+ $title = get_string('getpredictions', 'tool_analytics');
+ break;
+ case 'log':
+ $title = get_string('viewlog', 'tool_analytics');
+ break;
+ case 'enable':
+ $title = get_string('enable');
+ break;
+ case 'disable':
+ $title = get_string('disable');
+ break;
+
+ default:
+ throw new moodle_exception('errorunknownaction', 'analytics');
+}
+
+$PAGE->set_context($context);
+$PAGE->set_url($url);
+$PAGE->set_pagelayout('report');
+$PAGE->set_title($title);
+$PAGE->set_heading($title);
+
+switch ($action) {
+
+ case 'enable':
+ $model->enable();
+ redirect(new \moodle_url('/admin/tool/analytics/index.php'));
+
+ case 'disable':
+ $model->update(0, false, false);
+ redirect(new \moodle_url('/admin/tool/analytics/index.php'));
+
+ case 'edit':
+
+ if ($model->is_static()) {
+ echo $OUTPUT->header();
+ throw new moodle_exception('errornostaticedit', 'tool_analytics');
+ }
+
+ $customdata = array(
+ 'id' => $model->get_id(),
+ 'model' => $model,
+ 'indicators' => $model->get_potential_indicators(),
+ 'timesplittings' => \core_analytics\manager::get_enabled_time_splitting_methods()
+ );
+ $mform = new \tool_analytics\output\form\edit_model(null, $customdata);
+
+ if ($mform->is_cancelled()) {
+ redirect(new \moodle_url('/admin/tool/analytics/index.php'));
+
+ } else if ($data = $mform->get_data()) {
+ confirm_sesskey();
+
+ // Converting option names to class names.
+ $indicators = array();
+ foreach ($data->indicators as $indicator) {
+ $indicatorclass = \tool_analytics\output\helper::option_to_class($indicator);
+ $indicators[] = \core_analytics\manager::get_indicator($indicatorclass);
+ }
+ $timesplitting = \tool_analytics\output\helper::option_to_class($data->timesplitting);
+ $model->update($data->enabled, $indicators, $timesplitting);
+ redirect(new \moodle_url('/admin/tool/analytics/index.php'));
+ }
+
+ echo $OUTPUT->header();
+
+ $modelobj = $model->get_model_obj();
+
+ $callable = array('\tool_analytics\output\helper', 'class_to_option');
+ $modelobj->indicators = array_map($callable, json_decode($modelobj->indicators));
+ $modelobj->timesplitting = \tool_analytics\output\helper::class_to_option($modelobj->timesplitting);
+ $mform->set_data($modelobj);
+ $mform->display();
+ break;
+
+ case 'evaluate':
+ echo $OUTPUT->header();
+
+ if ($model->is_static()) {
+ throw new moodle_exception('errornostaticevaluate', 'tool_analytics');
+ }
+
+ // Web interface is used by people who can not use CLI nor code stuff, always use
+ // cached stuff as they will change the model through the web interface as well
+ // which invalidates the previously analysed stuff.
+ $results = $model->evaluate(array('reuseprevanalysed' => true));
+ $renderer = $PAGE->get_renderer('tool_analytics');
+ echo $renderer->render_evaluate_results($results, $model->get_analyser()->get_logs());
+ break;
+
+ case 'getpredictions':
+ echo $OUTPUT->header();
+
+ $trainresults = $model->train();
+ $trainlogs = $model->get_analyser()->get_logs();
+
+ // Looks dumb to get a new instance but better be conservative.
+ $model = new \core_analytics\model($model->get_model_obj());
+ $predictresults = $model->predict();
+ $predictlogs = $model->get_analyser()->get_logs();
+
+ $renderer = $PAGE->get_renderer('tool_analytics');
+ echo $renderer->render_get_predictions_results($trainresults, $trainlogs, $predictresults, $predictlogs);
+ break;
+
+ case 'log':
+ echo $OUTPUT->header();
+
+ if ($model->is_static()) {
+ throw new moodle_exception('errornostaticlog', 'tool_analytics');
+ }
+
+ $renderer = $PAGE->get_renderer('tool_analytics');
+ $modellogstable = new \tool_analytics\output\model_logs('model-' . $model->get_id(), $model);
+ echo $renderer->render_table($modellogstable);
+ break;
+}
+
+echo $OUTPUT->footer();
--- /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 tool_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();
+
+$ADMIN->add('reports', new admin_externalpage('analyticmodels', get_string('analyticmodels', 'tool_analytics'),
+ "$CFG->wwwroot/$CFG->admin/tool/analytics/index.php", 'moodle/analytics:managemodels'));
--- /dev/null
+{{!
+ 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/>.
+}}
+{{!
+ @template tool_analytics/models_list
+
+ Template for models list.
+
+ Classes required for JS:
+ * none
+
+ Data attributes required for JS:
+ * none
+
+ Context variables required for this template:
+ * none
+
+ Example context (json):
+ {
+ "models": [
+ {
+ "target": "Prevent devs at risk",
+ "enabled": 1,
+ "indicators": [
+ "Indicator 1",
+ "Indicator 2",
+ "Indicator 3",
+ "Indicator 4"
+ ],
+ "timesplitting": "Quarters",
+ "noinsights": "No insights available yet"
+ }
+ ],
+ "warnings": {
+ "message": "Hey, this is a warning"
+ }
+ }
+}}
+
+{{#warnings}}
+ {{> core/notification_warning}}
+{{/warnings}}
+<div class="box">
+ <table class="generaltable fullwidth">
+ <caption>{{#str}}modelslist, tool_analytics{{/str}}</caption>
+ <thead>
+ <tr>
+ <th scope="col">{{#str}}target, tool_analytics{{/str}}</th>
+ <th scope="col">{{#str}}enabled, tool_analytics{{/str}}</th>
+ <th scope="col">{{#str}}indicators, tool_analytics{{/str}}</th>
+ <th scope="col">{{#str}}modeltimesplitting, tool_analytics{{/str}}</th>
+ <th scope="col">{{#str}}insights, tool_analytics{{/str}}</th>
+ <th scope="col">{{#str}}actions{{/str}}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {{#models}}
+ <tr>
+ <td>{{target}}</td>
+ <td>
+ {{#enabled}}
+ {{#pix}}i/checked, core, {{#str}}yes{{/str}}{{/pix}}
+ {{/enabled}}
+ {{^enabled}}
+ {{#str}}no{{/str}}
+ {{/enabled}}
+ </td>
+ <td>
+ <ul>
+ {{#indicators}}
+ <li>{{.}}</li>
+ {{/indicators}}
+ </ul>
+ </td>
+ <td>
+ {{#timesplitting}}{{timesplitting}}{{/timesplitting}}{{^timesplitting}}{{#str}}notdefined, tool_analytics{{/str}}{{/timesplitting}}
+ </td>
+ <td>
+ {{! models_list renderer is responsible of sending one or the other}}
+ {{#insights}}
+ {{> core/single_select }}
+ {{/insights}}
+ {{#noinsights}}
+ {{.}}
+ {{/noinsights}}
+ </td>
+ <td>
+ {{#actions}}
+ {{> core/action_menu}}
+ {{/actions}}
+ </td>
+ </tr>
+ {{/models}}
+ </tbody>
+ </table>
+</div>
--- /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/>.
+
+/**
+ * Version details.
+ *
+ * @package tool_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com/}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$plugin->version = 2017051500; // The current plugin version (Date: YYYYMMDDXX).
+$plugin->requires = 2017050500; // Requires this Moodle version.
+$plugin->component = 'tool_analytics'; // Full name of the plugin (used for diagnostics).
--- /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/>.
+
+/**
+ * Extension to show an error message if the selected predictor is not available.
+ *
+ * @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');
+
+/**
+ * Extension to show an error message if the selected predictor is not available.
+ *
+ * @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 admin_setting_predictor extends \admin_setting_configselect {
+
+ /**
+ * Save a setting
+ *
+ * @param string $data
+ * @return string empty of error string
+ */
+ public function write_setting($data) {
+ if (!$this->load_choices() or empty($this->choices)) {
+ return '';
+ }
+ if (!array_key_exists($data, $this->choices)) {
+ return '';
+ }
+
+ // 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) {
+ return get_string('errorprocessornotready', 'analytics', $isready);
+ }
+
+ return ($this->config_write($this->name, $data) ? '' : get_string('errorsetting', 'admin'));
+ }
+}
--- /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 element analysers can analyse.
+ *
+ * @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();
+
+/**
+ * Any element analysers can analyse.
+ *
+ * @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 {
+
+ /**
+ * Max timestamp.
+ */
+ const MAX_TIME = 9999999999;
+
+ /**
+ * The analysable unique identifier in the site.
+ *
+ * @return int.
+ */
+ public function get_id();
+
+ /**
+ * The analysable context.
+ *
+ * @return \context
+ */
+ public function get_context();
+
+ /**
+ * The start of the analysable if there is one.
+ *
+ * @return int|false
+ */
+ public function get_start();
+
+ /**
+ * The end of the analysable if there is one.
+ *
+ * @return int|false
+ */
+ 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 {
+
+ /**
+ * Neutral calculation outcome.
+ */
+ const OUTCOME_NEUTRAL = 0;
+
+ /**
+ * Very positive calculation outcome.
+ */
+ const OUTCOME_VERY_POSITIVE = 1;
+
+ /**
+ * Positive calculation outcome.
+ */
+ const OUTCOME_OK = 2;
+
+ /**
+ * Negative calculation outcome.
+ */
+ const OUTCOME_NEGATIVE = 3;
+
+ /**
+ * Very negative calculation outcome.
+ */
+ const OUTCOME_VERY_NEGATIVE = 4;
+
+ /**
+ * @var array[]
+ */
+ protected $sampledata = array();
+
+ /**
+ * 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();
+ }
+
+ /**
+ * The class id is the calculable class full qualified class name.
+ *
+ * @return string
+ */
+ public function get_id() {
+ return '\\' . get_class($this);
+ }
+
+ /**
+ * add_sample_data
+ *
+ * @param array $data
+ * @return void
+ */
+ public function add_sample_data($data) {
+ $this->sampledata = $this->array_merge_recursive_keep_keys($this->sampledata, $data);
+ }
+
+ /**
+ * clear_sample_data
+ *
+ * @return void
+ */
+ public function clear_sample_data() {
+ $this->sampledata = array();
+ }
+
+ /**
+ * Returns the visible value of the calculated value.
+ *
+ * @param float $value
+ * @param string|false $subtype
+ * @return string
+ */
+ public function get_display_value($value, $subtype = false) {
+ return $value;
+ }
+
+ /**
+ * Returns how good the calculated value is.
+ *
+ * Use one of \core_analytics\calculable::OUTCOME_* values.
+ *
+ * @param float $value
+ * @param string|false $subtype
+ * @return int
+ */
+ abstract public function get_calculation_outcome($value, $subtype = false);
+
+ /**
+ * Retrieve the specified element associated to $sampleid.
+ *
+ * @param string $elementname
+ * @param int $sampleid
+ * @return \stdClass|false An \stdClass object or false if it can not be found.
+ */
+ protected function retrieve($elementname, $sampleid) {
+ if (empty($this->sampledata[$sampleid]) || empty($this->sampledata[$sampleid][$elementname])) {
+ // 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][$elementname];
+ }
+
+ /**
+ * Returns the number of weeks a time range contains.
+ *
+ * Useful for calculations that depend on the time range duration. Note that it returns
+ * a float, rounding the float may lead to inaccurate results.
+ *
+ * @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.');
+ }
+
+ $starttimedt = new \DateTime();
+ $starttimedt->setTimestamp($starttime);
+ $starttimedt->setTimezone(new \DateTimeZone('UTC'));
+ $endtimedt = new \DateTime();
+ $endtimedt->setTimestamp($endtime);
+ $endtimedt->setTimezone(new \DateTimeZone('UTC'));
+
+ $diff = $endtimedt->getTimestamp() - $starttimedt->getTimestamp();
+ 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'
+ *
+ * @throws \coding_exception
+ * @param int|float $value
+ * @param array $ranges e.g. [ ['lt', 20], ['ge', 20] ]
+ * @return float
+ */
+ 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 new \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.');
+ }
+
+ /**
+ * Merges arrays recursively keeping the same keys the original arrays have.
+ *
+ * @link http://php.net/manual/es/function.array-merge-recursive.php#114818
+ * @return array
+ */
+ private function array_merge_recursive_keep_keys() {
+ $arrays = func_get_args();
+ $base = array_shift($arrays);
+
+ foreach ($arrays as $array) {
+ reset($base);
+ while (list($key, $value) = each($array)) {
+ if (is_array($value) && !empty($base[$key]) && is_array($base[$key])) {
+ $base[$key] = $this->array_merge_recursive_keep_keys($base[$key], $value);
+ } else {
+ if (isset($base[$key]) && is_int($key)) {
+ $key++;
+ }
+ $base[$key] = $value;
+ }
+ }
+ }
+
+ return $base;
+ }
+}
--- /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/>.
+
+/**
+ * Moodle course analysable
+ *
+ * @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');
+require_once($CFG->dirroot . '/lib/enrollib.php');
+
+/**
+ * Moodle course analysable
+ *
+ * @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 {
+
+ /**
+ * @var \core_analytics\course[] $instances
+ */
+ protected static $instances = array();
+
+ /**
+ * Course object
+ *
+ * @var \stdClass
+ */
+ protected $course = null;
+
+ /**
+ * The course context.
+ *
+ * @var \context_course
+ */
+ protected $coursecontext = null;
+
+ /**
+ * The course activities organized by activity type.
+ *
+ * @var array
+ */
+ protected $courseactivities = array();
+
+ /**
+ * Course start time.
+ *
+ * @var int
+ */
+ protected $starttime = null;
+
+
+ /**
+ * Has the course already started?
+ *
+ * @var bool
+ */
+ protected $started = null;
+
+ /**
+ * Course end time.
+ *
+ * @var int
+ */
+ protected $endtime = null;
+
+ /**
+ * Is the course finished?
+ *
+ * @var bool
+ */
+ protected $finished = null;
+
+ /**
+ * Course students ids.
+ *
+ * @var int[]
+ */
+ protected $studentids = [];
+
+
+ /**
+ * Course teachers ids
+ *
+ * @var int[]
+ */
+ protected $teacherids = [];
+
+ /**
+ * Cached copy of the total number of logs in the course.
+ *
+ * @var int
+ */
+ 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.
+ *
+ * Loads course students and teachers.
+ *
+ * @param int|stdClass $course Course id
+ * @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);
+
+ $this->now = time();
+
+ // Get the course users, including users assigned to student and teacher roles at an higher context.
+ $studentroles = array_keys(get_archetype_roles('student'));
+ $this->studentids = $this->get_user_ids($studentroles);
+
+ $teacherroles = array_keys(get_archetype_roles('editingteacher') + get_archetype_roles('teacher'));
+ $this->teacherids = $this->get_user_ids($teacherroles);
+ }
+
+ /**
+ * Returns an analytics course instance.
+ *
+ * @param int|stdClass $course Course id
+ * @return \core_analytics\course
+ */
+ 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];
+ }
+
+ /**
+ * Clears all statically cached instances.
+ *
+ * @return void
+ */
+ public static function reset_caches() {
+ self::$instances = array();
+ }
+
+ /**
+ * get_id
+ *
+ * @return int
+ */
+ public function get_id() {
+ return $this->course->id;
+ }
+
+ /**
+ * get_context
+ *
+ * @return \context
+ */
+ 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() {
+
+ 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;
+ }
+
+ /**
+ * Guesses the start of the course based on students' activity and enrolment start dates.
+ *
+ * @return int
+ */
+ public function guess_start() {
+ global $DB;
+
+ if (!$this->get_total_logs()) {
+ // Can't guess.
+ return 0;
+ }
+
+ if (!$logstore = \core_analytics\manager::get_analytics_logstore()) {
+ return 0;
+ }
+
+ // We first try to find current course student logs.
+ $firstlogs = array();
+ foreach ($this->studentids as $studentid) {
+ // Grrr, we are limited by logging API, we could do this easily with a
+ // select min(timecreated) from xx where courseid = yy group by userid.
+
+ // Filters based on the premise that more than 90% of people will be using
+ // standard logstore, which contains a userid, contextlevel, contextinstanceid index.
+ $select = "userid = :userid AND contextlevel = :contextlevel AND contextinstanceid = :contextinstanceid";
+ $params = array('userid' => $studentid, 'contextlevel' => CONTEXT_COURSE, 'contextinstanceid' => $this->get_id());
+ $events = $logstore->get_events_select($select, $params, 'timecreated ASC', 0, 1);
+ if ($events) {
+ $event = reset($events);
+ $firstlogs[] = $event->timecreated;
+ }
+ }
+ if (empty($firstlogs)) {
+ // Can't guess if no student accesses.
+ return 0;
+ }
+
+ sort($firstlogs);
+ $firstlogsmedian = $this->median($firstlogs);
+
+ $studentenrolments = enrol_get_course_users($this->get_id(), $this->studentids);
+ if (empty($studentenrolments)) {
+ return 0;
+ }
+
+ $enrolstart = array();
+ foreach ($studentenrolments as $studentenrolment) {
+ $enrolstart[] = ($studentenrolment->uetimestart) ? $studentenrolment->uetimestart : $studentenrolment->uetimecreated;
+ }
+ 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);
+ }
+
+ /**
+ * Returns a course plain object.
+ *
+ * @return \stdClass
+ */
+ 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();
+ if (!$logstore = \core_analytics\manager::get_analytics_logstore()) {
+ $this->ntotallogs = 0;
+ } else {
+ $this->ntotallogs = $logstore->get_events_select_count($filterselect, $filterparams);
+ }
+ }
+
+ return $this->ntotallogs;
+ }
+
+ /**
+ * Returns all the activities of the provided type the course has.
+ *
+ * @param string $activitytype
+ * @return array
+ */
+ 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];
+ }
+
+ /**
+ * Returns the course students grades.
+ *
+ * @param array $courseactivities
+ * @return array
+ */
+ 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;
+ }
+
+ /**
+ * Guesses all activities that were available during a period of time.
+ *
+ * @param string $activitytype
+ * @param int $starttime
+ * @param int $endtime
+ * @param \stdClass $student
+ * @return array
+ */
+ public function get_activities($activitytype, $starttime, $endtime, $student = false) {
+
+ // Var $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;
+ }
+
+ /**
+ * Was the activity supposed to be completed during the provided time range?.
+ *
+ * @param \cm_info $activity
+ * @param int $starttime
+ * @param int $endtime
+ * @return bool
+ */
+ 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;
+ }
+
+ // 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;
+ }
+ }
+
+ 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;
+ }
+
+ /**
+ * Check if the activity/section should have been completed during the provided period according to its availability rules.
+ *
+ * @param \core_availability\info $info
+ * @param int $starttime
+ * @param int $endtime
+ * @return bool|null
+ */
+ 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.
+ $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 the logstore by this course students.
+ *
+ * @param string $prefix
+ * @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/>.
+
+/**
+ * Datasets manager.
+ *
+ * @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();
+
+/**
+ * Datasets manager.
+ *
+ * @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 {
+
+ /**
+ * File area for labelled datasets.
+ */
+ const LABELLED_FILEAREA = 'labelled';
+
+ /**
+ * File area for unlabelled datasets.
+ */
+ const UNLABELLED_FILEAREA = 'unlabelled';
+
+ /**
+ * Evaluation file file name.
+ */
+ 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;
+
+ /**
+ * Constructor method.
+ *
+ * @param int $modelid
+ * @param int $analysableid
+ * @param string $timesplittingid
+ * @param bool $evaluation
+ * @param bool $includetarget
+ * @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 bool Could we get the lock or not.
+ */
+ public function init_process() {
+ $lockkey = 'modelid:' . $this->modelid . '-analysableid:' . $this->analysableid .
+ '-timesplitting:' . self::clean_time_splitting_id($this->timesplittingid) .
+ '-includetarget:' . (int)$this->includetarget;
+
+ // Large timeout as processes may be quite long.
+ $lockfactory = \core\lock\lock_config::get_lock_factory('core_analytics');
+
+ // If it is not ready in 10 secs skip this model + analysable + timesplittingmethod combination
+ // it will attempt it again during next cron run.
+ if (!$this->lock = $lockfactory->get_lock($lockkey, 10)) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * 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::clean_time_splitting_id($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+');
+ if (!$fh) {
+ $this->close_process();
+ throw new \moodle_exception('errorcannotwritedataset', 'analytics', '', $tmpfilepath);
+ }
+ 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();
+ }
+
+ /**
+ * 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::clean_time_splitting_id($timesplittingid) . '/', self::EVALUATION_FILENAME);
+ }
+
+ /**
+ * Deletes previous evaluation files of this model.
+ *
+ * @param int $modelid
+ * @param string $timesplittingid
+ * @return bool
+ */
+ public static function delete_previous_evaluation_file($modelid, $timesplittingid) {
+ if ($file = self::get_previous_evaluation_file($modelid, $timesplittingid)) {
+ $file->delete();
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns this (model + analysable + time splitting) file.
+ *
+ * @param int $modelid
+ * @param int $analysableid
+ * @param string $timesplittingid
+ * @return \stored_file
+ */
+ 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::clean_time_splitting_id($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 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');
+ if (!$wh) {
+ throw new \moodle_exception('errorcannotwritedataset', 'analytics', '', $tmpfilepath);
+ }
+
+ 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::clean_time_splitting_id($timesplittingid) . '/',
+ 'filename' => self::get_filename($evaluation)
+ ];
+
+ $fs = get_file_storage();
+
+ return $fs->create_file_from_pathname($filerecord, $tmpfilepath);
+ }
+
+ /**
+ * Returns the dataset file data structured by sampleids using the indicators and target column names.
+ *
+ * @param \stored_file $dataset
+ * @return array
+ */
+ 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;
+ }
+
+ /**
+ * Delete all files of a model.
+ *
+ * @param int $modelid
+ * @return bool
+ */
+ public static function clear_model_files($modelid) {
+ $fs = get_file_storage();
+ return $fs->delete_area_files(\context_system::instance()->id, 'analytics', false, $modelid);
+ }
+
+ /**
+ * Remove all possibly problematic chars from the time splitting method id (id = its full class name).
+ *
+ * @param string $timesplittingid
+ * @return string
+ */
+ protected static function clean_time_splitting_id($timesplittingid) {
+ $timesplittingid = str_replace('\\', '-', $timesplittingid);
+ return clean_param($timesplittingid, PARAM_ALPHANUMEXT);
+ }
+
+ /**
+ * Returns the file name to be used.
+ *
+ * @param strinbool $evaluation
+ * @return string
+ */
+ 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 = microtime(false) . '.csv';
+ }
+
+ return $filename;
+ }
+
+ /**
+ * Returns the file area to be used.
+ *
+ * @param bool $includetarget
+ * @return string
+ */
+ 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/>.
+
+/**
+ * Analysers base 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\local\analyser;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Analysers base 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 base {
+
+ /**
+ * @var int
+ */
+ protected $modelid;
+
+ /**
+ * The model target.
+ *
+ * @var \core_analytics\local\target\base
+ */
+ protected $target;
+
+ /**
+ * The model indicators.
+ *
+ * @var \core_analytics\local\indicator\base[]
+ */
+ protected $indicators;
+
+ /**
+ * Time splitting methods to use.
+ *
+ * Multiple time splitting methods during evaluation and 1 single
+ * time splitting method once the model is enabled.
+ *
+ * @var \core_analytics\local\time_splitting\base[]
+ */
+ protected $timesplittings;
+
+ /**
+ * Execution options.
+ *
+ * @var array
+ */
+ protected $options;
+
+ /**
+ * Simple log array.
+ *
+ * @var string[]
+ */
+ protected $log;
+
+ /**
+ * Constructor method.
+ *
+ * @param int $modelid
+ * @param \core_analytics\local\target\base $target
+ * @param \core_analytics\local\indicator\base[] $indicators
+ * @param \core_analytics\local\time_splitting\base[] $timesplittings
+ * @param array $options
+ * @return void
+ */
+ 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 this analysable list of samples.
+ *
+ * @param \core_analytics\analysable $analysable
+ * @return array array[0] = int[] (sampleids) and array[1] = array (samplesdata)
+ */
+ abstract protected function get_all_samples(\core_analytics\analysable $analysable);
+
+ /**
+ * This function returns the samples data from a list of sample ids.
+ *
+ * @param int[] $sampleids
+ * @return array array[0] = int[] (sampleids) and array[1] = array (samplesdata)
+ */
+ abstract public function get_samples($sampleids);
+
+ /**
+ * Returns the analysable of a sample.
+ *
+ * @param int $sampleid
+ * @return \core_analytics\analysable
+ */
+ abstract public function get_sample_analysable($sampleid);
+
+ /**
+ * Returns the sample's origin in moodle database.
+ *
+ * @return string
+ */
+ abstract protected function get_samples_origin();
+
+ /**
+ * Returns the context of a sample.
+ *
+ * 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);
+
+ /**
+ * Describes a sample with a description summary and a \renderable (an image for example)
+ *
+ * @param int $sampleid
+ * @param int $contextid
+ * @param array $sampledata
+ * @return array array(string, \renderable)
+ */
+ abstract public function sample_description($sampleid, $contextid, $sampledata);
+
+ /**
+ * 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.
+ *
+ * @param bool $includetarget
+ * @return \stored_file[]
+ */
+ abstract public function get_analysable_data($includetarget);
+
+ /**
+ * Samples data this analyser provides.
+ *
+ * @return string[]
+ */
+ protected function provided_sample_data() {
+ return array($this->get_samples_origin());
+ }
+
+ /**
+ * Returns labelled data (training and evaluation).
+ *
+ * @return array
+ */
+ public function get_labelled_data() {
+ return $this->get_analysable_data(true);
+ }
+
+ /**
+ * Returns unlabelled data (prediction).
+ *
+ * @return array
+ */
+ 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));
+ }
+ }
+ }
+
+ /**
+ * Checks that this analyser satisfies the provided 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 = call_user_func(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->add_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());
+ // 1 week is a partly random time interval, no need to worry about DST.
+ $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->add_log(get_string('analysablenotused', 'analytics', $a));
+ }
+
+ return $files;
+ }
+
+ /**
+ * Adds a register to the analysis log.
+ *
+ * @param string $string
+ * @return void
+ */
+ public function add_log($string) {
+ $this->log[] = $string;
+ }
+
+ /**
+ * Returns the analysis logs.
+ *
+ * @return string[]
+ */
+ public function get_logs() {
+ return $this->log;
+ }
+
+ /**
+ * Processes the analysable samples using the provided time splitting method.
+ *
+ * @param \core_analytics\local\time_splitting\base $timesplitting
+ * @param \core_analytics\analysable $analysable
+ * @param \core_analytics\local\target\base|false $target
+ * @return \stdClass Results object.
+ */
+ protected function process_time_splitting($timesplitting, $analysable, $target = false) {
+
+ $result = new \stdClass();
+
+ if (!$timesplitting->is_valid_analysable($analysable)) {
+ $result->status = \core_analytics\model::ANALYSABLE_REJECTED_TIME_SPLITTING_METHOD;
+ $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::ANALYSABLE_REJECTED_TIME_SPLITTING_METHOD;
+ $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::ANALYSABLE_REJECTED_TIME_SPLITTING_METHOD;
+ $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::ANALYSABLE_REJECTED_TIME_SPLITTING_METHOD;
+ $result->message = get_string('nonewdata', 'analytics');
+ return $result;
+ }
+
+ // 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::ANALYSABLE_REJECTED_TIME_SPLITTING_METHOD;
+ $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).
+ if (!$dataset->init_process()) {
+ // If this model + analysable + timesplitting combination is being analysed we skip this process.
+ $result->status = \core_analytics\model::NO_DATASET;
+ $result->message = get_string('analysisinprogress', 'analytics');
+ return $result;
+ }
+
+ // Remove samples the target consider invalid. Note that we use $this->target, $target will be false
+ // during prediction, but we still need to discard samples the target considers invalid.
+ $this->target->add_sample_data($samplesdata);
+ $this->target->filter_out_invalid_samples($sampleids, $analysable, $target);
+
+ if (!$sampleids) {
+ $result->status = \core_analytics\model::NO_DATASET;
+ $result->message = get_string('novalidsamples', 'analytics');
+ $dataset->close_process();
+ return $result;
+ }
+
+ 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]->add_sample_data($samplesdata);
+ }
+ // Provide samples to the target instance (different than $this->target) $target is the new instance we get
+ // for each analysis in progress.
+ if ($target) {
+ $target->add_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::ANALYSABLE_REJECTED_TIME_SPLITTING_METHOD;
+ $result->message = get_string('novaliddata', 'analytics');
+ $dataset->close_process();
+ 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;
+ }
+
+ /**
+ * Returns the ranges of a time splitting that can be used to predict.
+ *
+ * @param \core_analytics\local\time_splitting\base $timesplitting
+ * @return array
+ */
+ 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;
+ }
+
+ /**
+ * Filters out samples that have already been used for training.
+ *
+ * @param int[] $sampleids
+ * @param \core_analytics\local\time_splitting\base $timesplitting
+ * @return int[]
+ */
+ 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;
+ }
+
+ /**
+ * Filters out samples that have already been used for prediction.
+ *
+ * @param array $ranges
+ * @param \core_analytics\local\time_splitting\base $timesplitting
+ * @return int[]
+ */
+ 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;
+
+ }
+
+ /**
+ * Saves samples that have just been used for training.
+ *
+ * @param int[] $sampleids
+ * @param \core_analytics\local\time_splitting\base $timesplitting
+ * @param \stored_file $file
+ * @return bool
+ */
+ 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();
+
+ $trainingsamples->sampleids = json_encode($sampleids);
+ $trainingsamples->timecreated = time();
+
+ return $DB->insert_record('analytics_train_samples', $trainingsamples);
+ }
+
+ /**
+ * Saves samples that have just been used for prediction.
+ *
+ * @param array $ranges
+ * @param \core_analytics\local\time_splitting\base $timesplitting
+ * @return void
+ */
+ 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/>.
+
+/**
+ * Abstract analyser in course basis.
+ *
+ * @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();
+
+/**
+ * Abstract analyser in course basis.
+ *
+ * @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 {
+
+ /**
+ * Return the list of courses to analyse.
+ *
+ * @return \core_analytics\course[]
+ */
+ public function get_courses() {
+
+ // Default to all system courses.
+ if (!empty($this->options['filter'])) {
+ $courses = $this->options['filter'];
+ } else {
+ // Iterate through all potentially valid courses.
+ $courses = get_courses();
+ }
+ unset($courses[SITEID]);
+
+ $analysables = array();
+ foreach ($courses as $course) {
+ // Skip the frontpage course.
+ $analysable = \core_analytics\course::instance($course);
+ $analysables[$analysable->get_id()] = $analysable;
+ }
+
+ if (empty($analysables)) {
+ $this->log[] = get_string('nocourses', 'analytics');
+ }
+
+ return $analysables;
+ }
+
+ /**
+ * Returns the analysed data
+ *
+ * @param bool $includetarget
+ * @return \stored_file[]
+ */
+ public function get_analysable_data($includetarget) {
+
+ $filesbytimesplitting = array();
+
+ // This class and all children will iterate through a list of courses (\core_analytics\course).
+ $analysables = $this->get_courses('all', 'c.sortorder ASC');
+ 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;
+ }
+
+ /**
+ * Merges analysable dataset files into 1.
+ *
+ * @param array $filesbytimesplitting
+ * @param bool $includetarget
+ * @return \stored_file[]
+ */
+ 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/>.
+
+/**
+ * Site-level contents abstract analysable.
+ *
+ * @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();
+
+/**
+ * Site-level contents abstract analysable.
+ *
+ * @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 {
+
+ /**
+ * Returns the analysable data.
+ *
+ * @param bool $includetarget
+ * @return \stored_file[] One file for each time splitting method.
+ */
+ public function get_analysable_data($includetarget) {
+
+ // Here there is a single analysable and it is the system.
+ $analysable = new \core_analytics\site();
+
+ $files = $this->process_analysable($analysable, $includetarget);
+
+ // Copy to range files as there is just one analysable.
+ 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.
+ $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/>.
+
+/**
+ * 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 {
+
+ /**
+ * Min value an indicator can return.
+ */
+ const MIN_VALUE = -1;
+
+ /**
+ * Max value an indicator can return.
+ */
+ const MAX_VALUE = 1;
+
+ /**
+ * Converts the calculated indicators to dataset 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);
+
+ /**
+ * Should this value be displayed?
+ *
+ * Indicators providing multiple features can be used this method to discard some of them.
+ *
+ * @param float $value
+ * @param string $subtype
+ * @return bool
+ */
+ public function should_be_displayed($value, $subtype) {
+ // We should everything by default.
+ return true;
+ }
+
+ /**
+ * Allows indicators to specify data they need.
+ *
+ * e.g. A model using courses as samples will not provide users data, but an indicator like
+ * "user is hungry" needs user data.
+ *
+ * @return null|string[] Name of the required elements (use the database tablename)
+ */
+ public static function required_sample_data() {
+ return null;
+ }
+
+ /**
+ * Returns an instance of the indicator.
+ *
+ * Useful to reset cached data.
+ *
+ * @return \core_analytics\local\indicator\base
+ */
+ public static function instance() {
+ return new static();
+ }
+
+ /**
+ * Returns the maximum value an indicator calculation can return.
+ *
+ * @return float
+ */
+ public static function get_max_value() {
+ return self::MAX_VALUE;
+ }
+
+ /**
+ * Returns the minimum value an indicator calculation can return.
+ *
+ * @return float
+ */
+ public static function get_min_value() {
+ return self::MIN_VALUE;
+ }
+
+ /**
+ * Calculates the indicator.
+ *
+ * Returns an array of values which size matches $sampleids size.
+ *
+ * @param int[] $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;
+ }
+}
--- /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 {
+
+ /**
+ * get_classes
+ *
+ * @return array
+ */
+ public static final function get_classes() {
+ // It does not really matter, all \core_analytics\local\indicator\discrete get_classes calls have been overwriten as we
+ // only need 1 column here.
+ return array(0);
+ }
+
+ /**
+ * get_display_value
+ *
+ * @param float $value
+ * @param string $subtype
+ * @return string
+ */
+ 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');
+ }
+ }
+
+ /**
+ * get_calculation_outcome
+ *
+ * @param float $value
+ * @param string $subtype
+ * @return int
+ */
+ public function get_calculation_outcome($value, $subtype = false) {
+
+ // No subtypes for binary values by default.
+ if ($value == -1) {
+ return self::OUTCOME_NEGATIVE;
+ } else if ($value == 1) {
+ return self::OUTCOME_OK;
+ } else {
+ throw new \moodle_exception('errorpredictionformat', 'analytics');
+ }
+ }
+
+ /**
+ * get_feature_headers
+ *
+ * @return array
+ */
+ public static function get_feature_headers() {
+ // Just 1 single feature obtained from the calculated value.
+ return array('\\' . get_called_class());
+ }
+
+ /**
+ * to_features
+ *
+ * @param array $calculatedvalues
+ * @return array
+ */
+ 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/>.
+
+/**
+ * Community of inquiry 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 {
+
+ /**
+ * @var \core_analytics\course
+ */
+ protected $course = null;
+
+ /**
+ * @var array Array of logs by [contextid][userid]
+ */
+ protected $activitylogs = null;
+
+ /**
+ * @var array Array of grades by [contextid][userid]
+ */
+ protected $grades = null;
+
+ /**
+ * Constant cognitive indicator type.
+ */
+ const INDICATOR_COGNITIVE = "cognitve";
+
+ /**
+ * Constant social indicator type.
+ */
+ const INDICATOR_SOCIAL = "social";
+
+ /**
+ * Returns the activity type. No point in changing this class in children classes.
+ *
+ * @var string The activity name (e.g. assign or quiz)
+ */
+ protected final function get_activity_type() {
+ $class = get_class($this);
+ $package = stristr($class, "\\", true);
+ $type = str_replace("mod_", "", $package);
+ if ($type === $package) {
+ throw new \coding_exception("$class does not belong to any module specific namespace");
+ }
+ return $type;
+ }
+
+ /**
+ * Returns the potential level of cognitive depth.
+ *
+ * @param \cm_info $cm
+ * @return int
+ */
+ 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');
+ }
+
+ /**
+ * Returns the potential level of social breadth.
+ *
+ * @param \cm_info $cm
+ * @return int
+ */
+ protected function get_social_breadth_level(\cm_info $cm) {
+ throw new \coding_exception('Overwrite get_social_breadth_level method to set your activity potential social ' .
+ 'breadth level');
+ }
+
+ /**
+ * required_sample_data
+ *
+ * @return string[]
+ */
+ public static function required_sample_data() {
+ // Only course because the indicator is valid even without students.
+ return array('course');
+ }
+
+ /**
+ * Do activity logs contain any log of user in this context?
+ *
+ * If user is empty we look for any log in this context.
+ *
+ * @param int $contextid
+ * @param \stdClass|false $user
+ * @return bool
+ */
+ 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;
+ }
+
+ /**
+ * Do activity logs contain any write log of user in this context?
+ *
+ * If user is empty we look for any write log in this context.
+ *
+ * @param int $contextid
+ * @param \stdClass|false $user
+ * @return bool
+ */
+ 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 $events) {
+ foreach ($events as $log) {
+ if ($log->crud === 'c' || $log->crud === 'u') {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Is there any feedback activity log for this user in this context?
+ *
+ * This method returns true if $user is empty and there is any feedback activity logs.
+ *
+ * @param string $action
+ * @param \cm_info $cm
+ * @param int $contextid
+ * @param \stdClass|false $user
+ * @return bool
+ */
+ protected function any_feedback($action, \cm_info $cm, $contextid, $user) {
+
+ if (!in_array($action, ['submitted', 'replied', 'viewed'])) {
+ throw new \coding_exception('Provided action "' . $action . '" is not valid.');
+ }
+
+ 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 => $events) {
+ $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);
+ }
+
+ /**
+ * $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_replied(\cm_info $cm, $contextid, $userid, $after = null) {
+ return $this->feedback_post_action($cm, $contextid, $userid, $this->feedback_replied_events(), $after);
+ }
+
+ /**
+ * $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_submitted(\cm_info $cm, $contextid, $userid, $after = null) {
+ return $this->feedback_post_action($cm, $contextid, $userid, $this->feedback_submitted_events(), $after);
+ }
+
+ /**
+ * Returns the list of events that involve viewing feedback from other users.
+ *
+ * @return string[]
+ */
+ protected function feedback_viewed_events() {
+ throw new \coding_exception('Activities with a potential cognitive or social level that include viewing feedback ' .
+ 'should define "feedback_viewed_events" method or should override feedback_viewed method.');
+ }
+
+ /**
+ * Returns the list of events that involve replying to feedback from other users.
+ *
+ * @return string[]
+ */
+ protected function feedback_replied_events() {
+ throw new \coding_exception('Activities with a potential cognitive or social level that include replying to feedback ' .
+ 'should define "feedback_replied_events" method or should override feedback_replied method.');
+ }
+
+ /**
+ * Returns the list of events that involve submitting something after receiving feedback from other users.
+ *
+ * @return string[]
+ */
+ protected function feedback_submitted_events() {
+ throw new \coding_exception('Activities with a potential cognitive or social level that include viewing feedback ' .
+ 'should define "feedback_submitted_events" method or should override feedback_submitted method.');
+ }
+
+ /**
+ * Whether this user in this context did any of the provided actions (events)
+ *
+ * @param \cm_info $cm
+ * @param int $contextid
+ * @param int $userid
+ * @param string[] $eventnames
+ * @param int|false $after
+ * @return bool
+ */
+ 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;
+ }
+
+ /**
+ * Returns the date a user was graded.
+ *
+ * @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;
+ }
+
+ /**
+ * Returns the activities the user had access to between a time period.
+ *
+ * @param int $sampleid
+ * @param string $tablename
+ * @param int $starttime
+ * @param int $endtime
+ * @return array
+ */
+ 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;
+ }
+
+ /**
+ * Fetch acitivity logs from database
+ *
+ * @param array $activities
+ * @param int $starttime
+ * @param int $endtime
+ * @return array
+ */
+ 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);
+ $select = "contextid $contextsql AND timecreated > :starttime AND timecreated <= :endtime";
+ $params = $contextparams + array('starttime' => $starttime, 'endtime' => $endtime);
+
+ // Pity that we need to pass through logging readers API when most of the people just uses the standard one.
+ if (!$logstore = \core_analytics\manager::get_analytics_logstore()) {
+ throw new \coding_exception('No log store available');
+ }
+ $events = $logstore->get_events_select_iterator($select, $params, 'timecreated ASC', 0, 0);
+
+ // 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".
+ $processedevents = array();
+ foreach ($events as $event) {
+ if (!isset($processedevents[$event->contextid])) {
+ $processedevents[$event->contextid] = array();
+ }
+ if (!isset($processedevents[$event->contextid][$event->userid])) {
+ $processedevents[$event->contextid][$event->userid] = array();
+ }
+
+ // Contextid and userid have already been used to index the events, 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($processedevents[$event->contextid][$event->userid][$event->eventname])) {
+
+ // Remove all data that can change between events of the same type.
+ $data = (object)$event->get_data();
+ unset($data->id);
+ unset($data->anonymous);
+ unset($data->relateduserid);
+ unset($data->other);
+ unset($data->origin);
+ unset($data->ip);
+ $processedevents[$event->contextid][$event->userid][$event->eventname] = $data;
+ // We want timecreated attribute to be an array containing all user access times.
+ $processedevents[$event->contextid][$event->userid][$event->eventname]->timecreated = array();
+ }
+
+ // Add the event timecreated.
+ $processedevents[$event->contextid][$event->userid][$event->eventname]->timecreated[] = intval($event->timecreated);
+ }
+ $events->close();
+
+ return $processedevents;
+ }
+
+ /**
+ * Whether grades should be checked or not when looking for feedback.
+ *
+ * @return bool
+ */
+ protected function feedback_check_grades() {
+ return true;
+ }
+
+ /**
+ * Calculates the cognitive depth of a sample.
+ *
+ * @param int $sampleid
+ * @param string $tablename
+ * @param int $starttime
+ * @param int $endtime
+ * @return float|int|null
+ * @throws \coding_exception
+ */
+ protected function cognitive_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 cognitive depth go from 1 to 5.');
+ }
+ $scoreperlevel = $scoreperactivity / $potentiallevel;
+
+ switch ($potentiallevel) {
+ case 5:
+ // Cognitive level 5 is to submit after 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;
+ }
+
+ /**
+ * Calculates the social breadth of a sample.
+ *
+ * @param int $sampleid
+ * @param string $tablename
+ * @param int $starttime
+ * @param int $endtime
+ * @return float|int|null
+ */
+ protected function social_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) {
+
+ $potentiallevel = $this->get_social_breadth_level($cm);
+ if (!is_int($potentiallevel) || $potentiallevel > 2 || $potentiallevel < 1) {
+ throw new \coding_exception('Activities\' potential social breadth go from 1 to 2.');
+ }
+ $scoreperlevel = $scoreperactivity / $potentiallevel;
+ switch ($potentiallevel) {
+ case 2:
+ // Social breadth level 2 is to view feedback. (Same as cognitive level 3).
+
+ if ($this->any_feedback('viewed', $cm, $contextid, $user)) {
+ // Max score for level 2.
+ $score += $scoreperlevel * 2;
+ break;
+ }
+ // The user didn't reach the activity max social breadth, continue with level 1.
+
+ case 1:
+ // Social breadth level 1 is just accessing the activity.
+ if ($this->any_log($contextid, $user)) {
+ $score += $scoreperlevel;
+ }
+ }
+
+ }
+
+ // 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;
+ }
+
+ /**
+ * calculate_sample
+ *
+ * @throws \coding_exception
+ * @param int $sampleid
+ * @param string $tablename
+ * @param int $starttime
+ * @param int $endtime
+ * @return float|int|null
+ */
+ protected function calculate_sample($sampleid, $tablename, $starttime = false, $endtime = false) {
+ if ($this->get_indicator_type() == self::INDICATOR_COGNITIVE) {
+ return $this->cognitive_calculate_sample($sampleid, $tablename, $starttime, $endtime);
+ } else if ($this->get_indicator_type() == self::INDICATOR_SOCIAL) {
+ return $this->social_calculate_sample($sampleid, $tablename, $starttime, $endtime);
+ }
+ throw new \coding_exception("Indicator type is invalid.");
+ }
+
+ /**
+ * Defines indicator type.
+ *
+ * @return string
+ */
+ abstract protected function get_indicator_type();
+}
--- /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');
+ }
+
+ /**
+ * Returns 1 feature header for each of the classes.
+ *
+ * @return string[]
+ */
+ public static function get_feature_headers() {
+ $fullclassname = '\\' . get_called_class();
+
+ $headers = array($fullclassname);
+ foreach (self::get_classes() as $class) {
+ $headers[] = $fullclassname . '/' . $class;
+ }
+
+ return $headers;
+ }
+
+ /**
+ * Whether the value should be displayed or not.
+ *
+ * @param float $value
+ * @param string $subtype
+ * @return bool
+ */
+ 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 float $value
+ * @param string $subtype
+ * @return string
+ */
+ public function get_display_value($value, $subtype = false) {
+
+ $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;
+ }
+
+ /**
+ * get_display_style
+ *
+ * @param float $ignoredvalue
+ * @param string $ignoredsubtype
+ * @return int
+ */
+ public function get_display_style($ignoredvalue, $ignoredsubtype) {
+ // 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 \core_analytics\calculable::OUTCOME_NEUTRAL;
+ }
+
+ /**
+ * From calculated values to dataset features.
+ *
+ * One column for each class.
+ *
+ * @param float[] $calculatedvalues
+ * @return float[]
+ */
+ 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/>.
+
+/**
+ * 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;
+ }
+
+ /**
+ * get_feature_headers
+ *
+ * @return array
+ */
+ 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;
+ }
+
+ /**
+ * should_be_displayed
+ *
+ * @param float $value
+ * @param string $subtype
+ * @return bool
+ */
+ public function should_be_displayed($value, $subtype) {
+ if ($subtype != false) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * get_display_value
+ *
+ * @param float $value
+ * @param string $subtype
+ * @return string
+ */
+ 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) . '%';
+ }
+
+ /**
+ * get_calculation_outcome
+ *
+ * @param float $value
+ * @param string $subtype
+ * @return int
+ */
+ public function get_calculation_outcome($value, $subtype = false) {
+ if ($value < 0) {
+ return self::OUTCOME_NEGATIVE;
+ } else {
+ return self::OUTCOME_OK;
+ }
+ }
+
+ /**
+ * Converts the calculated values to a list of features for the dataset.
+ *
+ * @param array $calculatedvalues
+ * @return array
+ */
+ 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/>.
+
+/**
+ * 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 The full class name as a 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);
+
+ /**
+ * Is this sample from the $analysable valid?
+ *
+ * @param int $sampleid
+ * @param \core_analytics\analysable $analysable
+ * @param bool $fortraining
+ * @return bool
+ */
+ abstract public function is_valid_sample($sampleid, \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 $sampleid
+ * @param \core_analytics\analysable $analysable
+ * @param int|false $starttime Limit calculations to start time
+ * @param int|false $endtime Limit calculations to end time
+ * @return float|null
+ */
+ abstract protected function calculate_sample($sampleid, \core_analytics\analysable $analysable, $starttime = false, $endtime = false);
+
+ /**
+ * Is this target generating insights?
+ *
+ * Defaults to true.
+ *
+ * @return bool
+ */
+ public static function uses_insights() {
+ return true;
+ }
+
+ /**
+ * Based on facts (processed by machine learning backends) by default.
+ *
+ * @return bool
+ */
+ public static function based_on_assumptions() {
+ return false;
+ }
+
+ /**
+ * Suggested actions for a user.
+ *
+ * @param \core_analytics\prediction $prediction
+ * @param bool $includedetailsaction
+ * @return \core_analytics\prediction_action[]
+ */
+ public function prediction_actions(\core_analytics\prediction $prediction, $includedetailsaction = false) {
+ $actions = array();
+
+ if ($includedetailsaction) {
+
+ $predictionurl = new \moodle_url('/report/insights/prediction.php',
+ array('id' => $prediction->get_prediction_data()->id));
+
+ $actions['predictiondetails'] = new \core_analytics\prediction_action('predictiondetails', $prediction,
+ $predictionurl, new \pix_icon('t/preview', get_string('viewprediction', 'analytics')),
+ get_string('viewprediction', 'analytics'));
+ }
+
+ return $actions;
+ }
+
+ /**
+ * Callback to execute once a prediction has been returned from the predictions processor.
+ *
+ * @param int $modelid
+ * @param int $sampleid
+ * @param int $rangeindex
+ * @param \context $samplecontext
+ * @param float|int $prediction
+ * @param float $predictionscore
+ * @return void
+ */
+ public function prediction_callback($modelid, $sampleid, $rangeindex, \context $samplecontext, $prediction, $predictionscore) {
+ return;
+ }
+
+ /**
+ * Generates insights notifications
+ *
+ * @param int $modelid
+ * @param \context[] $samplecontexts
+ * @return void
+ */
+ public function generate_insight_notifications($modelid, $samplecontexts) {
+
+ foreach ($samplecontexts as $context) {
+
+ $insightinfo = new \stdClass();
+ $insightinfo->insightname = $this->get_name();
+ $insightinfo->contextname = $context->get_context_name();
+ $subject = get_string('insightmessagesubject', 'analytics', $insightinfo);
+
+ $users = $this->get_insights_users($context);
+
+ if (!$coursecontext = $context->get_course_context(false)) {
+ $coursecontext = \context_course::instance(SITEID);
+ }
+
+ foreach ($users as $user) {
+
+ $message = new \core\message\message();
+ $message->component = 'moodle';
+ $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('insightinfomessagehtml', 'analytics', $insighturl->out());
+ $message->smallmessage = get_string('insightinfomessage', 'analytics', $insighturl->out());
+ $message->contexturl = $insighturl->out(false);
+
+ message_send($message);
+ }
+ }
+
+ }
+
+ /**
+ * Returns the list of users that will receive insights notifications.
+ *
+ * Feel free to overwrite if you need to but keep in mind that moodle/analytics:listinsights
+ * capability is required to access the list of insights.
+ *
+ * @param \context $context
+ * @return array
+ */
+ protected function get_insights_users(\context $context) {
+ if ($context->contextlevel >= CONTEXT_COURSE) {
+ // At course level or below only enrolled users although this is not ideal for
+ // teachers assigned at category level.
+ $users = get_enrolled_users($context, 'moodle/analytics:listinsights');
+ } else {
+ $users = get_users_by_capability($context, 'moodle/analytics:listinsights');
+ }
+ return $users;
+ }
+
+ /**
+ * Returns an instance of the child class.
+ *
+ * Useful to reset cached data.
+ *
+ * @return \core_analytics\base\target
+ */
+ 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 $predictedvalue
+ * @param float $predictionscore
+ * @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;
+ }
+
+ 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 int $starttime
+ * @param int $endtime
+ * @return array The format to follow is [userid] = scalar|null
+ */
+ public function calculate($sampleids, \core_analytics\analysable $analysable, $starttime = false, $endtime = false) {
+
+ if (!PHPUNIT_TEST && CLI_SCRIPT) {
+ echo '.';
+ }
+
+ $calculations = [];
+ foreach ($sampleids as $sampleid => $unusedsampleid) {
+
+ // No time limits when calculating the target to train models.
+ $calculatedvalue = $this->calculate_sample($sampleid, $analysable, $starttime, $endtime);
+
+ 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;
+ }
+
+ /**
+ * Filters out invalid samples for training.
+ *
+ * @param int[] $sampleids
+ * @param \core_analytics\analysable $analysable
+ * @param bool $fortraining
+ * @return void
+ */
+ public function filter_out_invalid_samples(&$sampleids, \core_analytics\analysable $analysable, $fortraining = true) {
+ foreach ($sampleids as $sampleid => $unusedsampleid) {
+ if (!$this->is_valid_sample($sampleid, $analysable, $fortraining)) {
+ // Skip it and remove the sample from the list of calculated samples.
+ unset($sampleids[$sampleid]);
+ }
+ }
+ }
+}
--- /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 {
+
+ /**
+ * is_linear
+ *
+ * @return bool
+ */