MDL-64783 analytics: Improvements to the insights system

This commit is contained in:
David Monllaó
2019-04-09 00:26:23 +02:00
committed by Eloy Lafuente (stronk7)
parent 50935afba4
commit e85c2f372b
21 changed files with 197 additions and 76 deletions
+44 -13
View File
@@ -122,12 +122,12 @@ abstract class base extends \core_analytics\calculable {
if ($includedetailsaction) {
$predictionurl = new \moodle_url('/report/insights/prediction.php',
array('id' => $predictionid));
$predictionurl = new \moodle_url('/report/insights/prediction.php', array('id' => $predictionid));
$detailstext = $this->get_view_details_text();
$actions[] = new \core_analytics\prediction_action(\core_analytics\prediction::ACTION_PREDICTION_DETAILS, $prediction,
$predictionurl, new \pix_icon('t/preview', get_string('viewprediction', 'analytics')),
get_string('viewprediction', 'analytics'));
$predictionurl, new \pix_icon('t/preview', $detailstext),
$detailstext);
}
// Flag as fixed / solved.
@@ -151,6 +151,25 @@ abstract class base extends \core_analytics\calculable {
return $actions;
}
/**
* Returns the view details link text.
* @return string
*/
private function get_view_details_text() {
if ($this->based_on_assumptions()) {
$analyserclass = $this->get_analyser_class();
if ($analyserclass::one_sample_per_analysable()) {
$detailstext = get_string('viewinsightdetails', 'analytics');
} else {
$detailstext = get_string('viewdetails', 'analytics');
}
} else {
$detailstext = get_string('viewprediction', 'analytics');
}
return $detailstext;
}
/**
* Callback to execute once a prediction has been returned from the predictions processor.
*
@@ -190,23 +209,34 @@ abstract class base extends \core_analytics\calculable {
$coursecontext = \context_course::instance(SITEID);
}
$predictionactions = $this->prediction_actions($prediction, false);
$predictionactions = $this->prediction_actions($prediction, true);
// TODO Proper language strings.
$fullmessage = '';
$fullmessagehtml = '';
$messageactions = '';
$messageactionshtml = '';
$insighturl = null;
foreach ($predictionactions as $action) {
$actionurl = $action->get_url();
if (!$actionurl->get_param('forwardurl')) {
$actiondoneurl = new \moodle_url('/report/insights/done.php');
// Set the forward url to the 'done' script.
$actionurl->param('forwardurl', $actiondoneurl->out(false));
}
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();
$insighturl = $action->get_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();
$messageactions .= $action->get_text() . ': ' . $action->get_url()->out(false) . PHP_EOL;
$messageactionshtml .= '<a href="' . $action->get_url()->out() . '" class="btn btn-default m-r-1 m-b-1">' .
$action->get_text() . '</a>';
}
$fullmessage = get_string('insightinfomessageprediction', 'analytics', $messageactions);
$fullmessagehtml = get_string('insightinfomessagepredictionhtml', 'analytics', $messageactionshtml);
foreach ($users as $user) {
$this->generate_insight_notification($user, $subject, $insighturl, $coursecontext, $fullmessage, $fullmessagehtml);
$this->generate_insight_notification($user, $subject, $insighturl, $coursecontext,
$fullmessage, $fullmessagehtml);
}
}
@@ -225,7 +255,8 @@ abstract class base extends \core_analytics\calculable {
$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);
$this->generate_insight_notification($user, $subject, $insighturl, $coursecontext,
$fullmessage, $fullmessagehtml);
}
}
}
+1 -1
View File
@@ -60,7 +60,7 @@ abstract class binary extends discrete {
*
* @return array
*/
protected function ignored_predicted_classes() {
public function ignored_predicted_classes() {
// Zero-value class is usually ignored in binary classifiers.
return array(0);
}
+1 -1
View File
@@ -145,7 +145,7 @@ abstract class discrete extends base {
*
* @return array List of values that will be ignored (array keys are ignored).
*/
protected function ignored_predicted_classes() {
public function ignored_predicted_classes() {
// Coding exception as this will only be called if this target have non-linear values.
throw new \coding_exception('Overwrite ignored_predicted_classes() and return an array with the classes that should not ' .
'trigger the callback');
+1 -1
View File
@@ -916,7 +916,7 @@ class model {
return $prediction;
}, $predictionrecords);
} else {
$predictions = false;
$predictions = [];
}
$this->get_target()->generate_insight_notifications($this->model->id, $samplecontexts, $predictions);
+7 -7
View File
@@ -43,7 +43,7 @@ class prediction_action {
/**
* @var \moodle_url
*/
protected $actionurl = null;
protected $url = null;
/**
* @var \action_menu_link
@@ -55,7 +55,7 @@ class prediction_action {
*
* @param string $actionname They should match a-zA-Z_0-9-, as we apply a PARAM_ALPHANUMEXT filter
* @param \core_analytics\prediction $prediction
* @param \moodle_url $actionurl
* @param \moodle_url $actionurl The final URL where the user should be forwarded.
* @param \pix_icon $icon Link icon
* @param string $text Link text
* @param bool $primary Primary button or secondary.
@@ -71,12 +71,12 @@ class prediction_action {
// 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));
$this->actionurl = new \moodle_url('/report/insights/action.php', $params);
$this->url = new \moodle_url('/report/insights/action.php', $params);
if ($primary === false) {
$this->actionlink = new \action_menu_link_secondary($this->actionurl, $icon, $this->text, $attributes);
$this->actionlink = new \action_menu_link_secondary($this->url, $icon, $this->text, $attributes);
} else {
$this->actionlink = new \action_menu_link_primary($this->actionurl, $icon, $this->text, $attributes);
$this->actionlink = new \action_menu_link_primary($this->url, $icon, $this->text, $attributes);
}
}
@@ -94,8 +94,8 @@ class prediction_action {
*
* @return \moodle_url
*/
public function get_action_url() {
return $this->actionurl;
public function get_url() {
return $this->url;
}
/**
+1 -1
View File
@@ -87,7 +87,7 @@ class test_target_shortname extends \core_analytics\local\target\binary {
* We don't want to discard results.
* @return array
*/
protected function ignored_predicted_classes() {
public function ignored_predicted_classes() {
return array();
}
+1 -1
View File
@@ -89,7 +89,7 @@ class test_target_site_users extends \core_analytics\local\target\binary {
* We don't want to discard results.
* @return array
*/
protected function ignored_predicted_classes() {
public function ignored_predicted_classes() {
return array();
}
@@ -34,6 +34,7 @@ defined('MOODLE_INTERNAL') || die();
class test_timesplitting_seconds extends \core_analytics\local\time_splitting\periodic {
/**
* Every second.
* @return \DateInterval
*/
public function periodicity() {
+1
View File
@@ -27,6 +27,7 @@ information provided here is intended especially for developers.
* 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.
* The visibility of target's ignored_predicted_classes method must now be public.
=== 3.5 ===
+9 -4
View File
@@ -68,10 +68,12 @@ $string['eventpredictionactionstarted'] = 'Prediction process started';
$string['eventinsightsviewed'] = 'Insights viewed';
$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['insightinfomessage'] = 'The system generated an insight for you: {$a}';
$string['insightinfomessagehtml'] = 'The system generated an insight for you.<br/><br/><a class="btn btn-primary" href="{$a}">View insight</a>';
$string['insightinfomessageprediction'] = 'Some suggested actions you could perform:
{$a}';
$string['insightinfomessagepredictionhtml'] = 'Some suggested actions you could perform:<br/><br/>{$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';
@@ -126,4 +128,7 @@ $string['successfullyanalysed'] = 'Successfully analysed';
$string['timesplittingmethod'] = 'Time-splitting method';
$string['timesplittingmethod_help'] = 'The time-splitting method is what defines when the system will calculate predictions and the portion of activity logs that will be considered for those predictions. E.g. they can divide the course duration in parts and generate a prediction at the end of these parts.';
$string['timesplittingmethod_link'] = 'Time_splitting_methods';
$string['viewdetails'] = 'View details';
$string['viewinsight'] = 'View insight';
$string['viewinsightdetails'] = 'View insight details';
$string['viewprediction'] = 'View prediction details';
+4 -1
View File
@@ -886,6 +886,7 @@ $string['general'] = 'General';
$string['geolocation'] = 'latitude - longitude';
$string['gettheselogs'] = 'Get these logs';
$string['go'] = 'Go';
$string['gotodashboard'] = 'Go to Dashboard';
$string['gpl'] = 'Copyright (C) 1999 onwards Martin Dougiamas (http://moodle.com)
This program is free software; you can redistribute it and/or modify
@@ -1430,6 +1431,7 @@ $string['nosuchemail'] = 'No such email address';
$string['notavailable'] = 'Not available';
$string['notavailablecourse'] = '{$a} is not available';
$string['noteachersyet'] = 'No teachers in this course yet';
$string['noteachingupcomingcourses'] = 'Upcoming courses have no teachers or students';
$string['notenrolled'] = '{$a} is not enrolled in this course.';
$string['notenrolledprofile'] = 'This profile is not available because this user is not enrolled in this course.';
$string['noteusercannotrolldatesoncontext'] = '<strong>Note:</strong> The ability to roll dates when restoring this backup has been disabled because you lack the required permissions.';
@@ -1949,6 +1951,7 @@ $string['stringsnotset'] = 'The following strings are not defined in {$a}';
$string['studentnotallowed'] = 'Sorry, but you can not enter this course as \'{$a}\'';
$string['students'] = 'Students';
$string['studentsandteachers'] = 'Students and teachers';
$string['studentsatriskincourse'] = 'Students at risk of dropping out in {$a} course';
$string['subcategories'] = 'Subcategories';
$string['subcategory'] = 'Subcategory';
$string['subcategoryof'] = 'Subcategory of {$a}';
@@ -2147,7 +2150,6 @@ $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';
@@ -2214,6 +2216,7 @@ $string['years'] = 'years';
$string['yes'] = 'Yes';
$string['youareabouttocreatezip'] = 'You are about to create a zip file containing';
$string['youaregoingtorestorefrom'] = 'You are about to start the restore process for';
$string['youhaveupcomingactivitiesdue'] = 'You have upcoming activities due';
$string['youneedtoenrol'] = 'To perform that action you need to enrol in this course.';
$string['yourlastlogin'] = 'Your last login was';
$string['yourself'] = 'yourself';
@@ -63,17 +63,6 @@ class course_dropout extends \core\analytics\target\course_enrolments {
);
}
/**
* Returns the predicted classes that will be ignored.
*
* Overwriten because we are also interested in knowing when the student is far from the risk of dropping out.
*
* @return array
*/
protected function ignored_predicted_classes() {
return array();
}
/**
* Discards courses that are not yet ready to be used for training or prediction.
*
+11 -10
View File
@@ -55,6 +55,17 @@ class no_teaching extends \core_analytics\local\target\binary {
return new \lang_string('target:noteachingactivity');
}
/**
* Overwritten to show a simpler language string.
*
* @param int $modelid
* @param \context $context
* @return string
*/
public function get_insight_subject(int $modelid, \context $context) {
return get_string('noteachingupcomingcourses');
}
/**
* prediction_actions
*
@@ -103,16 +114,6 @@ class no_teaching extends \core_analytics\local\target\binary {
);
}
/**
* Returns the predicted classes that will be ignored.
*
* @return array
*/
protected function ignored_predicted_classes() {
// No need to list the course if there is teaching activity.
return array(0);
}
/**
* get_analyser_class
*
+16 -4
View File
@@ -74,8 +74,20 @@ class insight implements \renderable, \templatable {
// Get the prediction data.
$predictiondata = $this->prediction->get_prediction_data();
$target = $this->model->get_target();
$data = new \stdClass();
$data->insightname = format_string($this->model->get_target()->get_name());
$data->insightname = format_string($target->get_name());
$data->showpredictionheading = true;
if (!$target->is_linear()) {
$nclasses = count($target::get_classes());
$nignoredclasses = count($target->ignored_predicted_classes());
if ($nclasses - $nignoredclasses <= 1) {
// Hide the prediction heading if there is only 1 class displayed. Otherwise it is redundant with the insight name.
$data->showpredictionheading = false;
}
}
// Get the details.
$data->timecreated = userdate($predictiondata->timecreated);
@@ -99,11 +111,11 @@ class insight implements \renderable, \templatable {
// Prediction info.
$predictedvalue = $predictiondata->prediction;
$predictionid = $predictiondata->id;
$data->predictiondisplayvalue = $this->model->get_target()->get_display_value($predictedvalue);
list($data->style, $data->outcomeicon) = self::get_calculation_display($this->model->get_target(),
$data->predictiondisplayvalue = $target->get_display_value($predictedvalue);
list($data->style, $data->outcomeicon) = self::get_calculation_display($target,
floatval($predictedvalue), $output);
$actions = $this->model->get_target()->prediction_actions($this->prediction, $this->includedetailsaction);
$actions = $target->prediction_actions($this->prediction, $this->includedetailsaction);
if ($actions) {
$actionsmenu = new \action_menu();
$actionsmenu->set_menu_trigger(get_string('actions'));
@@ -87,8 +87,20 @@ class insights_list implements \renderable, \templatable {
public function export_for_template(\renderer_base $output) {
global $PAGE;
$target = $this->model->get_target();
$data = new \stdClass();
$data->insightname = format_string($this->model->get_target()->get_name());
$data->insightname = format_string($target->get_name());
$data->showpredictionheading = true;
if (!$target->is_linear()) {
$nclasses = count($target::get_classes());
$nignoredclasses = count($target->ignored_predicted_classes());
if ($nclasses - $nignoredclasses <= 1) {
// Hide the prediction heading if there is only 1 class displayed. Otherwise it is redundant with the insight name.
$data->showpredictionheading = false;
}
}
$total = 0;
@@ -112,9 +124,9 @@ class insights_list implements \renderable, \templatable {
// Only need to fill this data once.
if (!isset($predictionvalues[$predictedvalue])) {
$preddata = array();
$preddata['predictiondisplayvalue'] = $this->model->get_target()->get_display_value($predictedvalue);
$preddata['predictiondisplayvalue'] = $target->get_display_value($predictedvalue);
list($preddata['style'], $preddata['outcomeicon']) =
insight::get_calculation_display($this->model->get_target(), floatval($predictedvalue), $output);
insight::get_calculation_display($target, floatval($predictedvalue), $output);
$predictionvalues[$predictedvalue] = $preddata;
}
@@ -123,7 +135,7 @@ class insights_list implements \renderable, \templatable {
}
// Order predicted values.
if ($this->model->get_target()->is_linear()) {
if ($target->is_linear()) {
// During regression what we will be interested on most of the time is in low values so let's show them first.
ksort($predictionvalues);
} else {
+30
View File
@@ -0,0 +1,30 @@
<?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/>.
/**
* Forwards the user to the action they selected.
*
* @package report_insights
* @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__ . '/../../config.php');
require_login();
$url = new \moodle_url('/');
redirect($url, get_string('actionsaved', 'report_insights'), null, \core\output\notification::NOTIFY_SUCCESS);
+16
View File
@@ -106,6 +106,22 @@ if (!$model->uses_insights()) {
$PAGE->set_title($insightinfo->insightname);
$PAGE->set_heading($insightinfo->contextname);
// Some models generate one single prediction per context. We can directly show the prediction details in this case.
if ($model->get_analyser()::one_sample_per_analysable()) {
// Param $perpage to 2 so we can detect if this model's analyser is using one_sample_per_analysable incorrectly.
$predictionsdata = $model->get_predictions($context, true, false, 2);
if ($predictionsdata) {
list($total, $predictions) = $predictionsdata;
if ($total > 1) {
throw new \coding_exception('This model\'s analyser processed more than one sample for a single analysable element.' .
'Therefore, the analyser\'s one_sample_per_analysable() method should return false.');
}
$prediction = reset($predictions);
$redirecturl = new \moodle_url('/report/insights/prediction.php', ['id' => $prediction->get_prediction_data()->id]);
redirect($redirecturl);
}
}
echo $OUTPUT->header();
$renderable = new \report_insights\output\insights_list($model, $context, $othermodels, $page, $perpage);
@@ -22,6 +22,7 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['actionsaved'] = 'Your action has been saved.';
$string['calculatedvalue'] = 'Calculated value';
$string['disabledmodel'] = 'Sorry, this model has been disabled by the administrator';
$string['indicator'] = 'Indicator';
@@ -75,7 +75,8 @@
<h2 class="mb-2">{{#str}}insightprediction, report_insights, {{insightname}} {{/str}}</h2>
<table class="generaltable insights-list">
<caption>
{{#showpredicionheading}}
<caption>
{{#str}}prediction, report_insights{{/str}}:
<span class="{{#style}}table-{{style}}{{/style}}">
{{#outcomeicon}}
@@ -84,6 +85,7 @@
{{predictiondisplayvalue}}
</span>
</caption>
{{/showpredicionheading}}
<thead>
<tr>
<th scope="col">{{#str}}name{{/str}}</th>
@@ -87,15 +87,17 @@
{{{ pagingbar }}}
{{#predictions}}
<table class="generaltable insights-list">
<caption>
{{#str}}prediction, report_insights{{/str}}:
<span class="{{#style}}table-{{style}}{{/style}}">
{{#outcomeicon}}
{{> core/pix_icon}}
{{/outcomeicon}}
{{predictiondisplayvalue}}
</span>
</caption>
{{#showpredicionheading}}
<caption>
{{#str}}prediction, report_insights{{/str}}:
<span class="{{#style}}table-{{style}}{{/style}}">
{{#outcomeicon}}
{{> core/pix_icon}}
{{/outcomeicon}}
{{predictiondisplayvalue}}
</span>
</caption>
{{/showpredicionheading}}
<thead>
<tr>
<th scope="col">{{#str}}name{{/str}}</th>
@@ -57,6 +57,17 @@ class upcoming_activities_due extends \core_analytics\local\target\binary {
return new \lang_string('target:upcomingactivitiesdue');
}
/**
* Overwritten to show a simpler language string.
*
* @param int $modelid
* @param \context $context
* @return string
*/
protected function get_insight_subject(int $modelid, \context $context) {
return get_string('youhaveupcomingactivitiesdue');
}
/**
* classes_description
*
@@ -74,7 +85,7 @@ class upcoming_activities_due extends \core_analytics\local\target\binary {
*
* @return array
*/
protected function ignored_predicted_classes() {
public function ignored_predicted_classes() {
// No need to process users without upcoming activities due.
return array(0);
}
@@ -142,15 +153,19 @@ class upcoming_activities_due extends \core_analytics\local\target\binary {
* @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'));
global $CFG, $USER;
$parentactions = parent::prediction_actions($prediction, $includedetailsaction);
if ($USER->id != $prediction->get_prediction_data()->sampleid) {
return $parentactions;
}
$url = new \moodle_url('/my/index.php');
$pix = new \pix_icon('i/dashboard', get_string('gotodashboard'));
$action = new \core_analytics\prediction_action('viewupcoming', $prediction,
$url, $pix, get_string('gotodashboard'));
return array_merge([$action], $parentactions);
}
}