Merge branch 'MDL-62191_master' of git://github.com/dmonllao/moodle

This commit is contained in:
Jun Pataleta
2019-10-02 16:35:50 +08:00
52 changed files with 1731 additions and 302 deletions
@@ -98,20 +98,15 @@ class effectiveness_report implements \renderable, \templatable {
// Using this unusual execution flow to init the chart data because $predictionactionrecords
// is a \moodle_recordset.
if (empty($actionlabels)) {
list($actionlabels, $actionvalues) = $this->init_action_labels($record);
list($actionlabels, $actionvalues, $actiontypes) = $this->init_action_labels($record);
}
// One value for each action.
$actionvalues['separated'][$record->actionname]++;
// Data grouped in three boxes.
if ($record->actionname == 'notuseful') {
$actionvalues['grouped']['negative']++;
} else if ($record->actionname == 'predictiondetails') {
$actionvalues['grouped']['neutral']++;
} else {
$actionvalues['grouped']['positive']++;
}
// Grouped value.
$actiontype = $actiontypes[$record->actionname];
$actionvalues['grouped'][$actiontype]++;
}
$predictionactionrecords->close();
@@ -162,18 +157,28 @@ class effectiveness_report implements \renderable, \templatable {
$actionlabels = [];
$actionvalues = ['separated' => [], 'grouped' => []];
$actiontypes = [];
foreach ($predictionactions as $action) {
$actionlabels['separated'][$action->get_action_name()] = $action->get_text();
$actionvalues['separated'][$action->get_action_name()] = 0;
$actiontypes[$action->get_action_name()] = $action->get_type();
}
$actionlabels['grouped']['positive'] = get_string('useful', 'analytics');
$actionlabels['grouped']['neutral'] = get_string('neutral', 'analytics');
$actionlabels['grouped']['negative'] = get_string('notuseful', 'analytics');
$actionvalues['grouped']['positive'] = 0;
$actionvalues['grouped']['neutral'] = 0;
$actionvalues['grouped']['negative'] = 0;
$bulkactions = $this->model->get_target()->bulk_actions($predictions);
foreach ($bulkactions as $action) {
$actionlabels['separated'][$action->get_action_name()] = $action->get_text();
$actionvalues['separated'][$action->get_action_name()] = 0;
$actiontypes[$action->get_action_name()] = $action->get_type();
}
return [$actionlabels, $actionvalues];
$actionlabels['grouped'][\core_analytics\action::TYPE_POSITIVE] = get_string('useful', 'analytics');
$actionlabels['grouped'][\core_analytics\action::TYPE_NEUTRAL] = get_string('neutral', 'analytics');
$actionlabels['grouped'][\core_analytics\action::TYPE_NEGATIVE] = get_string('notuseful', 'analytics');
$actionvalues['grouped'][\core_analytics\action::TYPE_POSITIVE] = 0;
$actionvalues['grouped'][\core_analytics\action::TYPE_NEUTRAL] = 0;
$actionvalues['grouped'][\core_analytics\action::TYPE_NEGATIVE] = 0;
return [$actionlabels, $actionvalues, $actiontypes];
}
}
+137
View File
@@ -0,0 +1,137 @@
<?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/>.
/**
* Representation of a suggested action.
*
* @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();
/**
* Representation of a suggested action.
*
* @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 action {
/**
* @var Action type useful.
*/
const TYPE_POSITIVE = 'useful';
/**
* @var Action type notuseful.
*/
const TYPE_NEGATIVE = 'notuseful';
/**
* @var Action type neutral.
*/
const TYPE_NEUTRAL = 'neutral';
/**
* @var string
*/
protected $actionname = null;
/**
* @var \moodle_url
*/
protected $url = null;
/**
* @var \renderable
*/
protected $actionlink = null;
/**
* @var string
*/
protected $text = null;
/**
* Returns the action name.
*
* @return string
*/
public function get_action_name() {
return $this->actionname;
}
/**
* Returns the url to the action.
*
* @return \moodle_url
*/
public function get_url() {
return $this->url;
}
/**
* Returns the link to the action.
*
* @return \renderable
*/
public function get_action_link() {
return $this->actionlink;
}
/**
* Returns the action text.
* @return string
*/
public function get_text() {
return $this->text;
}
/**
* Sets the type of the action according to its positiveness.
*
* @throws \coding_exception
* @param string|false $type \core_analytics\action::TYPE_POSITIVE, TYPE_NEGATIVE or TYPE_NEUTRAL
*/
public function set_type($type = false) {
if (!$type) {
// Any non-standard action specified by a target is considered positive by default because that is what
// they are meant to be.
$type = self::TYPE_POSITIVE;
}
if ($type !== self::TYPE_POSITIVE && $type !== self::TYPE_NEUTRAL &&
$type !== self::TYPE_NEGATIVE) {
throw new \coding_exception('The provided type must be ' . self::TYPE_POSITIVE . ', ' . self::TYPE_NEUTRAL .
' or ' . self::TYPE_NEGATIVE);
}
$this->type = $type;
}
/**
* Returns the type of action.
*
* @return string The positiveness of the action (self::TYPE_POSITIVE, self::TYPE_NEGATIVE or self::TYPE_NEUTRAL)
*/
public function get_type() {
return $this->type;
}
}
+65
View File
@@ -0,0 +1,65 @@
<?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/>.
/**
* Representation of a suggested bulk action.
*
* @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();
/**
* Representation of a suggested bulk action.
*
* @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 bulk_action extends action {
/**
* Prediction action constructor.
*
* @param string $actionname They should match a-zA-Z_0-9-, as we apply a PARAM_ALPHANUMEXT filter
* @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.
* @param array $attributes Link attributes
* @param string|false $type
* @return void
*/
public function __construct($actionname, \moodle_url $actionurl, \pix_icon $icon,
$text, $primary = false, $attributes = array(), $type = false) {
global $OUTPUT;
$this->actionname = $actionname;
$this->text = $text;
$this->set_type($type);
// 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, 'forwardurl' => $actionurl->out(false));
$this->url = new \moodle_url('/report/insights/action.php', $params);
$label = $OUTPUT->render($icon) . $this->text;
$this->actionlink = new \single_button($this->url, $label, 'get', $primary, $attributes);
}
}
+126
View File
@@ -0,0 +1,126 @@
<?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/>.
/**
* Default list of bulk actions to reuse across different targets as presets.
*
* @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();
/**
* Default list of bulk actions to reuse across different targets as presets.
*
* @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 default_bulk_actions {
/**
* Accepted prediction.
*
* @return \core_analytics\bulk_action
*/
public static function accept() {
$attrs = [
'data-bulk-actionname' => prediction::ACTION_FIXED
] + self::bulk_action_base_attrs();
return new bulk_action(prediction::ACTION_FIXED,
new \moodle_url(''), new \pix_icon('t/check', get_string('fixedack', 'analytics')),
get_string('fixedack', 'analytics'), false, $attrs, action::TYPE_POSITIVE);
}
/**
* The prediction is not applicable for this same (e.g. This student was unenrolled in the uni SIS).
*
* @return \core_analytics\bulk_action
*/
public static function not_applicable() {
$attrs = [
'data-bulk-actionname' => prediction::ACTION_NOT_APPLICABLE
] + self::bulk_action_base_attrs();
return new bulk_action(prediction::ACTION_NOT_APPLICABLE,
new \moodle_url(''), new \pix_icon('fp/cross', get_string('notapplicable', 'analytics'), 'theme'),
get_string('notapplicable', 'analytics'), false, $attrs, action::TYPE_NEUTRAL);
}
/**
* Incorrectly flagged prediction, useful for models based on data.
*
* @return \core_analytics\bulk_action
*/
public static function incorrectly_flagged() {
$attrs = [
'data-bulk-actionname' => prediction::ACTION_INCORRECTLY_FLAGGED
] + self::bulk_action_base_attrs();
return new bulk_action(prediction::ACTION_INCORRECTLY_FLAGGED,
new \moodle_url(''), new \pix_icon('i/incorrect', get_string('incorrectlyflagged', 'analytics')),
get_string('incorrectlyflagged', 'analytics'), false, $attrs, action::TYPE_NEGATIVE);
}
/**
* Useful prediction.
*
* @return \core_analytics\bulk_action
*/
public static function useful() {
$attrs = [
'data-bulk-actionname' => prediction::ACTION_USEFUL
] + self::bulk_action_base_attrs();
return new bulk_action(prediction::ACTION_USEFUL,
new \moodle_url(''), new \pix_icon('t/check', get_string('useful', 'analytics')),
get_string('useful', 'analytics'), false, $attrs, action::TYPE_POSITIVE);
}
/**
* Not useful prediction.
*
* @return \core_analytics\bulk_action
*/
public static function not_useful() {
$attrs = [
'data-bulk-actionname' => prediction::ACTION_NOT_USEFUL
] + self::bulk_action_base_attrs();
return new bulk_action(prediction::ACTION_NOT_USEFUL,
new \moodle_url(''), new \pix_icon('t/delete', get_string('notuseful', 'analytics')),
get_string('notuseful', 'analytics'), false, $attrs, action::TYPE_NEGATIVE);
}
/**
* Common attributes for all the action renderables.
*
* @return array
*/
private static final function bulk_action_base_attrs() {
return [
'disabled' => 'disabled',
'data-toggle' => 'action',
'data-action' => 'toggle',
];
}
}
+18 -9
View File
@@ -187,8 +187,11 @@ class insights_generator {
global $OUTPUT;
// The prediction actions get passed to the target so that it can show them in its preferred way.
$predictionactions = $this->target->prediction_actions($prediction, true, true);
$predictioninfo = $this->target->get_insight_body_for_prediction($context, $user, $prediction, $predictionactions);
$actions = array_merge(
$this->target->prediction_actions($prediction, true, true),
$this->target->bulk_actions([$prediction])
);
$predictioninfo = $this->target->get_insight_body_for_prediction($context, $user, $prediction, $actions);
// For FORMAT_PLAIN.
$fullmessageplaintext = '';
@@ -200,18 +203,23 @@ class insights_generator {
// For FORMAT_HTML.
$messageactions = [];
foreach ($predictionactions as $action) {
$actionurl = $action->get_url();
if (!$actionurl->get_param('forwardurl')) {
foreach ($actions as $action) {
if (!$action->get_url()->get_param('forwardurl')) {
$params = ['actionvisiblename' => $action->get_text(), 'target' => '_blank'];
$actiondoneurl = new \moodle_url('/report/insights/done.php', $params);
// Set the forward url to the 'done' script.
$actionurl->param('forwardurl', $actiondoneurl->out(false));
$action->get_url()->param('forwardurl', $actiondoneurl->out(false));
}
if ($action->get_url()->param('predictionid') === null) {
// Bulk actions do not include the prediction id by default.
$action->get_url()->param('predictionid', $prediction->get_prediction_data()->id);
}
if (empty($insighturl)) {
// We use the primary action url as insight url so we log that the user followed the provided link.
// Ideally the target provides us with the best URL for the insight, if it doesn't we default
// to the first actions.
$insighturl = $action->get_url();
}
@@ -221,7 +229,7 @@ class insights_generator {
$fullmessageplaintext .= get_string('insightinfomessageaction', 'analytics', $actiondata) . PHP_EOL;
// We now process the HTML version actions, with a special treatment for useful/notuseful.
if ($action->get_action_name() === 'fixed') {
if ($action->get_action_name() === 'useful') {
$usefulurl = $actiondata->url;
} else if ($action->get_action_name() === 'notuseful') {
$notusefulurl = $actiondata->url;
@@ -236,11 +244,12 @@ class insights_generator {
}
$contextinfo = [
'usefulbuttons' => $usefulbuttons,
'usefulbuttons' => !empty($usefulbuttons) ? $usefulbuttons : false,
'actions' => $messageactions,
'body' => $predictioninfo[FORMAT_HTML] ?? ''
];
$fullmessagehtml = $OUTPUT->render_from_template('core_analytics/insight_info_message_prediction', $contextinfo);
return [$insighturl, $fullmessageplaintext, $fullmessagehtml];
}
+47 -25
View File
@@ -141,7 +141,8 @@ abstract class base extends \core_analytics\calculable {
*
* @param \core_analytics\prediction $prediction
* @param bool $includedetailsaction
* @param bool $isinsightuser
* @param bool $isinsightuser Force all the available actions to be returned as it the user who
* receives the insight is the one logged in.
* @return \core_analytics\prediction_action[]
*/
public function prediction_actions(\core_analytics\prediction $prediction, $includedetailsaction = false,
@@ -152,8 +153,6 @@ abstract class base extends \core_analytics\calculable {
$contextid = $prediction->get_prediction_data()->contextid;
$modelid = $prediction->get_prediction_data()->modelid;
$PAGE->requires->js_call_amd('report_insights/actions', 'init', array($predictionid, $contextid, $modelid));
$actions = array();
if ($this->link_insights_report() && $includedetailsaction) {
@@ -163,30 +162,53 @@ abstract class base extends \core_analytics\calculable {
$actions[] = new \core_analytics\prediction_action(\core_analytics\prediction::ACTION_PREDICTION_DETAILS, $prediction,
$predictionurl, new \pix_icon('t/preview', $detailstext),
$detailstext);
$detailstext, false, [], \core_analytics\action::TYPE_NEUTRAL);
}
// 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;
}
// 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);
/**
* Suggested bulk actions for a user.
*
* @param \core_analytics\prediction[] $predictions List of predictions suitable for the bulk actions to use.
* @return \core_analytics\bulk_action[] The list of bulk actions.
*/
public function bulk_actions(array $predictions) {
$analyserclass = $this->get_analyser_class();
if ($analyserclass::one_sample_per_analysable()) {
// Default actions are useful / not useful.
$actions = [
\core_analytics\default_bulk_actions::useful(),
\core_analytics\default_bulk_actions::not_useful()
];
} else {
// Accept and not applicable.
$actions = [
\core_analytics\default_bulk_actions::accept(),
\core_analytics\default_bulk_actions::not_applicable()
];
if (!self::based_on_assumptions()) {
// We include incorrectly flagged.
$actions[] = \core_analytics\default_bulk_actions::incorrectly_flagged();
}
}
return $actions;
}
/**
* Adds the JS required to run the bulk actions.
*/
public function add_bulk_actions_js() {
global $PAGE;
$PAGE->requires->js_call_amd('report_insights/actions', 'initBulk', ['.insights-bulk-actions']);
}
/**
* Returns the view details link text.
* @return string
@@ -320,13 +342,13 @@ abstract class base extends \core_analytics\calculable {
* @param \context $context
* @param \stdClass $user
* @param \core_analytics\prediction $prediction
* @param \core_analytics\prediction_action[] $predictionactions Passed by reference to remove duplicate links to actions.
* @return array Plain text msg, HTML message and the main URL for this
* insight (you can return null if you are happy with the
* default insight URL calculated in prediction_info())
* @param \core_analytics\action[] $actions Passed by reference to remove duplicate links to actions.
* @return array Plain text msg, HTML message and the main URL for this
* insight (you can return null if you are happy with the
* default insight URL calculated in prediction_info())
*/
public function get_insight_body_for_prediction(\context $context, \stdClass $user, \core_analytics\prediction $prediction,
array &$predictionactions): array {
array &$actions) {
// No extra message by default.
return [FORMAT_PLAIN => '', FORMAT_HTML => '', 'url' => null];
}
+14 -2
View File
@@ -1236,11 +1236,17 @@ class model {
$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)
WHERE apa.predictionid = ap.id AND apa.userid = :userid AND
(apa.actionname = :fixed OR apa.actionname = :notuseful OR
apa.actionname = :useful OR apa.actionname = :notapplicable OR
apa.actionname = :incorrectlyflagged)
)";
$params['userid'] = $USER->id;
$params['fixed'] = \core_analytics\prediction::ACTION_FIXED;
$params['notuseful'] = \core_analytics\prediction::ACTION_NOT_USEFUL;
$params['useful'] = \core_analytics\prediction::ACTION_USEFUL;
$params['notapplicable'] = \core_analytics\prediction::ACTION_NOT_APPLICABLE;
$params['incorrectlyflagged'] = \core_analytics\prediction::ACTION_INCORRECTLY_FLAGGED;
}
return $DB->get_records_sql($sql, $params);
@@ -1319,11 +1325,17 @@ class model {
$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)
WHERE apa.predictionid = ap.id AND apa.userid = :userid AND
(apa.actionname = :fixed OR apa.actionname = :notuseful OR
apa.actionname = :useful OR apa.actionname = :notapplicable OR
apa.actionname = :incorrectlyflagged)
)";
$params['userid'] = $USER->id;
$params['fixed'] = \core_analytics\prediction::ACTION_FIXED;
$params['notuseful'] = \core_analytics\prediction::ACTION_NOT_USEFUL;
$params['useful'] = \core_analytics\prediction::ACTION_USEFUL;
$params['notapplicable'] = \core_analytics\prediction::ACTION_NOT_APPLICABLE;
$params['incorrectlyflagged'] = \core_analytics\prediction::ACTION_INCORRECTLY_FLAGGED;
}
$sql .= " ORDER BY ap.timecreated DESC";
+21
View File
@@ -40,6 +40,11 @@ class prediction {
*/
const ACTION_PREDICTION_DETAILS = 'predictiondetails';
/**
* Prediction useful (one of the default prediction actions)
*/
const ACTION_USEFUL = 'useful';
/**
* Prediction not useful (one of the default prediction actions)
*/
@@ -50,6 +55,16 @@ class prediction {
*/
const ACTION_FIXED = 'fixed';
/**
* Prediction not applicable.
*/
const ACTION_NOT_APPLICABLE = 'notapplicable';
/**
* Prediction incorrectly flagged.
*/
const ACTION_INCORRECTLY_FLAGGED = 'incorrectlyflagged';
/**
* @var \stdClass
*/
@@ -136,6 +151,12 @@ class prediction {
$found = true;
}
}
$bulkactions = $target->bulk_actions([$this]);
foreach ($bulkactions as $action) {
if ($action->get_action_name() === $actionname) {
$found = true;
}
}
if (empty($found)) {
throw new \moodle_exception('errorunknownaction', 'analytics');
}
+7 -54
View File
@@ -33,22 +33,7 @@ defined('MOODLE_INTERNAL') || die();
* @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class prediction_action {
/**
* @var string
*/
protected $actionname = null;
/**
* @var \moodle_url
*/
protected $url = null;
/**
* @var \action_menu_link
*/
protected $actionlink = null;
class prediction_action extends action {
/**
* Prediction action constructor.
@@ -60,58 +45,26 @@ class prediction_action {
* @param string $text Link text
* @param bool $primary Primary button or secondary.
* @param array $attributes Link attributes
* @param string|false $type
* @return void
*/
public function __construct($actionname, \core_analytics\prediction $prediction, \moodle_url $actionurl, \pix_icon $icon,
$text, $primary = false, $attributes = array()) {
$text, $primary = false, $attributes = array(), $type = false) {
$this->actionname = $actionname;
$this->text = $text;
$this->set_type($type);
$this->url = self::transform_to_forward_url($actionurl, $actionname, $prediction->get_prediction_data()->id);
// The \action_menu_link items are displayed as an icon with a label, no need to show any text.
if ($primary === false) {
$this->actionlink = new \action_menu_link_secondary($this->url, $icon, $this->text, $attributes);
$this->actionlink = new \action_menu_link_secondary($this->url, $icon, '', $attributes);
} else {
$this->actionlink = new \action_menu_link_primary($this->url, $icon, $this->text, $attributes);
$this->actionlink = new \action_menu_link_primary($this->url, $icon, '', $attributes);
}
}
/**
* Returns the action name.
*
* @return string
*/
public function get_action_name() {
return $this->actionname;
}
/**
* Returns the url to the action.
*
* @return \moodle_url
*/
public function get_url() {
return $this->url;
}
/**
* Returns the link to the action.
*
* @return \action_menu_link
*/
public function get_action_link() {
return $this->actionlink;
}
/**
* Returns the action text.
* @return string
*/
public function get_text() {
return $this->text;
}
/**
* Transforms the provided url to an action url so we can record the user actions.
*
+4 -1
View File
@@ -73,6 +73,9 @@ class stats {
public static function actions_not_useful() : int {
global $DB;
return $DB->count_records('analytics_prediction_actions', ['actionname' => prediction::ACTION_NOT_USEFUL]);
// Simple version using core's TYPE_NEGATIVE actions.
return $DB->count_records_select('analytics_prediction_actions',
'actionname = :notuseful OR actionname = :incorrectlyflagged',
['notuseful' => prediction::ACTION_NOT_USEFUL, 'incorrectlyflagged' => prediction::ACTION_INCORRECTLY_FLAGGED]);
}
}
+8 -8
View File
@@ -111,17 +111,17 @@ Feature: Manage analytics models
And I navigate to "Analytics > Analytics models" in site administration
# View predictions
When I select "C3" from the "contextid" singleselect
And I open the action menu in "Student 6" "table_row"
And I choose "View prediction details" in the open action menu
And I click on "View prediction details" "icon" in the "Student 6" "table_row"
And I should see "Prediction details"
And I should see "Any write action"
And I should see "Read actions amount"
And I open the action menu in "Student 6" "table_row"
And I choose "Acknowledged" in the open action menu
And I open the action menu in "Student 5" "table_row"
And I choose "View prediction details" in the open action menu
And I open the action menu in "Student 5" "table_row"
And I choose "Not useful" in the open action menu
And I click on "Select Student 6 for bulk action" "checkbox" in the "Student 6" "table_row"
And I click on "Accept" "button"
And I click on "Confirm" "button" in the "Accept" "dialogue"
And I click on "View prediction details" "icon" in the "Student 5" "table_row"
And I click on "Select Student 5 for bulk action" "checkbox" in the "Student 5" "table_row"
And I click on "Not applicable" "button"
And I click on "Confirm" "button" in the "Not applicable" "dialogue"
And I should see "No insights reported"
# Clear predictions
When I am on site homepage
+1 -1
View File
@@ -71,7 +71,7 @@ class analytics_manager_testcase extends advanced_testcase {
$predictions = $DB->get_records('analytics_predictions');
$prediction = reset($predictions);
$prediction = new \core_analytics\prediction($prediction, array('whatever' => 'not used'));
$prediction->action_executed(\core_analytics\prediction::ACTION_FIXED, $model->get_target());
$prediction->action_executed(\core_analytics\prediction::ACTION_USEFUL, $model->get_target());
$predictioncontextid = $prediction->get_prediction_data()->contextid;
+6 -4
View File
@@ -103,7 +103,7 @@ class analytics_prediction_actions_testcase extends advanced_testcase {
$action = $DB->get_record('analytics_prediction_actions', array('userid' => $this->teacher2->id));
$this->assertEquals(\core_analytics\prediction::ACTION_FIXED, $action->actionname);
$prediction->action_executed(\core_analytics\prediction::ACTION_NOT_USEFUL, $this->model->get_target());
$prediction->action_executed(\core_analytics\prediction::ACTION_INCORRECTLY_FLAGGED, $this->model->get_target());
$recordset = $this->model->get_prediction_actions($this->context);
$this->assertCount(2, $recordset);
$recordset->close();
@@ -127,12 +127,14 @@ class analytics_prediction_actions_testcase extends advanced_testcase {
list($ignored, $predictions) = $this->model->get_predictions($this->context, false);
$this->assertCount(2, $predictions);
// Teacher 2 flags a prediction (it doesn't matter which one) as fixed.
// Teacher 2 flags a prediction (it doesn't matter which one).
$prediction = reset($predictions);
$prediction->action_executed(\core_analytics\prediction::ACTION_FIXED, $this->model->get_target());
$prediction->action_executed(\core_analytics\prediction::ACTION_NOT_APPLICABLE, $this->model->get_target());
$prediction->action_executed(\core_analytics\prediction::ACTION_INCORRECTLY_FLAGGED, $this->model->get_target());
$recordset = $this->model->get_prediction_actions($this->context);
$this->assertCount(1, $recordset);
$this->assertCount(3, $recordset);
$recordset->close();
list($ignored, $predictions) = $this->model->get_predictions($this->context, true);
@@ -148,7 +150,7 @@ class analytics_prediction_actions_testcase extends advanced_testcase {
$this->assertCount(2, $predictions);
$recordset = $this->model->get_prediction_actions($this->context);
$this->assertCount(1, $recordset);
$this->assertCount(3, $recordset);
$recordset->close();
}
}
+3 -3
View File
@@ -108,7 +108,7 @@ class core_analytics_privacy_model_testcase extends \core_privacy\tests\provider
$this->setUser($this->u3);
$prediction = reset($predictions);
$prediction->action_executed('notuseful', $this->model2->get_target());
$prediction->action_executed(\core_analytics\prediction::ACTION_INCORRECTLY_FLAGGED, $this->model2->get_target());
$this->setAdminUser();
}
@@ -381,7 +381,7 @@ class core_analytics_privacy_model_testcase extends \core_privacy\tests\provider
}
}
$this->setUser($this->u3);
$otheruserprediction->action_executed('notuseful', $this->model1->get_target());
$otheruserprediction->action_executed(\core_analytics\prediction::ACTION_INCORRECTLY_FLAGGED, $this->model1->get_target());
$this->setAdminUser();
$this->export_context_data_for_user($this->u3->id, $system, 'core_analytics');
@@ -411,7 +411,7 @@ class core_analytics_privacy_model_testcase extends \core_privacy\tests\provider
get_string('privacy:metadata:analytics:predictionactions', 'analytics'), $u3action->id]);
$this->assertEquals(get_string('adminhelplogs'), $data->target);
$this->assertEquals(get_string('coresystem'), $data->context);
$this->assertEquals('notuseful', $data->action);
$this->assertEquals(\core_analytics\prediction::ACTION_INCORRECTLY_FLAGGED, $data->action);
}
}
+1 -1
View File
@@ -156,7 +156,7 @@ class analytics_stats_testcase extends advanced_testcase {
$this->assertEquals(0, \core_analytics\stats::actions_not_useful());
// The user has marked the other prediction as not useful.
$p2->action_executed(\core_analytics\prediction::ACTION_NOT_USEFUL, $model->get_target());
$p2->action_executed(\core_analytics\prediction::ACTION_INCORRECTLY_FLAGGED, $model->get_target());
$this->assertEquals(2, \core_analytics\stats::actions());
$this->assertEquals(1, \core_analytics\stats::actions_not_useful());
}
+14
View File
@@ -15,6 +15,20 @@ information provided here is intended especially for developers.
* A new \core_analytics\local\time_splitting\past_periodic abstract class has been added. Time-splitting
methods extending \core_analytics\local\time_splitting\periodic directly should be extending past_periodic
now. 'periodic' can still be directly extended by implementing get_next_range and get_first_start methods.
* Targets can now specify a list of bulk actions in bulk_actions(). core_analytics\prediction_action is now
extending core_analytics\action and a new core_analytics\bulk_action class has been added. Actions can now
specify a type in its constructor: core_analytics\action::TYPE_POSITIVE, TYPE_NEUTRAL or TYPE_NEGATIVE. A list
of default bulk actions is available in \core_analytics\default_bulk_actions.
* The default suggested actions provided to users changed:
* For targets with one single sample per analysable (e.g. upcoming activities due) the default actions are
Useful and Not useful.
* For targets with multiple samples per analysable (e.g. students at risk) the default actions are
Accept, Not applicable and Incorrectly flagged.
* The suggested actions for the existing models have been reworded:
* Predictions flagged as "Acknowledged" in models whose targets use analysers that provide one sample per
analysable (e.g. upcoming activities due) have been updated to "Useful" flag.
* Predictions flagged as "Not useful" in models whose targets use analysers that provide multiple samples
per analysable (e.g. students at risk or no teaching) have been updated to "Incorrectly flagged".
=== 3.7 ===
@@ -35,6 +35,11 @@ defined('MOODLE_INTERNAL') || die();
*/
abstract class course_enrolments extends \core_analytics\local\target\binary {
/**
* @var string
*/
const MESSAGE_ACTION_NAME = 'studentmessage';
/**
* Students in the course.
* @var int[]
@@ -204,28 +209,62 @@ abstract class course_enrolments extends \core_analytics\local\target\binary {
*/
public function prediction_actions(\core_analytics\prediction $prediction, $includedetailsaction = false,
$isinsightuser = false) {
global $USER;
$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[] = new \core_analytics\prediction_action('studentmessage', $prediction, $url, $pix,
get_string('sendmessage', 'message'), false, $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[] = new \core_analytics\prediction_action('viewoutlinereport', $prediction, $url, $pix,
get_string('outlinereport'), false, $attrs);
get_string('outlinereport'), false, ['target' => '_blank']);
return array_merge($actions, parent::prediction_actions($prediction, $includedetailsaction));
return array_merge(parent::prediction_actions($prediction, $includedetailsaction, $isinsightuser), $actions);
}
/**
* Suggested bulk actions for a user.
*
* @param \core_analytics\prediction[] $predictions List of predictions suitable for the bulk actions to use.
* @return \core_analytics\bulk_action[] The list of bulk actions.
*/
public function bulk_actions(array $predictions) {
$actions = [];
$userids = [];
foreach ($predictions as $prediction) {
$sampledata = $prediction->get_sample_data();
$userid = $sampledata['user']->id;
// Indexed by prediction id because we want the predictionid-userid
// mapping later when sending the message.
$userids[$prediction->get_prediction_data()->id] = $userid;
}
// Send a message for all the students.
$attrs = array(
'data-bulk-sendmessage' => '1',
'data-prediction-to-user-id' => json_encode($userids)
);
$actions[] = new \core_analytics\bulk_action(self::MESSAGE_ACTION_NAME, new \moodle_url(''),
new \pix_icon('t/message', get_string('sendmessage', 'message')),
get_string('sendmessage', 'message'), true, $attrs);
return array_merge($actions, parent::bulk_actions($predictions));
}
/**
* Adds the JS required to run the bulk actions.
*/
public function add_bulk_actions_js() {
global $PAGE;
$PAGE->requires->js_call_amd('report_insights/message_users', 'init',
['.insights-bulk-actions', self::MESSAGE_ACTION_NAME]);
parent::add_bulk_actions_js();
}
}
@@ -128,9 +128,7 @@ class no_teaching extends \core_analytics\local\target\binary {
$url, $pix, get_string('participants'));
}
$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]);
$parentactions = parent::prediction_actions($prediction, $includedetailsaction, $isinsightuser);
return array_merge($actions, $parentactions);
}
+3 -1
View File
@@ -66,7 +66,8 @@ $string['errorunexistingmodel'] = 'Non-existing model {$a}';
$string['errorunknownaction'] = 'Unknown action';
$string['eventpredictionactionstarted'] = 'Prediction process started';
$string['eventinsightsviewed'] = 'Insights viewed';
$string['fixedack'] = 'Acknowledged';
$string['fixedack'] = 'Accept';
$string['incorrectlyflagged'] = 'Incorrectly flagged';
$string['insightmessagesubject'] = 'New insight for "{$a}"';
$string['insightinfomessagehtml'] = 'The system generated an insight for you.';
$string['insightinfomessageplain'] = 'The system generated an insight for you: {$a}';
@@ -101,6 +102,7 @@ $string['nonewdata'] = 'No new data available. The model will be analysed after
$string['nonewranges'] = 'No new predictions yet. The model will be analysed after the next analysis interval.';
$string['nopredictionsyet'] = 'No predictions available yet';
$string['noranges'] = 'No predictions yet';
$string['notapplicable'] = 'Not applicable';
$string['notrainingbasedassumptions'] = 'Models based on assumptions do not need training';
$string['notuseful'] = 'Not useful';
$string['novaliddata'] = 'No valid data available';
@@ -241,6 +241,7 @@ class icon_system_fontawesome extends icon_system_font {
'core:i/hide' => 'fa-eye',
'core:i/hierarchylock' => 'fa-lock',
'core:i/import' => 'fa-level-up',
'core:i/incorrect' => 'fa-exclamation',
'core:i/info' => 'fa-info',
'core:i/invalid' => 'fa-times text-danger',
'core:i/item' => 'fa-circle',
+5
View File
@@ -3551,5 +3551,10 @@ function xmldb_main_upgrade($oldversion) {
upgrade_main_savepoint(true, 2019090500.01);
}
if ($oldversion < 2019092700.01) {
upgrade_rename_prediction_actions_useful_incorrectly_flagged();
upgrade_main_savepoint(true, 2019092700.01);
}
return true;
}
+43
View File
@@ -566,3 +566,46 @@ function upgrade_delete_orphaned_file_records() {
$DB->delete_records_list('files_reference', 'id', $deletedfileids);
}
/**
* Updates the existing prediction actions in the database according to the new suggested actions.
* @return null
*/
function upgrade_rename_prediction_actions_useful_incorrectly_flagged() {
global $DB;
// The update depends on the analyser class used by each model so we need to iterate through the models in the system.
$modelids = $DB->get_records_sql("SELECT DISTINCT am.id, am.target
FROM {analytics_models} am
JOIN {analytics_predictions} ap ON ap.modelid = am.id
JOIN {analytics_prediction_actions} apa ON ap.id = apa.predictionid");
foreach ($modelids as $model) {
$targetname = $model->target;
if (!class_exists($targetname)) {
// The plugin may not be available.
continue;
}
$target = new $targetname();
$analyserclass = $target->get_analyser_class();
if (!class_exists($analyserclass)) {
// The plugin may not be available.
continue;
}
if ($analyserclass::one_sample_per_analysable()) {
// From 'fixed' to 'useful'.
$params = ['oldaction' => 'fixed', 'newaction' => 'useful'];
} else {
// From 'notuseful' to 'incorrectlyflagged'.
$params = ['oldaction' => 'notuseful', 'newaction' => 'incorrectlyflagged'];
}
$subsql = "SELECT id FROM {analytics_predictions} WHERE modelid = :modelid";
$updatesql = "UPDATE {analytics_prediction_actions}
SET actionname = :newaction
WHERE predictionid IN ($subsql) AND actionname = :oldaction";
$DB->execute($updatesql, $params + ['modelid' => $model->id]);
}
}
+24 -1
View File
@@ -867,17 +867,24 @@ class single_button implements renderable {
*/
public $actionid;
/**
* @var array
*/
protected $attributes = [];
/**
* Constructor
* @param moodle_url $url
* @param string $label button text
* @param string $method get or post submit method
* @param array $attributes Attributes for the HTML button tag
*/
public function __construct(moodle_url $url, $label, $method='post', $primary=false) {
public function __construct(moodle_url $url, $label, $method='post', $primary=false, $attributes = []) {
$this->url = clone($url);
$this->label = $label;
$this->method = $method;
$this->primary = $primary;
$this->attributes = $attributes;
}
/**
@@ -898,6 +905,17 @@ class single_button implements renderable {
$this->actions[] = $action;
}
/**
* Sets an attribute for the HTML button tag.
*
* @param string $name The attribute name
* @param mixed $value The value
* @return null
*/
public function set_attribute($name, $value) {
$this->attributes[$name] = $value;
}
/**
* Export data.
*
@@ -918,6 +936,11 @@ class single_button implements renderable {
$data->tooltip = $this->tooltip;
$data->primary = $this->primary;
$data->attributes = [];
foreach ($this->attributes as $key => $value) {
$data->attributes[] = ['name' => $key, 'value' => $value];
}
// Form parameters.
$params = $this->url->params();
if ($this->method === 'post') {
+2
View File
@@ -1998,6 +1998,8 @@ class core_renderer extends renderer_base {
foreach ((array)$options as $key=>$value) {
if (array_key_exists($key, $button)) {
$button->$key = $value;
} else {
$button->set_attribute($key, $value);
}
}
+1 -1
View File
@@ -77,7 +77,7 @@
}
}
}}
<div class="dropdown">
<div class="dropdown{{^secondary.items}} hidden{{/secondary.items}}">
<a href="#" tabindex="0" class="{{triggerextraclasses}} dropdown-toggle icon-no-margin" id="dropdown-{{instance}}" aria-label="{{title}}" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false" aria-controls="action-menu-{{instance}}-menu">
{{{actiontext}}}
{{{menutrigger}}}
+9 -2
View File
@@ -42,7 +42,13 @@
"url" : "#",
"primary" : true,
"tooltip" : "This is a tooltip",
"label" : "This is a the button text"
"label" : "This is a the button text",
"attributes": [
{
"name": "data-attribute",
"value": "yeah"
}
]
}
}}
<div class="{{classes}}">
@@ -53,7 +59,8 @@
<button type="submit" class="btn {{#primary}}btn-primary{{/primary}}{{^primary}}btn-secondary{{/primary}}"
id="{{id}}"
title={{#quote}}{{tooltip}}{{/quote}}
{{#disabled}}disabled{{/disabled}}>{{label}}</button>
{{#disabled}}disabled{{/disabled}}
{{#attributes}} {{name}}={{#quote}}{{value}}{{/quote}} {{/attributes}}>{{label}}</button>
</form>
</div>
{{#hasactions}}
+119
View File
@@ -1005,4 +1005,123 @@ class core_upgradelib_testcase extends advanced_testcase {
$file = reset($files);
$this->assertEquals($file, $newstoredfile[1]);
}
/**
* Test that the previous records are updated according to the reworded actions.
* @return null
*/
public function test_upgrade_rename_prediction_actions_useful_incorrectly_flagged() {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
$models = $DB->get_records('analytics_models');
$upcomingactivitiesdue = null;
$noteaching = null;
foreach ($models as $model) {
if ($model->target === '\\core_user\\analytics\\target\\upcoming_activities_due') {
$upcomingactivitiesdue = new \core_analytics\model($model);
}
if ($model->target === '\\core_course\\analytics\\target\\no_teaching') {
$noteaching = new \core_analytics\model($model);
}
}
// Upcoming activities due generating some insights.
$course1 = $this->getDataGenerator()->create_course();
$attrs = ['course' => $course1, 'duedate' => time() + WEEKSECS - DAYSECS];
$assign = $this->getDataGenerator()->get_plugin_generator('mod_assign')->create_instance($attrs);
$student = $this->getDataGenerator()->create_user();
$usercontext = \context_user::instance($student->id);
$this->getDataGenerator()->enrol_user($student->id, $course1->id, 'student');
$upcomingactivitiesdue->predict();
list($ignored, $predictions) = $upcomingactivitiesdue->get_predictions($usercontext, true);
$prediction = reset($predictions);
$predictionaction = (object)[
'predictionid' => $prediction->get_prediction_data()->id,
'userid' => 2,
'actionname' => 'fixed',
'timecreated' => time()
];
$DB->insert_record('analytics_prediction_actions', $predictionaction);
$predictionaction->actionname = 'notuseful';
$DB->insert_record('analytics_prediction_actions', $predictionaction);
upgrade_rename_prediction_actions_useful_incorrectly_flagged();
$this->assertEquals(0, $DB->count_records('analytics_prediction_actions',
['actionname' => \core_analytics\prediction::ACTION_FIXED]));
$this->assertEquals(1, $DB->count_records('analytics_prediction_actions',
['actionname' => \core_analytics\prediction::ACTION_USEFUL]));
$this->assertEquals(1, $DB->count_records('analytics_prediction_actions',
['actionname' => \core_analytics\prediction::ACTION_NOT_USEFUL]));
$this->assertEquals(0, $DB->count_records('analytics_prediction_actions',
['actionname' => \core_analytics\prediction::ACTION_INCORRECTLY_FLAGGED]));
// No teaching generating some insights.
$course2 = $this->getDataGenerator()->create_course(['startdate' => time() + (2 * DAYSECS)]);
$noteaching->predict();
list($ignored, $predictions) = $noteaching->get_predictions(\context_system::instance(), true);
$prediction = reset($predictions);
$predictionaction = (object)[
'predictionid' => $prediction->get_prediction_data()->id,
'userid' => 2,
'actionname' => 'notuseful',
'timecreated' => time()
];
$DB->insert_record('analytics_prediction_actions', $predictionaction);
$predictionaction->actionname = 'fixed';
$DB->insert_record('analytics_prediction_actions', $predictionaction);
upgrade_rename_prediction_actions_useful_incorrectly_flagged();
$this->assertEquals(1, $DB->count_records('analytics_prediction_actions',
['actionname' => \core_analytics\prediction::ACTION_FIXED]));
$this->assertEquals(1, $DB->count_records('analytics_prediction_actions',
['actionname' => \core_analytics\prediction::ACTION_USEFUL]));
$this->assertEquals(1, $DB->count_records('analytics_prediction_actions',
['actionname' => \core_analytics\prediction::ACTION_NOT_USEFUL]));
$this->assertEquals(1, $DB->count_records('analytics_prediction_actions',
['actionname' => \core_analytics\prediction::ACTION_INCORRECTLY_FLAGGED]));
// We also check that there are no records incorrectly switched in upcomingactivitiesdue.
$upcomingactivitiesdue->clear();
$this->assertEquals(1, $DB->count_records('analytics_prediction_actions',
['actionname' => \core_analytics\prediction::ACTION_FIXED]));
$this->assertEquals(0, $DB->count_records('analytics_prediction_actions',
['actionname' => \core_analytics\prediction::ACTION_USEFUL]));
$this->assertEquals(0, $DB->count_records('analytics_prediction_actions',
['actionname' => \core_analytics\prediction::ACTION_NOT_USEFUL]));
$this->assertEquals(1, $DB->count_records('analytics_prediction_actions',
['actionname' => \core_analytics\prediction::ACTION_INCORRECTLY_FLAGGED]));
$upcomingactivitiesdue->predict();
list($ignored, $predictions) = $upcomingactivitiesdue->get_predictions($usercontext, true);
$prediction = reset($predictions);
$predictionaction = (object)[
'predictionid' => $prediction->get_prediction_data()->id,
'userid' => 2,
'actionname' => 'fixed',
'timecreated' => time()
];
$DB->insert_record('analytics_prediction_actions', $predictionaction);
$predictionaction->actionname = 'notuseful';
$DB->insert_record('analytics_prediction_actions', $predictionaction);
upgrade_rename_prediction_actions_useful_incorrectly_flagged();
$this->assertEquals(1, $DB->count_records('analytics_prediction_actions',
['actionname' => \core_analytics\prediction::ACTION_FIXED]));
$this->assertEquals(1, $DB->count_records('analytics_prediction_actions',
['actionname' => \core_analytics\prediction::ACTION_USEFUL]));
$this->assertEquals(1, $DB->count_records('analytics_prediction_actions',
['actionname' => \core_analytics\prediction::ACTION_NOT_USEFUL]));
$this->assertEquals(1, $DB->count_records('analytics_prediction_actions',
['actionname' => \core_analytics\prediction::ACTION_INCORRECTLY_FLAGGED]));
}
}
+1
View File
@@ -65,6 +65,7 @@ validation against and defaults to null (so, no user needed) if not provided.
This setting can be set via the UI or by defining $CFG->cachetemplates in your config.php file. It is a boolean
and should be set to either false or true. Developers will probably want to set this to false.
* The core_enrol_edit_user_enrolment webservice has been deprecated. Please use core_enrol_submit_user_enrolment_form instead.
* \single_button constructor has a new attributes param to add attributes to the button HTML tag.
=== 3.7 ===
+1 -1
View File
@@ -1,2 +1,2 @@
define ("report_insights/actions",["jquery","core/ajax","core/notification","core/url"],function(a,b,c,d){return{init:function init(e,f,g){a("a[data-prediction-methodname][data-prediction-id="+e+"]").on("click",function(h){h.preventDefault();var i=a(h.currentTarget),j=i.attr("data-prediction-methodname"),k=i.closest("tr");if(0<k.length){var l=b.call([{methodname:j,args:{predictionid:e}}])[0];l.done(function(){k[0].remove();if(2>a(".insights-list tr").length){var b=a.param({contextid:f,modelid:g});window.location.assign(d.relativeUrl("report/insights/insights.php?"+b))}}).fail(c.exception)}})}}});
define ("report_insights/actions",["jquery","core/str","core/ajax","core/notification","core/url","core/modal_factory","core/modal_events"],function(a,b,c,d,e,f,g){return{initBulk:function initBulk(h){var i=function(a,b,f){return c.call([{methodname:"report_insights_action_executed",args:{predictionids:a,actionname:f}}])[0].then(function(){var a=!1;b.forEach(function(b){if(!1===a){a=b.closest("table")}b.remove()});if(0===a.find("tbody > tr").length){var c={contextid:a.closest("div.insight-container").data("context-id"),modelid:a.closest("div.insight-container").data("model-id")};window.location.assign(e.relativeUrl("report/insights/insights.php",c,!1))}}).catch(d.exception)};a(h+" [data-bulk-actionname]").on("click",function(c){c.preventDefault();var e=a(c.currentTarget),h=e.data("bulk-actionname"),j=e.text().trim(),k=[],l=[];a(".insights-list input[data-togglegroup^=\"insight-bulk-action-\"][data-toggle=\"slave\"]:checked").each(function(){var b=a(this).closest("tr[data-prediction-id]");l.push(b);k.push(b.data("prediction-id"))});if(0===k.length){return this}var m=[];b.get_strings([{key:"confirmbulkaction",component:"report_insights",param:{action:j,nitems:k.length}},{key:"confirm",component:"moodle"}]).then(function(a){m=a;return f.create({type:f.types.SAVE_CANCEL,title:j,body:m[0]})}).then(function(a){a.setSaveButtonText(m[1]);a.show();a.getRoot().on(g.save,function(){return i(k,l,h)});return a}).catch(d.exception);return this})}}});
//# sourceMappingURL=actions.min.js.map
File diff suppressed because one or more lines are too long
+2
View File
@@ -0,0 +1,2 @@
define ("report_insights/message_users",["jquery","core/str","core/log","core/modal_factory","core/modal_events","core/templates","core/notification","core/ajax"],function(a,b,c,d,e,f,g,h){var i={BULKACTIONSELECT:"#formactionid"},j=function(a,b){this.actionName=b;this.attachEventListeners(a)};j.prototype.actionName=null;j.prototype.modal=null;j.prototype.attachEventListeners=function(b){a(b+" button[data-bulk-sendmessage]").on("click",function(b){b.preventDefault();var d=a(b.currentTarget),e={},f=d.data("prediction-to-user-id");a(".insights-list input[data-togglegroup^=\"insight-bulk-action\"][data-toggle=\"slave\"]:checked").each(function(b,d){var g=a(d).closest("tr[data-prediction-id]").data("prediction-id");if("undefined"==typeof f[g]){c.error("Unknown user for prediction "+g);return}var h=f[g];e[g]=h});if(0===Object.keys(e).length){return this}this.showSendMessage(e);return this}.bind(this))};j.prototype.showSendMessage=function(c){var g=new Set(Object.values(c));if(0==g.length){return a.Deferred().resolve().promise()}var h=null;if(1==g.size){h=b.get_string("sendbulkmessagesingle","core_message")}else{h=b.get_string("sendbulkmessage","core_message",g.size)}return a.when(d.create({type:d.types.SAVE_CANCEL,body:f.render("core_user/send_bulk_message",{})}),h).then(function(b,d){this.modal=b;this.modal.setTitle(d);this.modal.setSaveButtonText(d);this.modal.getRoot().on(e.hidden,function(){a(i.BULKACTIONSELECT).focus();this.modal.getRoot().remove()}.bind(this));this.modal.getRoot().on(e.save,this.submitSendMessage.bind(this,c));this.modal.show();return this.modal}.bind(this))};j.prototype.submitSendMessage=function(a){var c=this.modal.getRoot().find("form textarea").val(),d=[],e=new Set(Object.values(a));e.forEach(function(a){d.push({touserid:a,text:c})});var f=this.actionName,i=null;return h.call([{methodname:"core_message_send_instant_messages",args:{messages:d}}])[0].then(function(a){if(1==a.length){return b.get_string("sendbulkmessagesentsingle","core_message")}else{return b.get_string("sendbulkmessagesent","core_message",a.length)}}).then(function(b){i=b;return h.call([{methodname:"report_insights_action_executed",args:{actionname:f,predictionids:Object.keys(a)}}])[0]}).then(function(){g.addNotification({message:i,type:"success"});return!0}).catch(g.exception)};return{init:function init(a,b){return new j(a,b)}}});
//# sourceMappingURL=message_users.min.js.map
File diff suppressed because one or more lines are too long
+91 -30
View File
@@ -26,50 +26,111 @@
*
* @module report_insights/actions
*/
define(['jquery', 'core/ajax', 'core/notification', 'core/url'], function($, Ajax, Notification, Url) {
define(['jquery', 'core/str', 'core/ajax', 'core/notification', 'core/url', 'core/modal_factory', 'core/modal_events'],
function($, Str, Ajax, Notification, Url, ModalFactory, ModalEvents) {
return {
/**
* Attach on click handlers to hide predictions.
* Attach on click handlers for bulk actions.
*
* @param {Number} predictionId The prediction id.
* @param {Number} contextId The context in which the prediction was made.
* @param {Number} modelId The model id model with which the prediction was made.
* @param {String} rootNode
* @access public
*/
init: function(predictionId, contextId, modelId) {
initBulk: function(rootNode) {
// Select the prediction with the provided id ensuring that an external function is set as method name.
$('a[data-prediction-methodname][data-prediction-id=' + predictionId + ']').on('click', function(e) {
/**
* Executes the provided action.
*
* @param {Array} predictionIds
* @param {Array} predictionContainers
* @param {String} actionName
* @return {Promise}
*/
var executeAction = function(predictionIds, predictionContainers, actionName) {
return Ajax.call([
{
methodname: 'report_insights_action_executed',
args: {
predictionids: predictionIds,
actionname: actionName
}
}
])[0].then(function() {
// Remove the selected elements from the list.
var tableNode = false;
predictionContainers.forEach(function(el) {
if (tableNode === false) {
tableNode = el.closest('table');
}
el.remove();
});
if (tableNode.find('tbody > tr').length === 0) {
let params = {
contextid: tableNode.closest('div.insight-container').data('context-id'),
modelid: tableNode.closest('div.insight-container').data('model-id')
};
window.location.assign(Url.relativeUrl("report/insights/insights.php", params, false));
}
return;
}).catch(Notification.exception);
};
$(rootNode + ' [data-bulk-actionname]').on('click', function(e) {
e.preventDefault();
var action = $(e.currentTarget);
var methodname = action.attr('data-prediction-methodname');
var predictionContainers = action.closest('tr');
var actionName = action.data('bulk-actionname');
var actionVisibleName = action.text().trim();
if (predictionContainers.length > 0) {
var promise = Ajax.call([
{
methodname: methodname,
args: {predictionid: predictionId}
}
])[0];
promise.done(function() {
predictionContainers[0].remove();
var predictionIds = [];
var predictionContainers = [];
// Move back if no remaining predictions.
if ($('.insights-list tr').length < 2) {
var params = {
contextid: contextId,
modelid: modelId
};
$('.insights-list input[data-togglegroup^="insight-bulk-action-"][data-toggle="slave"]:checked').each(function() {
var container = $(this).closest('tr[data-prediction-id]');
predictionContainers.push(container);
predictionIds.push(container.data('prediction-id'));
});
var queryparams = $.param(params);
window.location.assign(Url.relativeUrl("report/insights/insights.php?" + queryparams));
}
}).fail(Notification.exception);
if (predictionIds.length === 0) {
// No items selected message.
return this;
}
var strings = [];
Str.get_strings([{
key: 'confirmbulkaction',
component: 'report_insights',
param: {
action: actionVisibleName,
nitems: predictionIds.length
}
}, {
key: 'confirm',
component: 'moodle'
}]
).then(function(strs) {
strings = strs;
return ModalFactory.create({
type: ModalFactory.types.SAVE_CANCEL,
title: actionVisibleName,
body: strings[0],
});
}).then(function(modal) {
modal.setSaveButtonText(strings[1]);
modal.show();
modal.getRoot().on(ModalEvents.save, function() {
// The action is now confirmed, sending an action for it.
return executeAction(predictionIds, predictionContainers, actionName);
});
return modal;
}).catch(Notification.exception);
return this;
});
}
},
};
});
+208
View File
@@ -0,0 +1,208 @@
// 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/>.
/**
* Message users.
*
* @module report_insights/message_users
* @package report_insights
* @copyright 2019 David Monllao
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(['jquery', 'core/str', 'core/log', 'core/modal_factory', 'core/modal_events', 'core/templates',
'core/notification', 'core/ajax'],
function($, Str, Log, ModalFactory, ModalEvents, Templates, Notification, Ajax) {
var SELECTORS = {
BULKACTIONSELECT: "#formactionid"
};
/**
* Constructor.
*
* @param {String} rootNode
* @param {String} actionName
*/
var MessageUsers = function(rootNode, actionName) {
this.actionName = actionName;
this.attachEventListeners(rootNode);
};
/**
* @var {String} actionName
* @private
*/
MessageUsers.prototype.actionName = null;
/**
* @var {Modal} modal
* @private
*/
MessageUsers.prototype.modal = null;
/**
* Attach the event listener to the send message bulk action.
* @param {String} rootNode
*/
MessageUsers.prototype.attachEventListeners = function(rootNode) {
$(rootNode + ' button[data-bulk-sendmessage]').on('click', function(e) {
e.preventDefault();
var cTarget = $(e.currentTarget);
// Using an associative array in case there is more than 1 prediction for the same user.
var users = {};
var predictionToUserMapping = cTarget.data('prediction-to-user-id');
var checkedSelector = '.insights-list input[data-togglegroup^="insight-bulk-action"][data-toggle="slave"]:checked';
$(checkedSelector).each(function(index, value) {
var predictionId = $(value).closest('tr[data-prediction-id]').data('prediction-id');
if (typeof predictionToUserMapping[predictionId] === 'undefined') {
Log.error('Unknown user for prediction ' + predictionId);
return;
}
var userId = predictionToUserMapping[predictionId];
users[predictionId] = userId;
});
if (Object.keys(users).length === 0) {
return this;
}
this.showSendMessage(users);
return this;
}.bind(this));
};
/**
* Show the send message popup.
*
* @method showSendMessage
* @private
* @param {Object} users Prediction id to user id mapping.
* @return {Promise}
*/
MessageUsers.prototype.showSendMessage = function(users) {
var userIds = new Set(Object.values(users));
if (userIds.length == 0) {
// Nothing to do.
return $.Deferred().resolve().promise();
}
var titlePromise = null;
if (userIds.size == 1) {
titlePromise = Str.get_string('sendbulkmessagesingle', 'core_message');
} else {
titlePromise = Str.get_string('sendbulkmessage', 'core_message', userIds.size);
}
return $.when(
ModalFactory.create({
type: ModalFactory.types.SAVE_CANCEL,
body: Templates.render('core_user/send_bulk_message', {})
}),
titlePromise
).then(function(modal, title) {
// Keep a reference to the modal.
this.modal = modal;
this.modal.setTitle(title);
this.modal.setSaveButtonText(title);
// We want to focus on the action select when the dialog is closed.
this.modal.getRoot().on(ModalEvents.hidden, function() {
$(SELECTORS.BULKACTIONSELECT).focus();
this.modal.getRoot().remove();
}.bind(this));
this.modal.getRoot().on(ModalEvents.save, this.submitSendMessage.bind(this, users));
this.modal.show();
return this.modal;
}.bind(this));
};
/**
* Send a message to these users.
*
* @method submitSendMessage
* @private
* @param {Object} users Prediction id to user id mapping.
* @param {Event} e Form submission event.
* @return {Promise}
*/
MessageUsers.prototype.submitSendMessage = function(users) {
var messageText = this.modal.getRoot().find('form textarea').val();
var messages = [];
var userIds = new Set(Object.values(users));
userIds.forEach(function(userId) {
messages.push({touserid: userId, text: messageText});
});
var actionName = this.actionName;
var message = null;
return Ajax.call([{
methodname: 'core_message_send_instant_messages',
args: {messages: messages}
}])[0].then(function(messageIds) {
if (messageIds.length == 1) {
return Str.get_string('sendbulkmessagesentsingle', 'core_message');
} else {
return Str.get_string('sendbulkmessagesent', 'core_message', messageIds.length);
}
}).then(function(msg) {
// Save this for the following callback. Now that we got everything
// done we can flag this action as executed.
message = msg;
return Ajax.call([{
methodname: 'report_insights_action_executed',
args: {
actionname: actionName,
predictionids: Object.keys(users)
}
}])[0];
}).then(function() {
Notification.addNotification({
message: message,
type: "success"
});
return true;
}).catch(Notification.exception);
};
return /** @alias module:report_insights/message_users */ {
// Public variables and functions.
/**
* @method init
* @param {String} rootNode
* @param {String} actionName
* @return {MessageUsers}
*/
'init': function(rootNode, actionName) {
return new MessageUsers(rootNode, actionName);
}
};
});
+70
View File
@@ -32,6 +32,7 @@ use external_api;
use external_function_parameters;
use external_value;
use external_single_structure;
use external_multiple_structure;
use external_warnings;
/**
@@ -90,6 +91,13 @@ class external extends external_api {
);
}
/**
* Deprecated in favour of action_executed.
*/
public static function set_notuseful_prediction_is_deprecated() {
return true;
}
/**
* set_fixed_prediction parameters.
*
@@ -138,6 +146,68 @@ class external extends external_api {
);
}
/**
* Deprecated in favour of action_executed.
*/
public static function set_fixed_prediction_is_deprecated() {
return true;
}
/**
* action_executed parameters.
*
* @return external_function_parameters
* @since Moodle 3.8
*/
public static function action_executed_parameters() {
return new external_function_parameters (
array(
'actionname' => new external_value(PARAM_ALPHANUMEXT, 'The name of the action', VALUE_REQUIRED),
'predictionids' => new external_multiple_structure(
new external_value(PARAM_INT, 'Prediction id', VALUE_REQUIRED),
'Array of prediction ids'
),
)
);
}
/**
* Stores an action executed over a group of predictions.
*
* @param string $actionname
* @param array $predictionids
* @return array an array of warnings and a boolean
* @since Moodle 3.8
*/
public static function action_executed(string $actionname, array $predictionids) {
$params = self::validate_parameters(self::action_executed_parameters(),
array('actionname' => $actionname, 'predictionids' => $predictionids));
foreach ($params['predictionids'] as $predictionid) {
list($model, $prediction, $context) = self::validate_prediction($predictionid);
// The method action_executed checks that the provided action is valid.
$prediction->action_executed($actionname, $model->get_target());
}
return array('warnings' => array());
}
/**
* action_executed return
*
* @return external_description
* @since Moodle 3.8
*/
public static function action_executed_returns() {
return new external_single_structure(
array(
'warnings' => new external_warnings(),
)
);
}
/**
* Validates access to the prediction and returns it.
*
@@ -0,0 +1,120 @@
<?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/>.
/**
* Output helper to export actions for rendering.
*
* @package report_insights
* @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace report_insights\output;
defined('MOODLE_INTERNAL') || die();
/**
* Output helper to export actions for rendering.
*
* @package report_insights
* @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class actions_exporter {
/**
* Add the prediction actions renderable.
*
* @param \core_analytics\local\target\base $target
* @param \renderer_base $output
* @param \core_analytics\prediction $prediction
* @param bool $includedetailsaction
* @return \stdClass|false
*/
public static function add_prediction_actions(\core_analytics\local\target\base $target, \renderer_base $output,
\core_analytics\prediction $prediction, bool $includedetailsaction = false) {
$actions = $target->prediction_actions($prediction, $includedetailsaction);
if ($actions) {
$actionsmenu = new \action_menu();
// Add all actions defined by the target.
foreach ($actions as $action) {
$actionsmenu->add_primary_action($action->get_action_link());
}
return $actionsmenu->export_for_template($output);
}
return false;
}
/**
* Add bulk actions renderables.
*
* Note that if you are planning to render the bulk actions, the provided predictions must share the same predicted value.
*
* @param \core_analytics\local\target\base $target
* @param \renderer_base $output
* @param \core_analytics\prediction[] $predictions Bulk actions for this set of predictions.
* @param \context $context The context of these predictions.
* @return \stdClass[]|false
*/
public static function add_bulk_actions(\core_analytics\local\target\base $target, \renderer_base $output, array $predictions,
\context $context) {
global $USER;
$bulkactions = $target->bulk_actions($predictions);
if ($context->contextlevel === CONTEXT_USER) {
// Remove useful / notuseful if the current user is not part of the users who receive the insight (e.g. a site manager
// who looks at the generated insights for a particular user).
$insightusers = $target->get_insights_users($context);
if (empty($insightusers[$USER->id])) {
foreach ($bulkactions as $key => $action) {
if ($action->get_action_name() === 'useful' || $action->get_action_name() === 'notuseful') {
unset($bulkactions[$key]);
}
}
}
}
if (!$bulkactions) {
return false;
}
$actionsmenu = [];
// All the predictions share a common predicted value.
$predictionvalue = reset($predictions)->get_prediction_data()->prediction;
// Add all actions defined by the target.
foreach ($bulkactions as $action) {
$action->get_action_link()->set_attribute('data-togglegroup', 'insight-bulk-action-' . $predictionvalue);
$actionsmenu[] = $action->get_action_link()->export_for_template($output);
}
if (empty($actionsmenu)) {
return false;
}
return $actionsmenu;
}
}
+45 -17
View File
@@ -50,18 +50,27 @@ class insight implements \renderable, \templatable {
*/
protected $includedetailsaction = false;
/**
* @var \context
*/
protected $context;
/**
* Constructor
*
* @param \core_analytics\prediction $prediction
* @param \core_analytics\model $model
* @param bool $includedetailsaction
* @param \context $context
* @return void
*/
public function __construct(\core_analytics\prediction $prediction, \core_analytics\model $model, $includedetailsaction = false) {
public function __construct(\core_analytics\prediction $prediction, \core_analytics\model $model, $includedetailsaction = false,
\context $context) {
$this->prediction = $prediction;
$this->model = $model;
$this->includedetailsaction = $includedetailsaction;
$this->context = $context;
}
/**
@@ -77,6 +86,9 @@ class insight implements \renderable, \templatable {
$target = $this->model->get_target();
$data = new \stdClass();
$data->modelid = $this->model->get_id();
$data->contextid = $this->context->id;
$data->predictionid = $predictiondata->id;
$data->insightname = format_string($target->get_name());
$data->showpredictionheading = true;
@@ -110,26 +122,13 @@ class insight implements \renderable, \templatable {
// Prediction info.
$predictedvalue = $predictiondata->prediction;
$predictionid = $predictiondata->id;
$data->predictiondisplayvalue = $target->get_display_value($predictedvalue);
list($data->style, $data->outcomeicon) = self::get_calculation_display($target,
floatval($predictedvalue), $output);
$actions = $target->prediction_actions($this->prediction, $this->includedetailsaction);
if ($actions) {
$actionsmenu = new \action_menu();
$actionsmenu->set_menu_trigger(get_string('actions'));
$actionsmenu->set_owner_selector('prediction-actions-' . $predictionid);
$actionsmenu->set_alignment(\action_menu::TL, \action_menu::BL);
// Add all actions defined by the target.
foreach ($actions as $action) {
$actionsmenu->add($action->get_action_link());
}
$data->actions = $actionsmenu->export_for_template($output);
} else {
$data->actions = false;
}
$data->actions = actions_exporter::add_prediction_actions($target, $output, $this->prediction,
$this->includedetailsaction);
$data->bulkactions = actions_exporter::add_bulk_actions($target, $output, [$this->prediction], $this->context);
// Calculated indicators values.
$data->calculations = array();
@@ -162,6 +161,26 @@ class insight implements \renderable, \templatable {
);
}
// This is only rendered in report_insights/insight_details template. We need it to automatically enable
// the bulk action buttons in report/insights/prediction.php.
$toggleall = new \core\output\checkbox_toggleall('insight-bulk-action-' . $predictedvalue, true, [
'id' => 'id-toggle-all-' . $predictedvalue,
'name' => 'toggle-all-' . $predictedvalue,
'classes' => 'hidden',
'label' => get_string('selectall'),
'labelclasses' => 'sr-only',
'checked' => false
]);
$data->hiddencheckboxtoggleall = $output->render($toggleall);
$toggle = new \core\output\checkbox_toggleall('insight-bulk-action-' . $predictedvalue, false, [
'id' => 'id-select-' . $data->predictionid,
'name' => 'select-' . $data->predictionid,
'label' => get_string('selectprediction', 'report_insights', $data->sampledescription),
'labelclasses' => 'accesshide',
]);
$data->toggleslave = $output->render($toggle);
return $data;
}
@@ -211,4 +230,13 @@ class insight implements \renderable, \templatable {
$icon = new \pix_icon($icon, $text);
return array($style, $icon->export_for_template($output));
}
/**
* Model getter.
*
* @return \core_analytics\model
*/
public function get_model(): \core_analytics\model {
return $this->model;
}
}
@@ -90,6 +90,8 @@ class insights_list implements \renderable, \templatable {
$target = $this->model->get_target();
$data = new \stdClass();
$data->modelid = $this->model->get_id();
$data->contextid = $this->context->id;
$data->insightname = format_string($target->get_name());
$data->showpredictionheading = true;
@@ -105,6 +107,9 @@ class insights_list implements \renderable, \templatable {
$total = 0;
if ($this->model->uses_insights()) {
$target->add_bulk_actions_js();
$predictionsdata = $this->model->get_predictions($this->context, true, $this->page, $this->perpage);
if (!$this->model->is_static()) {
@@ -118,6 +123,9 @@ class insights_list implements \renderable, \templatable {
if ($predictionsdata) {
list($total, $predictions) = $predictionsdata;
$data->bulkactions = actions_exporter::add_bulk_actions($target, $output, $predictions, $this->context);
$data->multiplepredictions = count($predictions) > 1 ? true : false;
foreach ($predictions as $prediction) {
$predictedvalue = $prediction->get_prediction_data()->prediction;
@@ -130,7 +138,7 @@ class insights_list implements \renderable, \templatable {
$predictionvalues[$predictedvalue] = $preddata;
}
$insightrenderable = new \report_insights\output\insight($prediction, $this->model, true);
$insightrenderable = new \report_insights\output\insight($prediction, $this->model, true, $this->context);
$insights[$predictedvalue][] = $insightrenderable->export_for_template($output);
}
@@ -146,6 +154,17 @@ class insights_list implements \renderable, \templatable {
// Ok, now we have all the data we want, put it into a format that mustache can handle.
foreach ($predictionvalues as $key => $prediction) {
if (isset($insights[$key])) {
$toggleall = new \core\output\checkbox_toggleall('insight-bulk-action-' . $key, true, [
'id' => 'id-toggle-all-' . $key,
'name' => 'toggle-all-' . $key,
'label' => get_string('selectall'),
'labelclasses' => 'sr-only',
'checked' => false
]);
$prediction['checkboxtoggleall'] = $output->render($toggleall);
$prediction['predictedvalue'] = $key;
$prediction['insights'] = $insights[$key];
}
@@ -58,6 +58,7 @@ class renderer extends plugin_renderer_base {
*/
protected function render_insight(renderable $renderable) {
$data = $renderable->export_for_template($this);
$renderable->get_model()->get_target()->add_bulk_actions_js();
return parent::render_from_template('report_insights/insight_details', $data);
}
+10
View File
@@ -42,6 +42,16 @@ $functions = array(
'type' => 'write',
'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
'ajax' => true,
),
'report_insights_action_executed' => array(
'classname' => 'report_insights\external',
'methodname' => 'action_executed',
'description' => 'Stores an action executed over a group of predictions.',
'type' => 'write',
'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
'ajax' => true,
)
);
+1 -1
View File
@@ -82,7 +82,7 @@ $PAGE->set_url($url);
$PAGE->set_pagelayout('report');
if ($context->contextlevel === CONTEXT_SYSTEM) {
admin_externalpage_setup('reportinsights', '', null, '', array('pagelayout' => 'report'));
admin_externalpage_setup('reportinsights', '', $url->params(), $url->out(false), array('pagelayout' => 'report'));
} else if ($context->contextlevel === CONTEXT_USER) {
$user = \core_user::get_user($context->instanceid, '*', MUST_EXIST);
$PAGE->navigation->extend_for_user($user);
@@ -23,6 +23,7 @@
*/
$string['actionsaved'] = 'Your feedback of \'{$a}\' has been saved.';
$string['confirmbulkaction'] = 'Are you use you want to flag the {$a->nitems} selected predictions as "{$a->action}"?';
$string['disabledmodel'] = 'Sorry, this model has been disabled by the administrator';
$string['indicators'] = 'Indicators';
$string['insight'] = 'Insight';
@@ -39,6 +40,7 @@ $string['pluginname'] = 'Insights';
$string['prediction'] = 'Prediction';
$string['predictiondetails'] = 'Prediction details';
$string['nodetailsavailable'] = 'No prediction details are relevant.';
$string['selectprediction'] = 'Select {$a} for bulk action';
$string['timecreated'] = 'Time predicted';
$string['timerange'] = 'Analysis interval';
$string['timerangewithdata'] = '{$a->timestart} to {$a->timeend}';
+1 -1
View File
@@ -80,7 +80,7 @@ $PAGE->set_title($insightinfo->insightname);
echo $OUTPUT->header();
$renderable = new \report_insights\output\insight($prediction, $model, false);
$renderable = new \report_insights\output\insight($prediction, $model, false, $context);
echo $renderer->render($renderable);
echo $OUTPUT->footer();
@@ -0,0 +1,61 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template report_insights/bulk_action_button
Extension of core/single_button to show the label with HTML
Classes required for JS:
* none
Data attributes required for JS:
* none
Context variables required for this template:
* classes - a list of classes to wrap the form.
* url - the action url to submit to
* params - array of params with name and value attributes
* primary - true if this is a primary action button
* id - id for the element
* tooltip - tooltip text for the button
* disabled - true if this element is disabled
* label - text to show on the button
Example context (json):
{
"url" : "#",
"primary" : true,
"tooltip" : "This is a tooltip",
"label" : "This is a the button text, it can include HTML",
"attributes": [
{
"name": "data-attribute",
"value": "yeah"
}
]
}
}}
<div class="{{classes}}">
{{#params}}
<input type="hidden" name="{{name}}" value="{{value}}">
{{/params}}
<button type="submit" class="btn {{#primary}}btn-primary{{/primary}}{{^primary}}btn-outline-primary{{/primary}}"
id="{{id}}"
title={{#quote}}{{tooltip}}{{/quote}}
{{#disabled}}disabled{{/disabled}}
{{#attributes}} {{name}}={{#quote}}{{value}}{{/quote}} {{/attributes}}>{{{label}}}</button>
</div>
@@ -0,0 +1,42 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template report_insights/insight_details
Actions panel at the bottom of the assignment grading UI.
Classes required for JS:
* none
Data attributes required for JS:
* none
Context variables required for this template:
* none
Example context (json):
{
}
}}
<div class="{{^bulkactions}} hidden{{/bulkactions}}">
<div class="insights-bulk-actions btn-group mt-3" role="group">
{{#bulkactions}}
{{> report_insights/bulk_action_button}}
{{/bulkactions}}
</div>
</div>
+18 -4
View File
@@ -31,17 +31,31 @@
Example context (json):
{
"sampleimage": "<a href=\"#\">Link</a>",
"sampledescription": "Sample description"
"sampledescription": "Sample description",
"actions": [
{
"classes": "",
"primary": {
"items": [{"rawhtml": "<p>View details</p>"}]
},
"secondary": {
"items": [{"rawhtml": "<p>Not useful</p>"}]
}
}
]
}
}}
<tr>
<td>
<tr data-prediction-id="{{predictionid}}" class="d-flex">
<td class="insight-checkbox-cell col-xs-1{{^bulkactions}} hidden{{/bulkactions}}">
{{{toggleslave}}}
</td>
<td class="col-xs-8">
{{#sampleimage}}
{{{sampleimage}}}
{{/sampleimage}}
{{{sampledescription}}}
</td>
<td>
<td class="col-xs-3{{^actions}} hidden{{/actions}}">
{{#actions}}
{{> core/action_menu}}
{{/actions}}
@@ -30,11 +30,24 @@
Example context (json):
{
"contextid": 123,
"modelid": 321,
"insightname": "Best insight ever",
"timecreated": "Thursday, 5 October 2017, 4:16 PM",
"timerange": "Monday, 4 September 2017, 6:00 PM to Thursday, 5 October 2017, 12:00 AM",
"sampleimage": "<a href=\"#\">Link</a>",
"sampledescription": "Sample description",
"actions": [
{
"classes": "",
"primary": {
"items": [{"rawhtml": "<p>View details</p>"}]
},
"secondary": {
"items": [{"rawhtml": "<p>Not useful</p>"}]
}
}
],
"style": "success",
"outcomeicon": {
"attributes": [
@@ -73,56 +86,64 @@
}
}}
<h2 class="mb-2">{{insightname}}</h2>
<table class="generaltable insights-list">
{{#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>
<th scope="col">{{#str}}actions{{/str}}</th>
</tr>
</thead>
<tbody>
{{> report_insights/insight}}
</tbody>
</table>
<div class="insight-container" data-context-id="{{contextid}}" data-model-id="{{modelid}}">
<table class="generaltable prediction-timedetails">
<caption>{{#str}}predictiondetails, report_insights{{/str}}</caption>
<tbody>
<tr>
<th scope="row">{{#str}}timecreated, report_insights{{/str}}</td>
<td>{{timecreated}}</td>
</tr>
{{#timerange}}
<tr>
<th scope="row">{{#str}}timerange, report_insights{{/str}}</td>
<td>{{.}}</td>
<h2 class="mb-2">{{insightname}}</h2>
{{> report_insights/bulk_actions }}
<table class="generaltable insights-list mt-3">
{{#showpredictionheading}}
<caption>
{{#str}}prediction, report_insights{{/str}}:
<span class="{{#style}}table-{{style}}{{/style}}">
{{#outcomeicon}}
{{> core/pix_icon}}
{{/outcomeicon}}
{{predictiondisplayvalue}}
</span>
</caption>
{{/showpredictionheading}}
<thead>
<tr class="d-flex">
<th scope="col" class="col-xs-1{{^bulkactions}} hidden{{/bulkactions}}">{{{hiddencheckboxtoggleall}}}</th>
<th scope="col" class="col-xs-8">{{#str}}description{{/str}}</th>
<th scope="col" class="col-xs-3{{^actions}} hidden{{/actions}}">{{#str}}actions{{/str}}</th>
</tr>
{{/timerange}}
</tbody>
</table>
<table class="generaltable prediction-calculations">
<caption>{{#str}}indicators, report_insights{{/str}}</caption>
<tbody>
{{#calculations}}
<tr>
<th scope="row" class="{{#style}}table-{{style}}{{/style}}">{{name}}</td>
<td class="{{#style}}table-{{style}}{{/style}}">{{#outcomeicon}}{{> core/pix_icon}}{{/outcomeicon}} {{displayvalue}}</td>
</tr>
{{/calculations}}
</tbody>
</table>
{{#nocalculations}}
{{> core/notification_info}}
{{/nocalculations}}
</thead>
<tbody>
{{> report_insights/insight}}
</tbody>
</table>
<table class="generaltable prediction-timedetails">
<caption>{{#str}}predictiondetails, report_insights{{/str}}</caption>
<tbody>
<tr>
<th scope="row">{{#str}}timecreated, report_insights{{/str}}</td>
<td>{{timecreated}}</td>
</tr>
{{#timerange}}
<tr>
<th scope="row">{{#str}}timerange, report_insights{{/str}}</td>
<td>{{.}}</td>
</tr>
{{/timerange}}
</tbody>
</table>
<table class="generaltable prediction-calculations">
<caption>{{#str}}indicators, report_insights{{/str}}</caption>
<tbody>
{{#calculations}}
<tr>
<th scope="row" class="{{#style}}table-{{style}}{{/style}}">{{name}}</td>
<td class="{{#style}}table-{{style}}{{/style}}">{{#outcomeicon}}{{> core/pix_icon}}{{/outcomeicon}} {{displayvalue}}</td>
</tr>
{{/calculations}}
</tbody>
</table>
{{#nocalculations}}
{{> core/notification_info}}
{{/nocalculations}}
</div>
@@ -30,10 +30,13 @@
Example context (json):
{
"contextid": 123,
"modelid": 321,
"insightname": "Best insight ever",
"nostaticmodelnotification": {
"message": "This is just a prediction."
},
"showpredictionheading": "true",
"predictions": [
{
"predictiondisplayvalue": "This dev will understand it",
@@ -43,10 +46,36 @@
{"name": "src", "value": "https://moodle.org/logo/moodle-logo.svg" }
]
},
"multiplepredictions": "true",
"insights": [
{
"sampleimage": "<a href=\"#\">Link</a>",
"sampledescription": "Sample description"
"sampledescription": "Sample description",
"actions": [
{
"classes": "",
"primary": {
"items": [{"rawhtml": "<p>View details</p>"}]
},
"secondary": {
"items": [{"rawhtml": "<p>Not useful</p>"}]
}
}
]
}, {
"sampleimage": "<a href=\"#\">Link</a>",
"sampledescription": "Sample description",
"actions": [
{
"classes": "",
"primary": {
"items": [{"rawhtml": "<p>View details</p>"}]
},
"secondary": {
"items": [{"rawhtml": "<p>Not useful</p>"}]
}
}
]
}
]
}, {
@@ -75,46 +104,68 @@
</div>
{{/modelselector}}
<h2 class="mb-2">{{{insightname}}}</h2>
<div class="insight-container" data-context-id="{{contextid}}" data-model-id="{{modelid}}">
{{^noinsights}}
{{#nostaticmodelnotification}}
<div class="mt-2">
{{> core/notification_info}}
</div>
{{/nostaticmodelnotification}}
<h2 class="mb-2">{{{insightname}}}</h2>
{{{ pagingbar }}}
{{#predictions}}
<table class="generaltable insights-list">
{{#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>
<th scope="col">{{#str}}actions{{/str}}</th>
</tr>
</thead>
{{#insights}}
<tbody>
{{> report_insights/insight}}
</tbody>
{{/insights}}
</table>
{{/predictions}}
{{{ pagingbar }}}
{{/noinsights}}
{{#noinsights}}
<div class="mt-2">
{{> core/notification_info}}
</div>
{{/noinsights}}
{{^noinsights}}
{{#nostaticmodelnotification}}
<div class="mt-2">
{{> core/notification_info}}
</div>
{{/nostaticmodelnotification}}
{{{ pagingbar }}}
{{> report_insights/bulk_actions}}
{{#predictions}}
<table class="generaltable insights-list mt-3">
{{#showpredictionheading}}
<caption>
{{#str}}prediction, report_insights{{/str}}:
<span class="{{#style}}table-{{style}}{{/style}}">
{{#outcomeicon}}
{{> core/pix_icon}}
{{/outcomeicon}}
{{predictiondisplayvalue}}
</span>
</caption>
{{/showpredictionheading}}
<thead>
<tr class="d-flex">
{{#multiplepredictions}}
<th class="col-xs-1{{^bulkactions}} hidden{{/bulkactions}}">
{{{checkboxtoggleall}}}
</th>
{{/multiplepredictions}}
{{^multiplepredictions}}
<th class="col-xs-1{{^bulkactions}} hidden{{/bulkactions}}">
{{/multiplepredictions}}
<th scope="col" class="col-xs-8">{{#str}}description{{/str}}</th>
<th scope="col" class="col-xs-3">{{#str}}actions{{/str}}</th>
</tr>
</thead>
<tbody>
{{#insights}}
{{> report_insights/insight}}
{{/insights}}
</tbody>
</table>
{{/predictions}}
{{#multiplepredictions}}
{{> report_insights/bulk_actions}}
{{/multiplepredictions}}
{{{ pagingbar }}}
{{/noinsights}}
{{#noinsights}}
<div class="mt-2">
{{> core/notification_info}}
</div>
{{/noinsights}}
</div>
+107
View File
@@ -0,0 +1,107 @@
<?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/>.
/**
* Unit tests for report_insights externallib.
*
* @package report_insights
* @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();
require_once(__DIR__ . '/../../../analytics/tests/fixtures/test_indicator_max.php');
require_once(__DIR__ . '/../../../analytics/tests/fixtures/test_target_shortname.php');
/**
* Unit tests for report_insights externallib.
*
* @package report_insights
* @copyright 2019 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class report_insights_external_testcase extends advanced_testcase {
/**
* test_action_executed
*/
public function test_action_executed() {
global $DB;
$this->setAdminUser();
$target = \core_analytics\manager::get_target('test_target_shortname');
$indicators = array('test_indicator_max');
foreach ($indicators as $key => $indicator) {
$indicators[$key] = \core_analytics\manager::get_indicator($indicator);
}
$model = \core_analytics\model::create($target, $indicators);
$modelobj = $model->get_model_obj();
$model->enable('\core\analytics\time_splitting\single_range');
$this->resetAfterTest(true);
$course1 = $this->getDataGenerator()->create_course();
$course2 = $this->getDataGenerator()->create_course();
$context = \context_course::instance($course1->id);
$teacher1 = $this->getDataGenerator()->create_user();
$teacher2 = $this->getDataGenerator()->create_user();
$this->getDataGenerator()->enrol_user($teacher1->id, $course1->id, 'editingteacher');
$this->getDataGenerator()->enrol_user($teacher2->id, $course1->id, 'editingteacher');
// The only relevant fields are modelid, contextid and sampleid. I'm cheating and setting
// contextid as the course context so teachers can access these predictions.
$pred = new \stdClass();
$pred->modelid = $model->get_id();
$pred->contextid = $context->id;
$pred->sampleid = $course1->id;
$pred->rangeindex = 1;
$pred->prediction = 1;
$pred->predictionscore = 1;
$pred->calculations = json_encode(array('test_indicator_max' => 1));
$pred->timecreated = time();
$DB->insert_record('analytics_predictions', $pred);
$pred->sampleid = $course2->id;
$DB->insert_record('analytics_predictions', $pred);
$this->assertEquals(0, $DB->count_records('analytics_prediction_actions'));
// Teacher 2 flags a prediction (it doesn't matter which one) as fixed.
$this->setUser($teacher2);
list($ignored, $predictions) = $model->get_predictions($context, true);
$prediction = reset($predictions);
\report_insights\external::action_executed(\core_analytics\prediction::ACTION_FIXED,
[$prediction->get_prediction_data()->id]);
$recordset = $model->get_prediction_actions($context);
$this->assertCount(1, $recordset);
$recordset->close();
$this->assertEquals(1, $DB->count_records('analytics_prediction_actions'));
$action = $DB->get_record('analytics_prediction_actions', array('userid' => $teacher2->id));
$this->assertEquals(\core_analytics\prediction::ACTION_FIXED, $action->actionname);
\report_insights\external::action_executed(\core_analytics\prediction::ACTION_INCORRECTLY_FLAGGED,
[$prediction->get_prediction_data()->id]);
$recordset = $model->get_prediction_actions($context);
$this->assertCount(2, $recordset);
$recordset->close();
$this->assertEquals(2, $DB->count_records('analytics_prediction_actions'));
}
}
+1 -1
View File
@@ -24,6 +24,6 @@
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2019052000; // The current plugin version (Date: YYYYMMDDXX).
$plugin->version = 2019052004; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2019051100; // Requires this Moodle version.
$plugin->component = 'report_insights'; // Full name of the plugin (used for diagnostics).
@@ -178,13 +178,13 @@ class upcoming_activities_due extends \core_analytics\local\target\binary {
* @param \context $context
* @param \stdClass $user
* @param \core_analytics\prediction $prediction
* @param \core_analytics\prediction_action[] $predictionactions Passed by reference to remove duplicate links to actions.
* @return array Plain text msg, HTML message and the main URL for this
* insight (you can return null if you are happy with the
* default insight URL calculated in prediction_info())
* @param \core_analytics\action[] $actions Passed by reference to remove duplicate links to actions.
* @return array Plain text msg, HTML message and the main URL for this
* insight (you can return null if you are happy with the
* default insight URL calculated in prediction_info())
*/
public function get_insight_body_for_prediction(\context $context, \stdClass $user, \core_analytics\prediction $prediction,
array &$predictionactions): array {
array &$actions) {
global $OUTPUT;
$fullmessageplaintext = get_string('youhaveupcomingactivitiesdueinfo', 'moodle', $user->firstname);
@@ -193,6 +193,8 @@ class upcoming_activities_due extends \core_analytics\local\target\binary {
$activitiesdue = $sampledata['core_course\analytics\indicator\activities_due:extradata'];
if (empty($activitiesdue)) {
// We can throw an exception here because this is a target based on assumptions and we require the
// activities_due indicator.
throw new \coding_exception('The activities_due indicator must be part of the model indicators.');
}
@@ -216,7 +218,7 @@ class upcoming_activities_due extends \core_analytics\local\target\binary {
$activitiestext[] = $activitydue->name . ': ' . $activitiesdue[$key]->url;
}
foreach ($predictionactions as $key => $action) {
foreach ($actions as $key => $action) {
if ($action->get_action_name() === 'viewupcoming') {
// Use it as the main URL of the insight if there are multiple activities due.
@@ -226,7 +228,7 @@ class upcoming_activities_due extends \core_analytics\local\target\binary {
// Remove the 'viewupcoming' action from the list of actions for this prediction as the action has
// been included in the link to the activity.
unset($predictionactions[$key]);
unset($actions[$key]);
break;
}
}
@@ -255,7 +257,7 @@ class upcoming_activities_due extends \core_analytics\local\target\binary {
$isinsightuser = false) {
global $CFG, $USER;
$parentactions = parent::prediction_actions($prediction, $includedetailsaction);
$parentactions = parent::prediction_actions($prediction, $includedetailsaction, $isinsightuser);
if (!$isinsightuser && $USER->id != $prediction->get_prediction_data()->sampleid) {
return $parentactions;
+1 -1
View File
@@ -29,7 +29,7 @@
defined('MOODLE_INTERNAL') || die();
$version = 2019092700.00; // YYYYMMDD = weekly release date of this DEV branch.
$version = 2019092700.01; // YYYYMMDD = weekly release date of this DEV branch.
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.