MDL-58835 analytics: Store prediction actions separately

New event for insights viewed as part of this issue.
This commit is contained in:
David Monllao 2017-08-28 12:36:54 +02:00
parent 32f9550e85
commit 025363d17c
12 changed files with 287 additions and 30 deletions

View File

@ -112,18 +112,42 @@ abstract class base extends \core_analytics\calculable {
* @return \core_analytics\prediction_action[]
*/
public function prediction_actions(\core_analytics\prediction $prediction, $includedetailsaction = false) {
global $PAGE;
$PAGE->requires->js_call_amd('report_insights/actions', 'init');
$actions = array();
$predictionid = $prediction->get_prediction_data()->id;
if ($includedetailsaction) {
$predictionurl = new \moodle_url('/report/insights/prediction.php',
array('id' => $prediction->get_prediction_data()->id));
array('id' => $predictionid));
$actions['predictiondetails'] = new \core_analytics\prediction_action('predictiondetails', $prediction,
$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'));
}
// Flag as not useful.
$notusefulattrs = array(
'data-prediction-id' => $predictionid,
'data-prediction-methodname' => 'report_insights_set_notuseful_prediction'
);
$actions[] = new \core_analytics\prediction_action(\core_analytics\prediction::ACTION_NOT_USEFUL,
$prediction, new \moodle_url(''), new \pix_icon('t/delete', get_string('notuseful', 'analytics')),
get_string('notuseful', 'analytics'), false, $notusefulattrs);
// Flag as fixed / solved.
$fixedattrs = array(
'data-prediction-id' => $predictionid,
'data-prediction-methodname' => 'report_insights_set_fixed_prediction'
);
$actions[] = new \core_analytics\prediction_action(\core_analytics\prediction::ACTION_FIXED,
$prediction, new \moodle_url(''), new \pix_icon('t/check', get_string('fixedack', 'analytics')),
get_string('fixedack', 'analytics'), false, $fixedattrs);
return $actions;
}

View File

@ -1100,8 +1100,8 @@ class model {
* @param int $perpage The max number of results to fetch. Ignored if $page is false.
* @return array($total, \core_analytics\prediction[])
*/
public function get_predictions(\context $context, $page = false, $perpage = 100) {
global $DB;
public function get_predictions(\context $context, $skiphidden = true, $page = false, $perpage = 100) {
global $DB, $USER;
\core_analytics\manager::check_can_list_insights($context);
@ -1111,12 +1111,27 @@ class model {
JOIN (
SELECT sampleid, max(rangeindex) AS rangeindex
FROM {analytics_predictions}
WHERE modelid = ? and contextid = ?
WHERE modelid = :modelidsubap and contextid = :contextidsubap
GROUP BY sampleid
) apsub
ON ap.sampleid = apsub.sampleid AND ap.rangeindex = apsub.rangeindex
WHERE ap.modelid = ? and ap.contextid = ?";
$params = array($this->model->id, $context->id, $this->model->id, $context->id);
WHERE ap.modelid = :modelid and ap.contextid = :contextid";
$params = array('modelid' => $this->model->id, 'contextid' => $context->id,
'modelidsubap' => $this->model->id, 'contextidsubap' => $context->id);
if ($skiphidden) {
$sql .= " AND NOT EXISTS (
SELECT 1
FROM {analytics_prediction_actions} apa
WHERE apa.predictionid = ap.id AND apa.userid = :userid AND (apa.actionname = :fixed OR apa.actionname = :notuseful)
)";
$params['userid'] = $USER->id;
$params['fixed'] = \core_analytics\prediction::ACTION_FIXED;
$params['notuseful'] = \core_analytics\prediction::ACTION_NOT_USEFUL;
}
$sql .= " ORDER BY ap.timecreated DESC";
if (!$predictions = $DB->get_records_sql($sql, $params)) {
return array();
}

View File

@ -35,6 +35,21 @@ defined('MOODLE_INTERNAL') || die();
*/
class prediction {
/**
* Prediction details (one of the default prediction actions)
*/
const ACTION_PREDICTION_DETAILS = 'predictiondetails';
/**
* Prediction not useful (one of the default prediction actions)
*/
const ACTION_NOT_USEFUL = 'notuseful';
/**
* Prediction already fixed (one of the default prediction actions)
*/
const ACTION_FIXED = 'fixed';
/**
* @var \stdClass
*/
@ -97,6 +112,51 @@ class prediction {
return $this->calculations;
}
/**
* Stores the executed action.
* Prediction instances should be retrieved using \core_analytics\manager::get_prediction,
* It is the caller responsability to check that the user can see the prediction.
*
* @param string $actionname
* @param \core_analytics\local\target\base $target
*/
public function action_executed($actionname, \core_analytics\local\target\base $target) {
global $USER, $DB;
$context = \context::instance_by_id($this->get_prediction_data()->contextid, IGNORE_MISSING);
if (!$context) {
throw new \moodle_exception('errorpredictioncontextnotavailable', 'analytics');
}
// Check that the provided action exists.
$actions = $target->prediction_actions($this, true);
foreach ($actions as $action) {
if ($action->get_action_name() === $actionname) {
$found = true;
}
}
if (empty($found)) {
throw new \moodle_exception('errorunknownaction', 'analytics');
}
$predictionid = $this->get_prediction_data()->id;
$action = new \stdClass();
$action->predictionid = $predictionid;
$action->userid = $USER->id;
$action->actionname = $actionname;
$action->timecreated = time();
$DB->insert_record('analytics_prediction_actions', $action);
$eventdata = array (
'context' => $context,
'objectid' => $predictionid,
'other' => array('actionname' => $actionname)
);
\core\event\prediction_action_started::create($eventdata)->trigger();
}
/**
* format_calculations
*

View File

@ -35,36 +35,54 @@ defined('MOODLE_INTERNAL') || die();
*/
class prediction_action {
/**
* @var string
*/
protected $actionname = null;
/**
* @var \action_menu_link
*/
protected $actionlink = null;
/**
* __construct
* Prediction action constructor.
*
* @param string $actionname
* @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 \pix_icon $icon
* @param string $text
* @param bool $primary
* @param \pix_icon $icon Link icon
* @param string $text Link text
* @param bool $primary Primary button or secondary.
* @param array $attributes Link attributes
* @return void
*/
public function __construct($actionname, \core_analytics\prediction $prediction, \moodle_url $actionurl, \pix_icon $icon, $text, $primary = false) {
public function __construct($actionname, \core_analytics\prediction $prediction, \moodle_url $actionurl, \pix_icon $icon,
$text, $primary = false, $attributes = array()) {
$this->actionname = $actionname;
// We want to track how effective are our suggested actions, we pass users through a script that will log these actions.
$params = array('action' => $actionname, 'predictionid' => $prediction->get_prediction_data()->id,
$params = array('action' => $this->actionname, 'predictionid' => $prediction->get_prediction_data()->id,
'forwardurl' => $actionurl->out(false));
$url = new \moodle_url('/report/insights/action.php', $params);
if ($primary === false) {
$this->actionlink = new \action_menu_link_secondary($url, $icon, $text);
$this->actionlink = new \action_menu_link_secondary($url, $icon, $text, $attributes);
} else {
$this->actionlink = new \action_menu_link_primary($url, $icon, $text);
$this->actionlink = new \action_menu_link_primary($url, $icon, $text, $attributes);
}
}
/**
* Returns the action name.
*
* @return string
*/
public function get_action_name() {
return $this->actionname;
}
/**
* Returns the link to the action.
*

View File

@ -53,6 +53,8 @@ $string['errorunexistingtimesplitting'] = 'The selected time-splitting method is
$string['errorunexistingmodel'] = 'Non-existing model {$a}';
$string['errorunknownaction'] = 'Unknown action';
$string['eventpredictionactionstarted'] = 'Prediction process started';
$string['eventinsightsviewed'] = 'Insights viewed';
$string['fixedack'] = 'Acknowledged / fixed';
$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>.';
@ -71,6 +73,7 @@ $string['nonewtimeranges'] = 'No new time ranges; nothing to predict.';
$string['nopredictionsyet'] = 'No predictions available yet';
$string['noranges'] = 'No predictions yet';
$string['notrainingbasedassumptions'] = 'Models based on assumptions do not need training';
$string['notuseful'] = 'Not useful';
$string['novaliddata'] = 'No valid data available';
$string['novalidsamples'] = 'No valid samples available';
$string['onlycli'] = 'Analytics processes execution via command line only';

View File

@ -61,25 +61,27 @@ class course_dropout extends \core_analytics\local\target\binary {
public function prediction_actions(\core_analytics\prediction $prediction, $includedetailsaction = false) {
global $USER;
$actions = parent::prediction_actions($prediction, $includedetailsaction);
$actions = array();
$sampledata = $prediction->get_sample_data();
$studentid = $sampledata['user']->id;
$attrs = array('target' => '_blank');
// Send a message.
$url = new \moodle_url('/message/index.php', array('user' => $USER->id, 'id' => $studentid));
$pix = new \pix_icon('t/message', get_string('sendmessage', 'message'));
$actions['studentmessage'] = new \core_analytics\prediction_action('studentmessage', $prediction, $url, $pix,
get_string('sendmessage', 'message'));
$actions[] = new \core_analytics\prediction_action('studentmessage', $prediction, $url, $pix,
get_string('sendmessage', 'message'), $attrs);
// View outline report.
$url = new \moodle_url('/report/outline/user.php', array('id' => $studentid, 'course' => $sampledata['course']->id,
'mode' => 'outline'));
$pix = new \pix_icon('i/report', get_string('outlinereport'));
$actions['viewoutlinereport'] = new \core_analytics\prediction_action('viewoutlinereport', $prediction, $url, $pix,
get_string('outlinereport'));
$actions[] = new \core_analytics\prediction_action('viewoutlinereport', $prediction, $url, $pix,
get_string('outlinereport'), $attrs);
return $actions;
return array_merge($actions, parent::prediction_actions($prediction, $includedetailsaction));
}
/**

View File

@ -64,25 +64,28 @@ class no_teaching extends \core_analytics\local\target\binary {
*/
public function prediction_actions(\core_analytics\prediction $prediction, $includedetailsaction = false) {
// No need to call the parent as the parent's action is view details and this target only have 1 feature.
$actions = array();
$sampledata = $prediction->get_sample_data();
$course = $sampledata['course'];
$actions = array();
$url = new \moodle_url('/course/view.php', array('id' => $course->id));
$pix = new \pix_icon('i/course', get_string('course'));
$actions['viewcourse'] = new \core_analytics\prediction_action('viewcourse', $prediction,
$actions[] = new \core_analytics\prediction_action('viewcourse', $prediction,
$url, $pix, get_string('view'));
if (has_any_capability(['moodle/course:viewparticipants', 'moodle/course:enrolreview'], $sampledata['context'])) {
$url = new \moodle_url('/user/index.php', array('id' => $course->id));
$pix = new \pix_icon('i/cohort', get_string('participants'));
$actions['viewparticipants'] = new \core_analytics\prediction_action('viewparticipants', $prediction,
$actions[] = new \core_analytics\prediction_action('viewparticipants', $prediction,
$url, $pix, get_string('participants'));
}
return $actions;
$parentactions = parent::prediction_actions($prediction, $includedetailsaction);
// No need to show details as there is only 1 indicator.
unset($parentactions[\core_analytics\prediction::ACTION_PREDICTION_DETAILS]);
return array_merge($actions, $parentactions);
}
/**

View File

@ -0,0 +1,79 @@
<?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/>.
/**
* Insights page viewed event.
*
* @property-read array $other {
* Extra information about event.
*
* - string modelid: The model id
* }
*
* @package core_analytics
* @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\event;
defined('MOODLE_INTERNAL') || die();
/**
* Event triggered after a user views the insights page.
*
* @package core_analytics
* @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class insights_viewed extends \core\event\base {
/**
* Set basic properties for the event.
*/
protected function init() {
$this->data['crud'] = 'r';
// It depends on the insight really.
$this->data['edulevel'] = self::LEVEL_OTHER;
}
/**
* Returns localised general event name.
*
* @return string
*/
public static function get_name() {
return get_string('eventinsightsviewed', 'analytics');
}
/**
* Returns non-localised event description with id's for admin use only.
*
* @return string
*/
public function get_description() {
return "The user with id '$this->userid' has viewed model '{$this->other['modelid']}' insights in " .
"context with id '{$this->data['contextid']}'";
}
/**
* Returns relevant URL.
* @return \moodle_url
*/
public function get_url() {
return new \moodle_url('/report/insights/insights.php', array('modelid' => $this->other['modelid'],
'contextid' => $this->data['contextid']));
}
}

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="lib/db" VERSION="20170814" COMMENT="XMLDB file for core Moodle tables"
<XMLDB PATH="lib/db" VERSION="20170904" COMMENT="XMLDB file for core Moodle tables"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
>
@ -3699,5 +3699,22 @@
<INDEX NAME="starttime-endtime-contextid" UNIQUE="false" FIELDS="starttime, endtime, contextid"/>
</INDEXES>
</TABLE>
<TABLE NAME="analytics_prediction_actions" COMMENT="Register of user actions over predictions.">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="predictionid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="actionname" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="predictionid" TYPE="foreign" FIELDS="predictionid" REFTABLE="analytics_predictions" REFFIELDS="id"/>
<KEY NAME="userid" TYPE="foreign" FIELDS="userid" REFTABLE="user" REFFIELDS="id"/>
</KEYS>
<INDEXES>
<INDEX NAME="predictionidanduseridandactionname" UNIQUE="false" FIELDS="predictionid, userid, actionname"/>
</INDEXES>
</TABLE>
</TABLES>
</XMLDB>

View File

@ -2436,5 +2436,35 @@ function xmldb_main_upgrade($oldversion) {
upgrade_main_savepoint(true, 2017082800.00);
}
if ($oldversion < 2017090700.01) {
// Define table analytics_prediction_actions to be created.
$table = new xmldb_table('analytics_prediction_actions');
// Adding fields to table analytics_prediction_actions.
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
$table->add_field('predictionid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
$table->add_field('userid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
$table->add_field('actionname', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null);
$table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
// Adding keys to table analytics_prediction_actions.
$table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
$table->add_key('predictionid', XMLDB_KEY_FOREIGN, array('predictionid'), 'analytics_predictions', array('id'));
$table->add_key('userid', XMLDB_KEY_FOREIGN, array('userid'), 'user', array('id'));
// Adding indexes to table analytics_prediction_actions.
$table->add_index('predictionidanduseridandactionname', XMLDB_INDEX_NOTUNIQUE,
array('predictionid', 'userid', 'actionname'));
// Conditionally launch create table for analytics_prediction_actions.
if (!$dbman->table_exists($table)) {
$dbman->create_table($table);
}
// Main savepoint reached.
upgrade_main_savepoint(true, 2017090700.01);
}
return true;
}

View File

@ -111,4 +111,10 @@ echo $OUTPUT->header();
$renderable = new \report_insights\output\insights_list($model, $context, $othermodels, $page, $perpage);
echo $renderer->render($renderable);
$eventdata = array (
'context' => $context,
'other' => array('modelid' => $model->get_id())
);
\core\event\insights_viewed::create($eventdata)->trigger();
echo $OUTPUT->footer();

View File

@ -29,7 +29,7 @@
defined('MOODLE_INTERNAL') || die();
$version = 2017090700.00; // YYYYMMDD = weekly release date of this DEV branch.
$version = 2017090700.01; // YYYYMMDD = weekly release date of this DEV branch.
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.