MDL-64783 analytics: Upcoming activities due and related API changes
This commit is contained in:
committed by
Eloy Lafuente (stronk7)
parent
78e77fab2e
commit
50935afba4
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
*
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -2469,4 +2469,12 @@ $capabilities = array(
|
||||
'user' => CAP_ALLOW,
|
||||
],
|
||||
],
|
||||
|
||||
'moodle/analytics:listowninsights' => array(
|
||||
'captype' => 'read',
|
||||
'contextlevel' => CONTEXT_SYSTEM,
|
||||
'archetypes' => array(
|
||||
'user' => CAP_ALLOW
|
||||
)
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user