MDL-64783 analytics: Upcoming activities due and related API changes

This commit is contained in:
David Monllaó
2019-04-09 00:25:24 +02:00
committed by Eloy Lafuente (stronk7)
parent 78e77fab2e
commit 50935afba4
22 changed files with 1494 additions and 86 deletions
+16 -27
View File
@@ -481,6 +481,20 @@ abstract class base {
throw new \coding_exception('This method should be implemented if static::processes_user_data returns true.');
}
/**
* Do this analyser's analysables have 1 single sample each?
*
* Overwrite and return true if your analysables only have
* one sample. The insights generated by models using this
* analyser will then include the suggested actions in the
* notification.
*
* @return bool
*/
public static function one_sample_per_analysable() {
return false;
}
/**
* Processes the analysable samples using the provided time splitting method.
*
@@ -518,10 +532,10 @@ abstract class base {
if ($includetarget) {
// All ranges are used when we are calculating data for training.
$ranges = $timesplitting->get_all_ranges();
$ranges = $timesplitting->get_training_ranges();
} else {
// The latest range that has not yet been used for prediction (it depends on the time range where we are right now).
$ranges = $this->get_most_recent_prediction_range($timesplitting);
$ranges = $timesplitting->get_most_recent_prediction_range();
}
// There is no need to keep track of the evaluated samples and ranges as we always evaluate the whole dataset.
@@ -651,31 +665,6 @@ abstract class base {
return $result;
}
/**
* Returns the most recent range that can be used to predict.
*
* @param \core_analytics\local\time_splitting\base $timesplitting
* @return array
*/
protected function get_most_recent_prediction_range($timesplitting) {
$now = time();
$ranges = $timesplitting->get_all_ranges();
// Opposite order as we are interested in the last range that can be used for prediction.
krsort($ranges);
// We already provided the analysable to the time splitting method, there is no need to feed it back.
foreach ($ranges as $rangeindex => $range) {
if ($timesplitting->ready_to_predict($range)) {
// We need to maintain the same indexes.
return array($rangeindex => $range);
}
}
return array();
}
/**
* Filters out samples that have already been used for training.
*
+106 -29
View File
@@ -173,61 +173,113 @@ abstract class base extends \core_analytics\calculable {
*
* @param int $modelid
* @param \context[] $samplecontexts
* @param \core_analytics\prediction[] $predictions
* @return void
*/
public function generate_insight_notifications($modelid, $samplecontexts) {
public function generate_insight_notifications($modelid, $samplecontexts, array $predictions = []) {
foreach ($samplecontexts as $context) {
$analyserclass = $this->get_analyser_class();
if ($analyserclass::one_sample_per_analysable()) {
foreach ($predictions as $prediction) {
$context = $samplecontexts[$prediction->get_prediction_data()->contextid];
$insightinfo = new \stdClass();
$insightinfo->insightname = $this->get_name();
$insightinfo->contextname = $context->get_context_name();
$subject = get_string('insightmessagesubject', 'analytics', $insightinfo);
$subject = $this->get_insight_subject($modelid, $context);
$users = $this->get_insights_users($context);
$users = $this->get_insights_users($context);
if (!$coursecontext = $context->get_course_context(false)) {
$coursecontext = \context_course::instance(SITEID);
}
if (!$coursecontext = $context->get_course_context(false)) {
$coursecontext = \context_course::instance(SITEID);
$predictionactions = $this->prediction_actions($prediction, false);
// TODO Proper language strings.
$fullmessage = '';
$fullmessagehtml = '';
$insighturl = null;
foreach ($predictionactions as $action) {
if (empty($insighturl)) {
// We use the primary action url as insight url so we logged that the user followed the provided link.
$insighturl = $action->get_action_url();
}
$fullmessage .= PHP_EOL . $action->get_action_name() . ': ' . $action->get_action_url()->out(false);
$fullmessagehtml .= '<br/>' . $action->get_action_name() . ': ' . $action->get_action_url()->out();
}
foreach ($users as $user) {
$this->generate_insight_notification($user, $subject, $insighturl, $coursecontext, $fullmessage, $fullmessagehtml);
}
}
foreach ($users as $user) {
} else {
$message = new \core\message\message();
$message->component = 'moodle';
$message->name = 'insights';
foreach ($samplecontexts as $context) {
$message->userfrom = \core_user::get_noreply_user();
$message->userto = $user;
$subject = $this->get_insight_subject($modelid, $context);
$users = $this->get_insights_users($context);
$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;
if (!$coursecontext = $context->get_course_context(false)) {
$coursecontext = \context_course::instance(SITEID);
}
$message->fullmessage = get_string('insightinfomessage', 'analytics', $insighturl->out(false));
$message->fullmessageformat = FORMAT_PLAIN;
$message->fullmessagehtml = get_string('insightinfomessagehtml', 'analytics', $insighturl->out());
$message->smallmessage = get_string('insightinfomessage', 'analytics', $insighturl->out(false));
$message->contexturl = $insighturl->out(false);
message_send($message);
foreach ($users as $user) {
$insighturl = $this->get_insight_context_url($modelid, $context);
$fullmessage = get_string('insightinfomessage', 'analytics', $insighturl->out(false));
$fullmessagehtml = get_string('insightinfomessagehtml', 'analytics', $insighturl->out());
$this->generate_insight_notification($user, $subject, $insighturl, $coursecontext, $fullmessage, $fullmessagehtml);
}
}
}
}
/**
* Generates a insight notification for the user.
*
* @param \stdClass $user The user
* @param string $subject The notification subject
* @param \moodle_url $insighturl The insight URL
* @param \context $coursecontext
* @param string $fullmessage
* @param string $fullmessagehtml
* @return null
*/
protected function generate_insight_notification(\stdClass $user, string $subject, \moodle_url $insighturl,
\context $coursecontext, string $fullmessage, string $fullmessagehtml) {
$message = new \core\message\message();
$message->component = 'moodle';
$message->name = 'insights';
$message->userfrom = \core_user::get_noreply_user();
$message->userto = $user;
$message->subject = $subject;
// Same than the subject.
$message->contexturlname = $message->subject;
$message->courseid = $coursecontext->instanceid;
$message->fullmessage = $fullmessage;
$message->fullmessageformat = FORMAT_PLAIN;
$message->fullmessagehtml = $fullmessagehtml;
$message->smallmessage = $fullmessage;
$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.
* or moodle/analytics:listowninsights 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) {
if ($context->contextlevel === CONTEXT_USER) {
$users = [$context->instanceid => \core_user::get_user($context->instanceid)];
} else 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');
@@ -237,6 +289,31 @@ abstract class base extends \core_analytics\calculable {
return $users;
}
/**
* URL to the insight.
*
* @param int $modelid
* @param \context $context
* @return \moodle_url
*/
protected function get_insight_context_url($modelid, $context) {
return new \moodle_url('/report/insights/insights.php?modelid=' . $modelid . '&contextid=' . $context->id);
}
/**
* The insight notification subject.
*
* @param int $modelid
* @param \context $context
* @return string
*/
protected function get_insight_subject(int $modelid, \context $context) {
$insightinfo = new \stdClass();
$insightinfo->insightname = $this->get_name();
$insightinfo->contextname = $context->get_context_name();
return get_string('insightmessagesubject', 'analytics', $insightinfo);
}
/**
* Returns an instance of the child class.
*
@@ -40,6 +40,13 @@ abstract class base {
*/
protected $id;
/**
* The model id.
*
* @var int
*/
protected $modelid;
/**
* @var \core_analytics\analysable
*/
@@ -75,7 +82,8 @@ abstract class base {
* Define the time splitting methods ranges.
*
* 'time' value defines when predictions are executed, their values will be compared with
* the current time in ready_to_predict
* the current time in ready_to_predict. The ranges should be sorted by 'time' in
* ascending order.
*
* @return array('start' => time(), 'end' => time(), 'time' => time())
*/
@@ -163,6 +171,20 @@ abstract class base {
return false;
}
/**
* Should we use this time range for training?
*
* @param array $range
* @return bool
*/
public function ready_to_train($range) {
$now = time();
if ($range['time'] <= $now && $range['end'] <= $now) {
return true;
}
return false;
}
/**
* Calculates indicators and targets.
*
@@ -341,7 +363,7 @@ abstract class base {
*/
protected function fill_dataset(&$dataset, $calculatedtarget = false) {
$nranges = count($this->get_all_ranges());
$nranges = count($this->get_distinct_ranges());
foreach ($dataset as $uniquesampleid => $unmodified) {
@@ -421,6 +443,53 @@ abstract class base {
return $this->ranges;
}
/**
* By default all ranges are for training.
*
* @return array
*/
public function get_training_ranges() {
return $this->ranges;
}
/**
* Returns the distinct range indexes in this time splitting method.
*
* @return int[]
*/
public function get_distinct_ranges() {
if ($this->include_range_info_in_training_data()) {
return array_keys($this->ranges);
} else {
return [0];
}
}
/**
* Returns the most recent range that can be used to predict.
*
* This method is only called when calculating predictions.
*
* @return array
*/
public function get_most_recent_prediction_range() {
$ranges = $this->get_all_ranges();
// Opposite order as we are interested in the last range that can be used for prediction.
krsort($ranges);
// We already provided the analysable to the time splitting method, there is no need to feed it back.
foreach ($ranges as $rangeindex => $range) {
if ($this->ready_to_predict($range)) {
// We need to maintain the same indexes.
return array($rangeindex => $range);
}
}
return array();
}
/**
* Returns range data by its index.
*
@@ -441,7 +510,7 @@ abstract class base {
* @param int $rangeindex
* @return string
*/
public function append_rangeindex($sampleid, $rangeindex) {
public final function append_rangeindex($sampleid, $rangeindex) {
return $sampleid . '-' . $rangeindex;
}
@@ -451,10 +520,34 @@ abstract class base {
* @param string $uniquesampleid
* @return array array($sampleid, $rangeindex)
*/
public function infer_sample_info($uniquesampleid) {
public final function infer_sample_info($uniquesampleid) {
return explode('-', $uniquesampleid);
}
/**
* Whether to include the range index in the training data or not.
*
* By default, we consider that the different time ranges included in a time splitting method may not be
* compatible between them (i.e. the indicators calculated at the end of the course can easily
* differ from indicators calculated at the beginning of the course). So we include the range index as
* one of the variables that the machine learning backend uses to generate predictions.
*
* If the indicators calculated using the different time ranges available in this time splitting method
* are comparable you can overwrite this method to return false.
*
* Note that:
* - This is only relevant for models whose predictions are not based on assumptions
* (i.e. the ones using a machine learning backend to generate predictions).
* - The ranges can only be included in the training data when
* we know the final number of ranges the time splitting method will have. E.g.
* We can not know the final number of ranges of a 'daily' time splitting method
* as we will have one new range every day.
* @return bool
*/
public function include_range_info_in_training_data() {
return true;
}
/**
* Returns the headers for the csv file based on the indicators and the target.
*
@@ -473,9 +566,9 @@ abstract class base {
// We always have 1 column for each time splitting method range, it does not depend on how
// many ranges we calculated.
$ranges = $this->get_all_ranges();
$ranges = $this->get_distinct_ranges();
if (count($ranges) > 1) {
foreach ($ranges as $rangeindex => $range) {
foreach ($ranges as $rangeindex) {
$headers[] = 'range/' . $rangeindex;
}
}
@@ -0,0 +1,143 @@
<?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/>.
/**
* Time splitting method that generates predictions regularly.
*
* @package core_analytics
* @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_analytics\local\time_splitting;
defined('MOODLE_INTERNAL') || die();
/**
* Time splitting method that generates predictions periodically.
*
* @package core_analytics
* @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class periodic extends base {
/**
* The periodicity of the predictions / training data generation.
*
* @return \DateInterval
*/
abstract protected function periodicity();
/**
* Returns whether the course can be processed by this time splitting method or not.
*
* @param \core_analytics\analysable $analysable
* @return bool
*/
public function is_valid_analysable(\core_analytics\analysable $analysable) {
if (!$analysable->get_start()) {
return false;
}
return true;
}
/**
* define_ranges
*
* @return array
*/
protected function define_ranges() {
$periodicity = $this->periodicity();
$now = new \DateTimeImmutable('now', \core_date::get_server_timezone_object());
if ($this->analysable->get_end()) {
$end = (new \DateTimeImmutable())->setTimestamp($this->analysable->get_end());
}
$next = (new \DateTimeImmutable())->setTimestamp($this->analysable->get_start());
$ranges = [];
while ($next < $now &&
(empty($end) || $next < $end)) {
$range = $this->get_next_range($next);
if ($range) {
$ranges[] = $range;
}
$next = $next->add($periodicity);
}
$nextrange = $this->get_next_range($next);
if ($this->ready_to_predict($nextrange) && (empty($end) || $next < $end)) {
// Add the next one if we we have not reached the analysable end yet.
// It will be used to get predictions.
$ranges[] = $nextrange;
}
return $ranges;
}
/**
* Overwritten as all generated rows are comparable.
*
* @return bool
*/
public function include_range_info_in_training_data() {
return false;
}
/**
* Overwritting as the last range may be for prediction.
*
* @return array
*/
public function get_training_ranges() {
// Cloning the array.
$trainingranges = $this->ranges;
foreach ($trainingranges as $rangeindex => $range) {
if (!$this->ready_to_train($range)) {
unset($trainingranges[$rangeindex]);
}
}
return $trainingranges;
}
/**
* The next range is based on the past period.
*
* @param \DateTimeImmutable $next
* @return array
*/
protected function get_next_range(\DateTimeImmutable $next) {
$end = $next->getTimestamp();
$start = $next->sub($this->periodicity())->getTimestamp();
if ($start < $this->analysable->get_start()) {
// We skip the first range generated as its start is prior to the analysable start.
return false;
}
return [
'start' => $start,
'end' => $end,
'time' => $end
];
}
}
@@ -0,0 +1,54 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Time splitting method that generates predictions periodically.
*
* @package core_analytics
* @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_analytics\local\time_splitting;
defined('MOODLE_INTERNAL') || die();
/**
* Time splitting method that generates predictions periodically.
*
* @package core_analytics
* @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class upcoming_periodic extends periodic {
/**
* The next range indicator calculations should be based on upcoming dates.
*
* @param \DateTimeImmutable $next
* @return array
*/
protected function get_next_range(\DateTimeImmutable $next) {
$start = $next->getTimestamp();
$end = $next->add($this->periodicity())->getTimestamp();
return [
'start' => $start,
'end' => $end,
'time' => $start
];
}
}
+15 -2
View File
@@ -80,10 +80,23 @@ class manager {
*
* @throws \required_capability_exception
* @param \context $context
* @param bool $return The method returns a bool if true.
* @return void
*/
public static function check_can_list_insights(\context $context) {
require_capability('moodle/analytics:listinsights', $context);
public static function check_can_list_insights(\context $context, bool $return = false) {
global $USER;
if ($context->contextlevel === CONTEXT_USER && $context->instanceid == $USER->id) {
$capability = 'moodle/analytics:listowninsights';
} else {
$capability = 'moodle/analytics:listinsights';
}
if ($return) {
return has_capability($capability, $context);
} else {
require_capability($capability, $context);
}
}
/**
+72 -10
View File
@@ -787,11 +787,12 @@ class model {
}
if ($result->predictions) {
$samplecontexts = $this->execute_prediction_callbacks($result->predictions, $indicatorcalculations);
list($samplecontexts, $predictionrecords) = $this->execute_prediction_callbacks($result->predictions,
$indicatorcalculations);
}
if (!empty($samplecontexts) && $this->uses_insights()) {
$this->trigger_insights($samplecontexts);
$this->trigger_insights($samplecontexts, $predictionrecords);
}
$this->flag_file_as_used($samplesfile, 'predicted');
@@ -855,7 +856,7 @@ class model {
* @param array $indicatorcalculations
* @return array
*/
protected function execute_prediction_callbacks($predictions, $indicatorcalculations) {
protected function execute_prediction_callbacks(&$predictions, $indicatorcalculations) {
// Here we will store all predictions' contexts, this will be used to limit which users will see those predictions.
$samplecontexts = array();
@@ -887,19 +888,38 @@ class model {
$this->save_predictions($records);
}
return $samplecontexts;
return [$samplecontexts, $records];
}
/**
* Generates insights and updates the cache.
*
* @param \context[] $samplecontexts
* @param \stdClass[] $predictionrecords
* @return void
*/
protected function trigger_insights($samplecontexts) {
protected function trigger_insights($samplecontexts, $predictionrecords) {
// Notify the target that all predictions have been processed.
$this->get_target()->generate_insight_notifications($this->model->id, $samplecontexts);
if ($this->get_analyser()::one_sample_per_analysable()) {
// We need to do something unusual here. self::save_predictions uses the bulk-insert function (insert_records()) for
// performance reasons and that function does not return us the inserted ids. We need to retrieve them from
// the database, and we need to do it using one single database query (for performance reasons as well).
$predictionrecords = $this->add_prediction_ids($predictionrecords);
// Get \core_analytics\prediction objects also fetching the samplesdata. This costs us
// 1 db read, but we have to pay it if we want that our insights include links to the
// suggested actions.
$predictions = array_map(function($predictionobj) {
$prediction = new \core_analytics\prediction($predictionobj, $this->prediction_sample_data($predictionobj));
return $prediction;
}, $predictionrecords);
} else {
$predictions = false;
}
$this->get_target()->generate_insight_notifications($this->model->id, $samplecontexts, $predictions);
// Update cache.
$cache = \cache::make('core', 'contextwithinsights');
@@ -969,16 +989,20 @@ class model {
$this->get_target()->add_sample_data($data->indicatorsdata);
// Append new elements (we can not get duplicates because sample-analysable relation is N-1).
$range = $this->get_time_splitting()->get_range_by_index($rangeindex);
$timesplitting = $this->get_time_splitting();
$timesplitting->set_analysable($data->analysable);
$range = $timesplitting->get_range_by_index($rangeindex);
$this->get_target()->filter_out_invalid_samples($data->sampleids, $data->analysable, false);
$calculations = $this->get_target()->calculate($data->sampleids, $data->analysable, $range['start'], $range['end']);
// Missing $indicatorcalculations values in $calculations are caused by is_valid_sample. We need to remove
// these $uniquesampleid from $indicatorcalculations because otherwise they will be stored as calculated
// by self::save_prediction.
$indicatorcalculations = array_filter($indicatorcalculations, function($indicators, $uniquesampleid) use ($calculations) {
list($sampleid, $rangeindex) = $this->get_time_splitting()->infer_sample_info($uniquesampleid);
if (!isset($calculations[$sampleid])) {
$indicatorcalculations = array_filter($indicatorcalculations, function($indicators, $uniquesampleid)
use ($calculations, $rangeindex) {
list($sampleid, $indicatorsrangeindex) = $this->get_time_splitting()->infer_sample_info($uniquesampleid);
if ($rangeindex == $indicatorsrangeindex && !isset($calculations[$sampleid])) {
return false;
}
return true;
@@ -1642,6 +1666,44 @@ class model {
$DB->update_record('analytics_models', $this->model);
}
/**
* Adds the id from {analytics_predictions} db table to the prediction \stdClass objects.
*
* @param \stdClass[] $predictionrecords
* @return \stdClass[] The prediction records including their ids in {analytics_predictions} db table.
*/
private function add_prediction_ids($predictionrecords) {
global $DB;
$firstprediction = reset($predictionrecords);
$contextids = array_map(function($predictionobj) {
return $predictionobj->contextid;
}, $predictionrecords);
list($contextsql, $contextparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED);
// We select the fields that will allow us to map ids to $predictionrecords. Given that we already filter by modelid
// we have enough with sampleid and rangeindex. The reason is that the sampleid relation to a site is N - 1.
$fields = 'id, sampleid, rangeindex';
// We include the contextid and the timecreated filter to reduce the number of records in $dbpredictions. We can not
// add as many OR conditions as records in $predictionrecords.
$sql = "SELECT $fields
FROM {analytics_predictions}
WHERE modelid = :modelid
AND contextid $contextsql
AND timecreated >= :firsttimecreated";
$params = $contextparams + ['modelid' => $this->model->id, 'firsttimecreated' => $firstprediction->timecreated];
$dbpredictions = $DB->get_recordset_sql($sql, $params);
foreach ($dbpredictions as $id => $dbprediction) {
// The append_rangeindex implementation is the same regardless of the time splitting method in use.
$uniqueid = $this->get_time_splitting()->append_rangeindex($dbprediction->sampleid, $dbprediction->rangeindex);
$predictionrecords[$uniqueid]->id = $dbprediction->id;
}
return $predictionrecords;
}
/**
* Purges the insights cache.
*/
+26 -3
View File
@@ -40,6 +40,11 @@ class prediction_action {
*/
protected $actionname = null;
/**
* @var \moodle_url
*/
protected $actionurl = null;
/**
* @var \action_menu_link
*/
@@ -61,16 +66,17 @@ class prediction_action {
$text, $primary = false, $attributes = array()) {
$this->actionname = $actionname;
$this->text = $text;
// We want to track how effective are our suggested actions, we pass users through a script that will log these actions.
$params = array('action' => $this->actionname, 'predictionid' => $prediction->get_prediction_data()->id,
'forwardurl' => $actionurl->out(false));
$url = new \moodle_url('/report/insights/action.php', $params);
$this->actionurl = new \moodle_url('/report/insights/action.php', $params);
if ($primary === false) {
$this->actionlink = new \action_menu_link_secondary($url, $icon, $text, $attributes);
$this->actionlink = new \action_menu_link_secondary($this->actionurl, $icon, $this->text, $attributes);
} else {
$this->actionlink = new \action_menu_link_primary($url, $icon, $text, $attributes);
$this->actionlink = new \action_menu_link_primary($this->actionurl, $icon, $this->text, $attributes);
}
}
@@ -83,6 +89,15 @@ class prediction_action {
return $this->actionname;
}
/**
* Returns the url to the action.
*
* @return \moodle_url
*/
public function get_action_url() {
return $this->actionurl;
}
/**
* Returns the link to the action.
*
@@ -91,4 +106,12 @@ class prediction_action {
public function get_action_link() {
return $this->actionlink;
}
/**
* Returns the action text.
* @return string
*/
public function get_text() {
return $this->text;
}
}
+203
View File
@@ -0,0 +1,203 @@
<?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 user analysable
*
* @package core_analytics
* @copyright 2019 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();
/**
* Moodle user analysable
*
* @package core_analytics
* @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class user implements \core_analytics\analysable {
/**
* @var bool Has this user data been already loaded.
*/
protected $loaded = false;
/**
* @var int $cachedid self::$cachedinstance analysable id.
*/
protected static $cachedid = 0;
/**
* @var \core_analytics\user $cachedinstance
*/
protected static $cachedinstance = null;
/**
* User object
*
* @var \stdClass
*/
protected $user = null;
/**
* The user context.
*
* @var \context_user
*/
protected $usercontext = null;
/**
* Constructor.
*
* Use self::instance() instead to get cached copies of the class. Instances obtained
* through this constructor will not be cached.
*
* Lazy load of the analysable data.
*
* @param int|\stdClass $user User id
* @return void
*/
public function __construct($user) {
if (is_scalar($user)) {
$this->user = new \stdClass();
$this->user->id = $user;
} else {
$this->user = $user;
}
}
/**
* Returns an analytics user instance.
*
* Lazy load of analysable data.
*
* @param int|\stdClass $user User object or user id
* @return \core_analytics\user
*/
public static function instance($user) {
$userid = $user;
if (!is_scalar($userid)) {
$userid = $user->id;
}
if (self::$cachedid === $userid) {
return self::$cachedinstance;
}
$cachedinstance = new \core_analytics\user($user);
self::$cachedinstance = $cachedinstance;
self::$cachedid = (int)$userid;
return self::$cachedinstance;
}
/**
* get_id
*
* @return int
*/
public function get_id() {
return $this->user->id;
}
/**
* Loads the analytics user object.
*
* @return void
*/
protected function load() {
// The instance constructor could be already loaded with the full user object. Using email
// because it is a required user field.
if (empty($this->user->email)) {
$this->user = \core_user::get_user($this->user->id);
}
$this->usercontext = $this->get_context();
$this->now = time();
// Flag the instance as loaded.
$this->loaded = true;
}
/**
* The user full name.
*
* @return string
*/
public function get_name() {
if (!$this->loaded) {
$this->load();
}
return fullname($this->user);
}
/**
* get_context
*
* @return \context
*/
public function get_context() {
if ($this->usercontext === null) {
$this->usercontext = \context_user::instance($this->user->id);
}
return $this->usercontext;
}
/**
* Get the start timestamp.
*
* @return int
*/
public function get_start() {
if (!$this->loaded) {
$this->load();
}
return $this->user->timecreated;
}
/**
* Get the end timestamp.
*
* @return int
*/
public function get_end() {
return self::MAX_TIME;
}
/**
* Returns a user plain object.
*
* @return \stdClass
*/
public function get_user_data() {
if (!$this->loaded) {
$this->load();
}
return $this->user;
}
}
+51
View File
@@ -0,0 +1,51 @@
<?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/>.
/**
* Test time splitting.
*
* @package core_analytics
* @copyright 2019 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Test time splitting.
*
* @package core_analytics
* @copyright 2019 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class test_timesplitting_seconds extends \core_analytics\local\time_splitting\periodic {
/**
* @return \DateInterval
*/
public function periodicity() {
return new \DateInterval('PT1S');
}
/**
* Just to comply with the interface.
*
* @return \lang_string
*/
public static function get_name() : \lang_string {
return new \lang_string('error');
}
}
+51
View File
@@ -0,0 +1,51 @@
<?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/>.
/**
* Time splitting method that generates weekly predictions.
*
* @package core_analytics
* @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Time splitting method that generates weekly predictions.
*
* @package core_analytics
* @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class test_timesplitting_weekly extends \core_analytics\local\time_splitting\periodic {
/**
* The time splitting method name.
* @return \lang_string
*/
public static function get_name() : \lang_string {
return new \lang_string('error');
}
/**
* Once per week.
* @return \DateInterval
*/
public function periodicity() {
return new \DateInterval('P1W');
}
}
+14
View File
@@ -13,6 +13,20 @@ information provided here is intended especially for developers.
has been replaced with automatic update of models provided by the core moodle component. There
is no need to call this method explicitly any more. Instead, adding new models can be achieved
by updating the lib/db/analytics.php file and bumping the core version.
* \core_analytics\local\time_splitting\base::append_rangeindex and
\core_analytics\local\time_splitting\base::infer_sample_info are now marked as final and can not
be overwritten.
* \core_analytics\model::execute_prediction_callbacks now returns an array with both sample's contexts
and the prediction records.
* \core_analytics\local\analyser\base::get_most_recent_prediction_range has been moved to
\core_analytics\local\time_splitting\base::get_most_recent_prediction_range and it is not overwritable
by time splitting methods.
* Time splitting methods can now re-implement include_range_info_in_training_data() and
get_training_ranges() methods. They can be used to create time splitting methods with a pre-defined
number of ranges.
* Analysers can overwrite a new one_sample_per_analysable method if the analysables they use only have
one sample. The insights generated by models will then include the suggested actions in
the notification.
=== 3.5 ===
@@ -0,0 +1,100 @@
<?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/>.
/**
* Activities due indicator.
*
* @package core
* @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\analytics\indicator;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/calendar/externallib.php');
/**
* Activities due indicator.
*
* @package core
* @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class activities_due extends \core_analytics\local\indicator\binary {
/**
* Returns the name.
*
* If there is a corresponding '_help' string this will be shown as well.
*
* @return \lang_string
*/
public static function get_name() : \lang_string {
return new \lang_string('indicator:activitiesdue');
}
/**
* required_sample_data
*
* @return string[]
*/
public static function required_sample_data() {
return array('user');
}
/**
* calculate_sample
*
* @param int $sampleid
* @param string $sampleorigin
* @param int $starttime
* @param int $endtime
* @return float
*/
protected function calculate_sample($sampleid, $sampleorigin, $starttime = false, $endtime = false) {
$actionevents = \core_calendar_external::get_calendar_action_events_by_timesort($starttime, $endtime, 0, 1,
true, $sampleid);
if ($actionevents->events) {
// We first need to check that at least one of the core_calendar_provide_event_action
// callbacks has the $userid param.
foreach ($actionevents->events as $event) {
$nparams = $this->get_provide_event_action_num_params($event->modulename);
if ($nparams > 2) {
return self::get_max_value();
}
}
}
return self::get_min_value();
}
/**
* Returns the number of params declared in core_calendar_provide_event_action's implementation.
*
* @param string $modulename The module name
* @return int
*/
private function get_provide_event_action_num_params(string $modulename) {
$functionname = 'mod_' . $modulename . '_core_calendar_provide_event_action';
$reflection = new \ReflectionFunction($functionname);
return $reflection->getNumberOfParameters();
}
}
+2
View File
@@ -70,6 +70,8 @@ $string['fixedack'] = 'Acknowledged';
$string['insightmessagesubject'] = 'New insight for "{$a->contextname}": {$a->insightname}';
$string['insightinfomessage'] = 'The system generated some insights for you: {$a}';
$string['insightinfomessagehtml'] = 'The system generated some insights for you: <a href="{$a}">{$a}</a>.';
$string['insightinfomessageprediction'] = 'The system generated some insights for you: {$a}';
$string['insightinfomessagepredictionhtml'] = 'The system generated some insights for you: <a href="{$a}">{$a}</a>.';
$string['invalidtimesplitting'] = 'Model with ID {$a} needs a time-splitting method before it can be used to train.';
$string['invalidanalysablefortimesplitting'] = 'It cannot be analysed using {$a} time-splitting method.';
$string['nocourses'] = 'No courses to analyse';
+7 -4
View File
@@ -1053,6 +1053,8 @@ $string['indicator:accessesafterend'] = 'Course accessed after end date';
$string['indicator:accessesafterend_help'] = 'This indicator reflects if the student accessed the course after the course end date.';
$string['indicator:accessesbeforestart'] = 'Course accessed before start date';
$string['indicator:accessesbeforestart_help'] = 'This indicator reflects if the student accessed the course before the course start date.';
$string['indicator:activitiesdue'] = 'Activities due';
$string['indicator:activitiesdue_help'] = 'The user has activities due.';
$string['indicator:anywrite'] = 'Any write action';
$string['indicator:anywrite_help'] = 'This indicator represents any write (submit) action taken by the student.';
$string['indicator:anywriteincourse'] = 'Any write action in the course';
@@ -1980,6 +1982,8 @@ $string['target:coursegradetopass'] = 'Students at risk of not getting the minim
$string['target:coursegradetopass_help'] = 'This target describes whether the student is at risk of not getting the minimum grade to pass the course.';
$string['target:noteachingactivity'] = 'No teaching';
$string['target:noteachingactivity_help'] = 'This target describes whether courses due to start in the coming week will have teaching activity.';
$string['target:upcomingactivitiesdue'] = 'Upcoming activities due';
$string['target:upcomingactivitiesdue_help'] = 'This target generates reminders for upcoming activities due.';
$string['targetlabelstudentcompletionno'] = 'Student who is likely to meet the course completion conditions';
$string['targetlabelstudentcompletionyes'] = 'Student at risk of not meeting the course completion conditions';
$string['targetlabelstudentcompetenciesno'] = 'Student who is likely to achieve the competencies assigned to a course';
@@ -2014,10 +2018,8 @@ $string['timesplitting:quartersaccum'] = 'Quarters accumulative';
$string['timesplitting:quartersaccum_help'] = 'This time-splitting method divides the course into quarters (4 equal parts), with each prediction being based on the data of all previous quarters.';
$string['timesplitting:singlerange'] = 'Single range';
$string['timesplitting:singlerange_help'] = 'This time-splitting method considers the entire course as a single span.';
$string['timesplitting:weekly'] = 'Weekly';
$string['timesplitting:weekly_help'] = 'This time-splitting method divides the course into weeks (7 days), with each prediction being based on the data of only the most recent previous week.';
$string['timesplitting:weeklyaccum'] = 'Weekly accumulative';
$string['timesplitting:weeklyaccum_help'] = 'This time-splitting method divides the course into weeks (7 days), with each prediction being based on the data of all previous weeks.';
$string['timesplitting:upcomingweek'] = 'Upcoming week';
$string['timesplitting:upcomingweek_help'] = 'This time-splitting method generates predictions every week. The indicators calculations will be based on the upcoming week.';
$string['thanks'] = 'Thanks';
$string['theme'] = 'Theme';
$string['themes'] = 'Themes';
@@ -2145,6 +2147,7 @@ $string['view'] = 'View';
$string['viewing'] = 'Viewing:';
$string['viewallcourses'] = 'View all courses';
$string['viewallcoursescategories'] = 'View all courses and categories';
$string['viewdashboard'] = 'View dashboard';
$string['viewmore'] = 'View more';
$string['viewallsubcategories'] = 'View all subcategories';
$string['viewfileinpopup'] = 'View file in a popup window';
@@ -44,6 +44,15 @@ class courses extends \core_analytics\local\analyser\by_course {
return 'course';
}
/**
* Just one sample per analysable.
*
* @return bool
*/
public static function one_sample_per_analysable() {
return true;
}
/**
* Returns the analysable of a sample
*
+187
View File
@@ -0,0 +1,187 @@
<?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/>.
/**
* Users analyser (insights for users).
*
* @package core
* @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\analytics\analyser;
defined('MOODLE_INTERNAL') || die();
/**
* Users analyser (insights for users).
*
* @package core
* @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class users extends \core_analytics\local\analyser\base {
/**
* The site users are the analysable elements returned by this analyser.
*
* @return \core_analytics\analysable[]
*/
public function get_analysables() {
global $DB, $CFG;
$siteadmins = explode(',', $CFG->siteadmins);
$params = ['deleted' => 0, 'confirmed' => 1, 'suspended' => 0];
$users = $DB->get_records('user', $params, 'timecreated', 'id');
$analysables = array();
foreach ($users as $user) {
if (in_array($user->id, $siteadmins) || isguestuser($user->id)) {
// Skip admins and the guest user.
continue;
}
$analysable = \core_analytics\user::instance($user->id);
$analysables[$analysable->get_id()] = $analysable;
}
if (empty($analysables)) {
$this->log[] = get_string('nousersfound');
}
return $analysables;
}
/**
* Just one sample per analysable.
*
* @return bool
*/
public static function one_sample_per_analysable() {
return true;
}
/**
* Samples origin is user table.
*
* @return string
*/
public function get_samples_origin() {
return 'user';
}
/**
* Returns the analysable of a sample
*
* @param int $sampleid
* @return \core_analytics\analysable
*/
public function get_sample_analysable($sampleid) {
return \core_analytics\user::instance($sampleid);
}
/**
* This provides samples' user and context.
*
* @return string[]
*/
protected function provided_sample_data() {
return ['user', 'context'];
}
/**
* Returns the context of a sample.
*
* @param int $sampleid
* @return \context
*/
public function sample_access_context($sampleid) {
return \context_user::instance($sampleid);
}
/**
* This will return just one user as we analyse users separately.
*
* @param \core_analytics\analysable $user
* @return array
*/
protected function get_all_samples(\core_analytics\analysable $user) {
$context = \context_user::instance($user->get_id());
// Just 1 sample per analysable.
return [
[$user->get_id() => $user->get_id()],
[$user->get_id() => ['user' => $user->get_user_data(), 'context' => $context]]
];
}
/**
* Returns samples data from sample ids.
*
* @param int[] $sampleids
* @return array
*/
public function get_samples($sampleids) {
global $DB;
list($sql, $params) = $DB->get_in_or_equal($sampleids, SQL_PARAMS_NAMED);
$users = $DB->get_records_select('user', "id $sql", $params);
$userids = array_keys($users);
$sampleids = array_combine($userids, $userids);
$users = array_map(function($user) {
return ['user' => $user, 'context' => \context_user::instance($user->id)];
}, $users);
// No related data attached.
return [$sampleids, $users];
}
/**
* Returns the description of a sample.
*
* @param int $sampleid
* @param int $contextid
* @param array $sampledata
* @return array array(string, \renderable)
*/
public function sample_description($sampleid, $contextid, $sampledata) {
$description = fullname($sampledata['user']);
return [$description, new \user_picture($sampledata['user'])];
}
/**
* We need to delete associated data if a user requests his data to be deleted.
*
* @return bool
*/
public function processes_user_data() {
return true;
}
/**
* Join the samples origin table with the user id table.
*
* @param string $sampletablealias
* @return string
*/
public function join_sample_user($sampletablealias) {
return "JOIN {user} u ON u.id = {$sampletablealias}.sampleid";
}
}
@@ -0,0 +1,53 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Time splitting method that generates weekly predictions.
*
* @package core_analytics
* @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\analytics\time_splitting;
defined('MOODLE_INTERNAL') || die();
/**
* Time splitting method that generates weekly predictions.
*
* @package core_analytics
* @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class upcoming_week extends \core_analytics\local\time_splitting\upcoming_periodic {
/**
* The time splitting method name.
* @return \lang_string
*/
public static function get_name() : \lang_string {
return new \lang_string('timesplitting:upcomingweek');
}
/**
* Once per week.
* @return \DateInterval
*/
public function periodicity() {
return new \DateInterval('P1W');
}
}
+8
View File
@@ -2469,4 +2469,12 @@ $capabilities = array(
'user' => CAP_ALLOW,
],
],
'moodle/analytics:listowninsights' => array(
'captype' => 'read',
'contextlevel' => CONTEXT_SYSTEM,
'archetypes' => array(
'user' => CAP_ALLOW
)
),
);
+121 -4
View File
@@ -25,7 +25,8 @@
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/../../analytics/tests/fixtures/test_target_shortname.php');
require_once(__DIR__ . '/../../analytics/tests/fixtures/test_timesplitting_seconds.php');
require_once(__DIR__ . '/../../analytics/tests/fixtures/test_timesplitting_weekly.php');
require_once(__DIR__ . '/../../lib/enrollib.php');
/**
@@ -52,8 +53,8 @@ class core_analytics_time_splittings_testcase extends advanced_testcase {
'startdate' => mktime(8, 15, 32, 10, 24, 2015),
'enddate' => mktime(12, 12, 31, 10, 24, 2016),
);
$course = $this->getDataGenerator()->create_course($params);
$this->analysable = new \core_analytics\course($course);
$this->course = $this->getDataGenerator()->create_course($params);
$this->analysable = new \core_analytics\course($this->course);
}
/**
@@ -70,7 +71,8 @@ class core_analytics_time_splittings_testcase extends advanced_testcase {
'\core\analytics\time_splitting\no_splitting',
'\core\analytics\time_splitting\quarters',
'\core\analytics\time_splitting\quarters_accum',
'\core\analytics\time_splitting\single_range'
'\core\analytics\time_splitting\single_range',
'\core\analytics\time_splitting\upcoming_week',
);
// Check that defined ranges are valid (tested through validate_ranges).
@@ -95,6 +97,8 @@ class core_analytics_time_splittings_testcase extends advanced_testcase {
$quarters->set_analysable($this->analysable);
$ranges = $quarters->get_all_ranges();
$this->assertCount(4, $ranges);
$this->assertCount(4, $quarters->get_training_ranges());
$this->assertCount(4, $quarters->get_distinct_ranges());
$this->assertGreaterThan($ranges[0]['start'], $ranges[1]['start']);
$this->assertGreaterThan($ranges[0]['end'], $ranges[1]['start']);
@@ -121,6 +125,8 @@ class core_analytics_time_splittings_testcase extends advanced_testcase {
$accum->set_analysable($this->analysable);
$ranges = $accum->get_all_ranges();
$this->assertCount(4, $ranges);
$this->assertCount(4, $accum->get_training_ranges());
$this->assertCount(4, $accum->get_distinct_ranges());
$this->assertEquals($ranges[0]['start'], $ranges[1]['start']);
$this->assertEquals($ranges[1]['start'], $ranges[2]['start']);
@@ -180,4 +186,115 @@ class core_analytics_time_splittings_testcase extends advanced_testcase {
$range['time'] = 0;
$this->assertTrue($singlerange->ready_to_predict($range));
}
/**
* test_periodic
*
* @return void
*/
public function test_periodic() {
// Using a finished course.
$weekly = new test_timesplitting_weekly();
$weekly->set_analysable($this->analysable);
$this->assertCount(1, $weekly->get_distinct_ranges());
$ranges = $weekly->get_all_ranges();
$this->assertEquals(52, count($ranges));
$this->assertEquals($this->course->startdate, $ranges[0]['start']);
$this->assertNotEquals($this->course->startdate, $ranges[0]['time']);
// The analysable is finished so all ranges are available for training.
$this->assertCount(count($ranges), $weekly->get_training_ranges());
$ranges = $weekly->get_most_recent_prediction_range();
$range = reset($ranges);
$this->assertEquals(51, key($ranges));
$upcomingweek = new \core\analytics\time_splitting\upcoming_week();
$upcomingweek->set_analysable($this->analysable);
$this->assertCount(1, $upcomingweek->get_distinct_ranges());
$ranges = $upcomingweek->get_all_ranges();
$this->assertEquals(53, count($ranges));
$this->assertEquals($this->course->startdate, $ranges[0]['start']);
$this->assertEquals($this->course->startdate, $ranges[0]['time']);
$this->assertCount(count($ranges), $upcomingweek->get_training_ranges());
$ranges = $upcomingweek->get_most_recent_prediction_range();
$range = reset($ranges);
$this->assertEquals(52, key($ranges));
// We now use an ongoing course.
$onemonthago = new DateTime('-30 days');
$params = array(
'startdate' => $onemonthago->getTimestamp(),
);
$ongoingcourse = $this->getDataGenerator()->create_course($params);
$ongoinganalysable = new \core_analytics\course($ongoingcourse);
$weekly = new test_timesplitting_weekly();
$weekly->set_analysable($ongoinganalysable);
$this->assertCount(1, $weekly->get_distinct_ranges());
$ranges = $weekly->get_all_ranges();
$this->assertEquals(4, count($ranges));
$this->assertCount(4, $weekly->get_training_ranges());
$ranges = $weekly->get_most_recent_prediction_range();
$range = reset($ranges);
$this->assertEquals(3, key($ranges));
$this->assertLessThan(time(), $range['time']);
$this->assertLessThan(time(), $range['start']);
$this->assertLessThan(time(), $range['end']);
$upcomingweek = new \core\analytics\time_splitting\upcoming_week();
$upcomingweek->set_analysable($ongoinganalysable);
$this->assertCount(1, $upcomingweek->get_distinct_ranges());
$ranges = $upcomingweek->get_all_ranges();
$this->assertEquals(5, count($ranges));
$this->assertCount(4, $upcomingweek->get_training_ranges());
$ranges = $upcomingweek->get_most_recent_prediction_range();
$range = reset($ranges);
$this->assertEquals(4, key($ranges));
$this->assertLessThan(time(), $range['time']);
$this->assertLessThan(time(), $range['start']);
$this->assertGreaterThan(time(), $range['end']);
// We now check how new ranges get added as time passes.
$fewsecsago = new DateTime('-5 seconds');
$params = array(
'startdate' => $fewsecsago->getTimestamp(),
'enddate' => (new DateTimeImmutable('+1 year'))->getTimestamp(),
);
$course = $this->getDataGenerator()->create_course($params);
$analysable = new \core_analytics\course($course);
$seconds = new test_timesplitting_seconds();
$seconds->set_analysable($analysable);
// Store the ranges we just obtained.
$nranges = count($seconds->get_all_ranges());
$ntrainingranges = count($seconds->get_training_ranges());
$mostrecentrange = $seconds->get_most_recent_prediction_range();
$mostrecentrange = reset($mostrecentrange);
// We wait for the next range to be added.
usleep(1000000);
$seconds->set_analysable($analysable);
$nnewranges = $seconds->get_all_ranges();
$nnewtrainingranges = $seconds->get_training_ranges();
$newmostrecentrange = $seconds->get_most_recent_prediction_range();
$newmostrecentrange = reset($newmostrecentrange);
$this->assertGreaterThan($nranges, $nnewranges);
$this->assertGreaterThan($ntrainingranges, $nnewtrainingranges);
$this->assertGreaterThan($mostrecentrange['time'], $newmostrecentrange['time']);
}
}
+1 -1
View File
@@ -59,7 +59,7 @@ function report_insights_extend_navigation_course($navigation, $course, $context
function report_insights_myprofile_navigation(core_user\output\myprofile\tree $tree, $user, $iscurrentuser, $course) {
$context = \context_user::instance($user->id);
if (has_capability('moodle/analytics:listinsights', $context)) {
if (\core_analytics\manager::check_can_list_insights($context, true)) {
$modelids = report_insights_context_insights($context);
if (!empty($modelids)) {
@@ -0,0 +1,156 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Upcoming activities due target.
*
* @package core
* @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_user\analytics\target;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/lib/enrollib.php');
/**
* Upcoming activities due target.
*
* @package core
* @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class upcoming_activities_due extends \core_analytics\local\target\binary {
/**
* Machine learning backends are not required to predict.
*
* @return bool
*/
public static function based_on_assumptions() {
return true;
}
/**
* Returns the name.
*
* If there is a corresponding '_help' string this will be shown as well.
*
* @return \lang_string
*/
public static function get_name() : \lang_string {
return new \lang_string('target:upcomingactivitiesdue');
}
/**
* classes_description
*
* @return string[]
*/
protected static function classes_description() {
return array(
get_string('no'),
get_string('yes'),
);
}
/**
* Returns the predicted classes that will be ignored.
*
* @return array
*/
protected function ignored_predicted_classes() {
// No need to process users without upcoming activities due.
return array(0);
}
/**
* get_analyser_class
*
* @return string
*/
public function get_analyser_class() {
return '\core\analytics\analyser\users';
}
/**
* Discard users without courses.
*
* @param \core_analytics\analysable $analysable
* @param mixed $fortraining
* @return true|string
*/
public function is_valid_analysable(\core_analytics\analysable $analysable, $fortraining = true) {
// We can skip this checking if we want to save db reads.
$courses = enrol_get_all_users_courses($analysable->get_id(), true, 'id');
if (!$courses) {
return get_string('nocourses');
}
return true;
}
/**
* Only process samples which start date is getting close.
*
* @param int $sampleid
* @param \core_analytics\analysable $analysable
* @param bool $fortraining
* @return bool
*/
public function is_valid_sample($sampleid, \core_analytics\analysable $analysable, $fortraining = true) {
return true;
}
/**
* Calculation based on activities due indicator.
*
* @param int $sampleid
* @param \core_analytics\analysable $analysable
* @param int $starttime
* @param int $endtime
* @return float
*/
protected function calculate_sample($sampleid, \core_analytics\analysable $analysable, $starttime = false, $endtime = false) {
$activitiesdueindicator = $this->retrieve('\core_course\analytics\indicator\activities_due', $sampleid);
if ($activitiesdueindicator == \core_course\analytics\indicator\activities_due::get_max_value()) {
return 1;
}
return 0;
}
/**
* Adds a view dashboard action.
*
* @param \core_analytics\prediction $prediction
* @param mixed $includedetailsaction
* @return \core_analytics\prediction_action[]
*/
public function prediction_actions(\core_analytics\prediction $prediction, $includedetailsaction = false) {
global $CFG;
$url = new \moodle_url('/my/index.php');
$pix = new \pix_icon('i/dashboard', get_string('viewdashboard'));
$action = new \core_analytics\prediction_action('viewupcoming', $prediction,
$url, $pix, get_string('viewdashboard'));
$parentactions = parent::prediction_actions($prediction, $includedetailsaction);
return array_merge([$action], $parentactions);
}
}