Merge branch 'MDL-62191_master' of git://github.com/dmonllao/moodle
This commit is contained in:
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -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') {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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}}}
|
||||
|
||||
@@ -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}}
|
||||
|
||||
@@ -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]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
);
|
||||
|
||||
|
||||
@@ -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}';
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user