diff --git a/analytics/classes/local/target/base.php b/analytics/classes/local/target/base.php
index 1a4f9786672..5b741d27678 100644
--- a/analytics/classes/local/target/base.php
+++ b/analytics/classes/local/target/base.php
@@ -112,18 +112,42 @@ abstract class base extends \core_analytics\calculable {
* @return \core_analytics\prediction_action[]
*/
public function prediction_actions(\core_analytics\prediction $prediction, $includedetailsaction = false) {
+ global $PAGE;
+
+ $predictionid = $prediction->get_prediction_data()->id;
+
+ $PAGE->requires->js_call_amd('report_insights/actions', 'init', array($predictionid));
+
$actions = array();
if ($includedetailsaction) {
$predictionurl = new \moodle_url('/report/insights/prediction.php',
- array('id' => $prediction->get_prediction_data()->id));
+ array('id' => $predictionid));
- $actions['predictiondetails'] = new \core_analytics\prediction_action('predictiondetails', $prediction,
+ $actions[] = new \core_analytics\prediction_action(\core_analytics\prediction::ACTION_PREDICTION_DETAILS, $prediction,
$predictionurl, new \pix_icon('t/preview', get_string('viewprediction', 'analytics')),
get_string('viewprediction', 'analytics'));
}
+ // Flag as 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);
+
+ // 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);
+
return $actions;
}
diff --git a/analytics/classes/model.php b/analytics/classes/model.php
index 8120dd691ae..dbee08afb29 100644
--- a/analytics/classes/model.php
+++ b/analytics/classes/model.php
@@ -1041,15 +1041,29 @@ class model {
/**
* Get the contexts with predictions.
*
+ * @param bool $skiphidden Skip hidden predictions
* @return \stdClass[]
*/
- public function get_predictions_contexts() {
- global $DB;
+ public function get_predictions_contexts($skiphidden = true) {
+ global $DB, $USER;
$sql = "SELECT DISTINCT ap.contextid FROM {analytics_predictions} ap
JOIN {context} ctx ON ctx.id = ap.contextid
- WHERE ap.modelid = ?";
- return $DB->get_records_sql($sql, array($this->model->id));
+ WHERE ap.modelid = :modelid";
+ $params = array('modelid' => $this->model->id);
+
+ if ($skiphidden) {
+ $sql .= " AND NOT EXISTS (
+ SELECT 1
+ FROM {analytics_prediction_actions} apa
+ WHERE apa.predictionid = ap.id AND apa.userid = :userid AND (apa.actionname = :fixed OR apa.actionname = :notuseful)
+ )";
+ $params['userid'] = $USER->id;
+ $params['fixed'] = \core_analytics\prediction::ACTION_FIXED;
+ $params['notuseful'] = \core_analytics\prediction::ACTION_NOT_USEFUL;
+ }
+
+ return $DB->get_records_sql($sql, $params);
}
/**
@@ -1096,12 +1110,13 @@ class model {
* Gets the predictions for this context.
*
* @param \context $context
+ * @param bool $skiphidden Skip hidden predictions
* @param int $page The page of results to fetch. False for all results.
* @param int $perpage The max number of results to fetch. Ignored if $page is false.
* @return array($total, \core_analytics\prediction[])
*/
- public function get_predictions(\context $context, $page = false, $perpage = 100) {
- global $DB;
+ public function get_predictions(\context $context, $skiphidden = true, $page = false, $perpage = 100) {
+ global $DB, $USER;
\core_analytics\manager::check_can_list_insights($context);
@@ -1111,12 +1126,27 @@ class model {
JOIN (
SELECT sampleid, max(rangeindex) AS rangeindex
FROM {analytics_predictions}
- WHERE modelid = ? and contextid = ?
+ WHERE modelid = :modelidsubap and contextid = :contextidsubap
GROUP BY sampleid
) apsub
ON ap.sampleid = apsub.sampleid AND ap.rangeindex = apsub.rangeindex
- WHERE ap.modelid = ? and ap.contextid = ?";
- $params = array($this->model->id, $context->id, $this->model->id, $context->id);
+ WHERE ap.modelid = :modelid and ap.contextid = :contextid";
+
+ $params = array('modelid' => $this->model->id, 'contextid' => $context->id,
+ 'modelidsubap' => $this->model->id, 'contextidsubap' => $context->id);
+
+ if ($skiphidden) {
+ $sql .= " AND NOT EXISTS (
+ SELECT 1
+ FROM {analytics_prediction_actions} apa
+ WHERE apa.predictionid = ap.id AND apa.userid = :userid AND (apa.actionname = :fixed OR apa.actionname = :notuseful)
+ )";
+ $params['userid'] = $USER->id;
+ $params['fixed'] = \core_analytics\prediction::ACTION_FIXED;
+ $params['notuseful'] = \core_analytics\prediction::ACTION_NOT_USEFUL;
+ }
+
+ $sql .= " ORDER BY ap.timecreated DESC";
if (!$predictions = $DB->get_records_sql($sql, $params)) {
return array();
}
diff --git a/analytics/classes/prediction.php b/analytics/classes/prediction.php
index f738f693dad..549795e816b 100644
--- a/analytics/classes/prediction.php
+++ b/analytics/classes/prediction.php
@@ -35,6 +35,21 @@ defined('MOODLE_INTERNAL') || die();
*/
class prediction {
+ /**
+ * Prediction details (one of the default prediction actions)
+ */
+ const ACTION_PREDICTION_DETAILS = 'predictiondetails';
+
+ /**
+ * Prediction not useful (one of the default prediction actions)
+ */
+ const ACTION_NOT_USEFUL = 'notuseful';
+
+ /**
+ * Prediction already fixed (one of the default prediction actions)
+ */
+ const ACTION_FIXED = 'fixed';
+
/**
* @var \stdClass
*/
@@ -97,6 +112,51 @@ class prediction {
return $this->calculations;
}
+ /**
+ * Stores the executed action.
+
+ * Prediction instances should be retrieved using \core_analytics\manager::get_prediction,
+ * It is the caller responsability to check that the user can see the prediction.
+ *
+ * @param string $actionname
+ * @param \core_analytics\local\target\base $target
+ */
+ public function action_executed($actionname, \core_analytics\local\target\base $target) {
+ global $USER, $DB;
+
+ $context = \context::instance_by_id($this->get_prediction_data()->contextid, IGNORE_MISSING);
+ if (!$context) {
+ throw new \moodle_exception('errorpredictioncontextnotavailable', 'analytics');
+ }
+
+ // Check that the provided action exists.
+ $actions = $target->prediction_actions($this, true);
+ foreach ($actions as $action) {
+ if ($action->get_action_name() === $actionname) {
+ $found = true;
+ }
+ }
+ if (empty($found)) {
+ throw new \moodle_exception('errorunknownaction', 'analytics');
+ }
+
+ $predictionid = $this->get_prediction_data()->id;
+
+ $action = new \stdClass();
+ $action->predictionid = $predictionid;
+ $action->userid = $USER->id;
+ $action->actionname = $actionname;
+ $action->timecreated = time();
+ $DB->insert_record('analytics_prediction_actions', $action);
+
+ $eventdata = array (
+ 'context' => $context,
+ 'objectid' => $predictionid,
+ 'other' => array('actionname' => $actionname)
+ );
+ \core\event\prediction_action_started::create($eventdata)->trigger();
+ }
+
/**
* format_calculations
*
diff --git a/analytics/classes/prediction_action.php b/analytics/classes/prediction_action.php
index db7e50ab46a..2fb4281ba50 100644
--- a/analytics/classes/prediction_action.php
+++ b/analytics/classes/prediction_action.php
@@ -35,36 +35,54 @@ defined('MOODLE_INTERNAL') || die();
*/
class prediction_action {
+ /**
+ * @var string
+ */
+ protected $actionname = null;
+
/**
* @var \action_menu_link
*/
protected $actionlink = null;
/**
- * __construct
+ * Prediction action constructor.
*
- * @param string $actionname
+ * @param string $actionname They should match a-zA-Z_0-9-, as we apply a PARAM_ALPHANUMEXT filter
* @param \core_analytics\prediction $prediction
* @param \moodle_url $actionurl
- * @param \pix_icon $icon
- * @param string $text
- * @param bool $primary
+ * @param \pix_icon $icon Link icon
+ * @param string $text Link text
+ * @param bool $primary Primary button or secondary.
+ * @param array $attributes Link attributes
* @return void
*/
- public function __construct($actionname, \core_analytics\prediction $prediction, \moodle_url $actionurl, \pix_icon $icon, $text, $primary = false) {
+ public function __construct($actionname, \core_analytics\prediction $prediction, \moodle_url $actionurl, \pix_icon $icon,
+ $text, $primary = false, $attributes = array()) {
+
+ $this->actionname = $actionname;
// We want to track how effective are our suggested actions, we pass users through a script that will log these actions.
- $params = array('action' => $actionname, 'predictionid' => $prediction->get_prediction_data()->id,
+ $params = array('action' => $this->actionname, 'predictionid' => $prediction->get_prediction_data()->id,
'forwardurl' => $actionurl->out(false));
$url = new \moodle_url('/report/insights/action.php', $params);
if ($primary === false) {
- $this->actionlink = new \action_menu_link_secondary($url, $icon, $text);
+ $this->actionlink = new \action_menu_link_secondary($url, $icon, $text, $attributes);
} else {
- $this->actionlink = new \action_menu_link_primary($url, $icon, $text);
+ $this->actionlink = new \action_menu_link_primary($url, $icon, $text, $attributes);
}
}
+ /**
+ * Returns the action name.
+ *
+ * @return string
+ */
+ public function get_action_name() {
+ return $this->actionname;
+ }
+
/**
* Returns the link to the action.
*
diff --git a/analytics/tests/prediction_actions_test.php b/analytics/tests/prediction_actions_test.php
new file mode 100644
index 00000000000..3acd0a16bdf
--- /dev/null
+++ b/analytics/tests/prediction_actions_test.php
@@ -0,0 +1,140 @@
+.
+
+/**
+ * Unit tests for prediction actions.
+ *
+ * @package core_analytics
+ * @copyright 2017 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__ . '/fixtures/test_indicator_max.php');
+require_once(__DIR__ . '/fixtures/test_target_shortname.php');
+
+/**
+ * Unit tests for prediction actions.
+ *
+ * @package core_analytics
+ * @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class analytics_prediction_actions_testcase extends advanced_testcase {
+
+ /**
+ * Common startup tasks
+ */
+ public function setUp() {
+ 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);
+ }
+
+ $this->model = \core_analytics\model::create($target, $indicators);
+ $this->modelobj = $this->model->get_model_obj();
+ $this->model->enable('\core\analytics\time_splitting\single_range');
+
+ $this->resetAfterTest(true);
+
+ $course1 = $this->getDataGenerator()->create_course();
+ $course2 = $this->getDataGenerator()->create_course();
+ $this->context = \context_course::instance($course1->id);
+
+ $this->teacher1 = $this->getDataGenerator()->create_user();
+ $this->teacher2 = $this->getDataGenerator()->create_user();
+
+ $this->getDataGenerator()->enrol_user($this->teacher1->id, $course1->id, 'editingteacher');
+ $this->getDataGenerator()->enrol_user($this->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 = $this->model->get_id();
+ $pred->contextid = $this->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);
+ }
+
+ /**
+ * test_get_predictions
+ */
+ public function test_action_executed() {
+ global $DB;
+
+ $this->assertEquals(0, $DB->count_records('analytics_prediction_actions'));
+
+ // Teacher 2 flags a prediction (it doesn't matter which one) as fixed.
+ $this->setUser($this->teacher2);
+ list($ignored, $predictions) = $this->model->get_predictions($this->context, true);
+ $prediction = reset($predictions);
+ $prediction->action_executed(\core_analytics\prediction::ACTION_FIXED, $this->model->get_target());
+
+ $this->assertEquals(1, $DB->count_records('analytics_prediction_actions'));
+ $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());
+ $this->assertEquals(2, $DB->count_records('analytics_prediction_actions'));
+ }
+
+ /**
+ * test_get_predictions
+ */
+ public function test_get_predictions() {
+
+ // Already logged in as admin.
+ list($ignored, $predictions) = $this->model->get_predictions($this->context, true);
+ $this->assertCount(2, $predictions);
+
+ $this->setUser($this->teacher1);
+ list($ignored, $predictions) = $this->model->get_predictions($this->context, true);
+ $this->assertCount(2, $predictions);
+
+ $this->setUser($this->teacher2);
+ 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.
+ $prediction = reset($predictions);
+ $prediction->action_executed(\core_analytics\prediction::ACTION_FIXED, $this->model->get_target());
+
+ list($ignored, $predictions) = $this->model->get_predictions($this->context, true);
+ $this->assertCount(1, $predictions);
+ list($ignored, $predictions) = $this->model->get_predictions($this->context, false);
+ $this->assertCount(2, $predictions);
+
+ // Teacher 1 can still see both predictions.
+ $this->setUser($this->teacher1);
+ list($ignored, $predictions) = $this->model->get_predictions($this->context, true);
+ $this->assertCount(2, $predictions);
+ list($ignored, $predictions) = $this->model->get_predictions($this->context, false);
+ $this->assertCount(2, $predictions);
+ }
+}
diff --git a/lang/en/analytics.php b/lang/en/analytics.php
index b127fcbfc15..17d12c040ee 100644
--- a/lang/en/analytics.php
+++ b/lang/en/analytics.php
@@ -53,6 +53,8 @@ $string['errorunexistingtimesplitting'] = 'The selected time-splitting method is
$string['errorunexistingmodel'] = 'Non-existing model {$a}';
$string['errorunknownaction'] = 'Unknown action';
$string['eventpredictionactionstarted'] = 'Prediction process started';
+$string['eventinsightsviewed'] = 'Insights viewed';
+$string['fixedack'] = 'Acknowledged / fixed';
$string['insightmessagesubject'] = 'New insight for "{$a->contextname}": {$a->insightname}';
$string['insightinfomessage'] = 'The system generated some insights for you: {$a}';
$string['insightinfomessagehtml'] = 'The system generated some insights for you: {$a}.';
@@ -71,6 +73,7 @@ $string['nonewtimeranges'] = 'No new time ranges; nothing to predict.';
$string['nopredictionsyet'] = 'No predictions available yet';
$string['noranges'] = 'No predictions yet';
$string['notrainingbasedassumptions'] = 'Models based on assumptions do not need training';
+$string['notuseful'] = 'Not useful';
$string['novaliddata'] = 'No valid data available';
$string['novalidsamples'] = 'No valid samples available';
$string['onlycli'] = 'Analytics processes execution via command line only';
diff --git a/lib/classes/analytics/target/course_dropout.php b/lib/classes/analytics/target/course_dropout.php
index b7736320c6a..baac51d3060 100644
--- a/lib/classes/analytics/target/course_dropout.php
+++ b/lib/classes/analytics/target/course_dropout.php
@@ -61,25 +61,27 @@ class course_dropout extends \core_analytics\local\target\binary {
public function prediction_actions(\core_analytics\prediction $prediction, $includedetailsaction = false) {
global $USER;
- $actions = parent::prediction_actions($prediction, $includedetailsaction);
+ $actions = array();
$sampledata = $prediction->get_sample_data();
$studentid = $sampledata['user']->id;
+ $attrs = array('target' => '_blank');
+
// Send a message.
$url = new \moodle_url('/message/index.php', array('user' => $USER->id, 'id' => $studentid));
$pix = new \pix_icon('t/message', get_string('sendmessage', 'message'));
- $actions['studentmessage'] = new \core_analytics\prediction_action('studentmessage', $prediction, $url, $pix,
- get_string('sendmessage', 'message'));
+ $actions[] = new \core_analytics\prediction_action('studentmessage', $prediction, $url, $pix,
+ get_string('sendmessage', 'message'), $attrs);
// View outline report.
$url = new \moodle_url('/report/outline/user.php', array('id' => $studentid, 'course' => $sampledata['course']->id,
'mode' => 'outline'));
$pix = new \pix_icon('i/report', get_string('outlinereport'));
- $actions['viewoutlinereport'] = new \core_analytics\prediction_action('viewoutlinereport', $prediction, $url, $pix,
- get_string('outlinereport'));
+ $actions[] = new \core_analytics\prediction_action('viewoutlinereport', $prediction, $url, $pix,
+ get_string('outlinereport'), $attrs);
- return $actions;
+ return array_merge($actions, parent::prediction_actions($prediction, $includedetailsaction));
}
/**
diff --git a/lib/classes/analytics/target/no_teaching.php b/lib/classes/analytics/target/no_teaching.php
index bacd39a7897..739670e31c4 100644
--- a/lib/classes/analytics/target/no_teaching.php
+++ b/lib/classes/analytics/target/no_teaching.php
@@ -67,25 +67,28 @@ class no_teaching extends \core_analytics\local\target\binary {
require_once($CFG->dirroot . '/course/lib.php');
- // No need to call the parent as the parent's action is view details and this target only have 1 feature.
- $actions = array();
-
$sampledata = $prediction->get_sample_data();
$course = $sampledata['course'];
+ $actions = array();
+
$url = new \moodle_url('/course/view.php', array('id' => $course->id));
$pix = new \pix_icon('i/course', get_string('course'));
- $actions['viewcourse'] = new \core_analytics\prediction_action('viewcourse', $prediction,
+ $actions[] = new \core_analytics\prediction_action('viewcourse', $prediction,
$url, $pix, get_string('view'));
if (course_can_view_participants($sampledata['context'])) {
$url = new \moodle_url('/user/index.php', array('id' => $course->id));
$pix = new \pix_icon('i/cohort', get_string('participants'));
- $actions['viewparticipants'] = new \core_analytics\prediction_action('viewparticipants', $prediction,
+ $actions[] = new \core_analytics\prediction_action('viewparticipants', $prediction,
$url, $pix, get_string('participants'));
}
- return $actions;
+ $parentactions = parent::prediction_actions($prediction, $includedetailsaction);
+ // No need to show details as there is only 1 indicator.
+ unset($parentactions[\core_analytics\prediction::ACTION_PREDICTION_DETAILS]);
+
+ return array_merge($actions, $parentactions);
}
/**
diff --git a/lib/classes/event/insights_viewed.php b/lib/classes/event/insights_viewed.php
new file mode 100644
index 00000000000..357474e9057
--- /dev/null
+++ b/lib/classes/event/insights_viewed.php
@@ -0,0 +1,79 @@
+.
+
+/**
+ * Insights page viewed event.
+ *
+ * @property-read array $other {
+ * Extra information about event.
+ *
+ * - string modelid: The model id
+ * }
+ *
+ * @package core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Event triggered after a user views the insights page.
+ *
+ * @package core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class insights_viewed extends \core\event\base {
+
+ /**
+ * Set basic properties for the event.
+ */
+ protected function init() {
+ $this->data['crud'] = 'r';
+ // It depends on the insight really.
+ $this->data['edulevel'] = self::LEVEL_OTHER;
+ }
+
+ /**
+ * Returns localised general event name.
+ *
+ * @return string
+ */
+ public static function get_name() {
+ return get_string('eventinsightsviewed', 'analytics');
+ }
+
+ /**
+ * Returns non-localised event description with id's for admin use only.
+ *
+ * @return string
+ */
+ public function get_description() {
+ return "The user with id '$this->userid' has viewed model '{$this->other['modelid']}' insights in " .
+ "context with id '{$this->data['contextid']}'";
+ }
+
+ /**
+ * Returns relevant URL.
+ * @return \moodle_url
+ */
+ public function get_url() {
+ return new \moodle_url('/report/insights/insights.php', array('modelid' => $this->other['modelid'],
+ 'contextid' => $this->data['contextid']));
+ }
+}
diff --git a/lib/db/install.xml b/lib/db/install.xml
index d35b5a8e79f..7e4b4f36254 100644
--- a/lib/db/install.xml
+++ b/lib/db/install.xml
@@ -1,5 +1,5 @@
-
@@ -3699,5 +3699,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php
index 945b3bf79fe..5a96d9f1260 100644
--- a/lib/db/upgrade.php
+++ b/lib/db/upgrade.php
@@ -2436,5 +2436,35 @@ function xmldb_main_upgrade($oldversion) {
upgrade_main_savepoint(true, 2017082800.00);
}
+ if ($oldversion < 2017090700.01) {
+
+ // Define table analytics_prediction_actions to be created.
+ $table = new xmldb_table('analytics_prediction_actions');
+
+ // Adding fields to table analytics_prediction_actions.
+ $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+ $table->add_field('predictionid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+ $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+ $table->add_field('actionname', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null);
+ $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+
+ // Adding keys to table analytics_prediction_actions.
+ $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+ $table->add_key('predictionid', XMLDB_KEY_FOREIGN, array('predictionid'), 'analytics_predictions', array('id'));
+ $table->add_key('userid', XMLDB_KEY_FOREIGN, array('userid'), 'user', array('id'));
+
+ // Adding indexes to table analytics_prediction_actions.
+ $table->add_index('predictionidanduseridandactionname', XMLDB_INDEX_NOTUNIQUE,
+ array('predictionid', 'userid', 'actionname'));
+
+ // Conditionally launch create table for analytics_prediction_actions.
+ if (!$dbman->table_exists($table)) {
+ $dbman->create_table($table);
+ }
+
+ // Main savepoint reached.
+ upgrade_main_savepoint(true, 2017090700.01);
+ }
+
return true;
}
diff --git a/report/insights/action.php b/report/insights/action.php
index b9f04912ed9..6f2d333e0a8 100644
--- a/report/insights/action.php
+++ b/report/insights/action.php
@@ -34,16 +34,15 @@ if ($context->contextlevel < CONTEXT_COURSE) {
$PAGE->set_context($context);
}
+if (empty($forwardurl)) {
+ $params = array('modelid' => $model->get_id(), 'contextid' => $context->id);
+ $forwardurl = new \moodle_url('/report/insights/insights.php', $params);
+}
+
$params = array('predictionid' => $prediction->get_prediction_data()->id, 'action' => $actionname, 'forwardurl' => $forwardurl);
$url = new \moodle_url('/report/insights/action.php', $params);
$PAGE->set_url($url);
-// Check that the provided action exists.
-$actions = $model->get_target()->prediction_actions($prediction, true);
-if (!isset($actions[$actionname])) {
- throw new \moodle_exception('errorunknownaction', 'analytics');
-}
-
$modelready = $model->is_enabled() && $model->is_trained() && $model->predictions_exist($context);
if (!$modelready) {
@@ -58,11 +57,6 @@ if (!$modelready) {
exit(0);
}
-$eventdata = array (
- 'context' => $context,
- 'objectid' => $predictionid,
- 'other' => array('actionname' => $actionname)
-);
-\core\event\prediction_action_started::create($eventdata)->trigger();
+$prediction->action_executed($actionname, $model->get_target());
redirect($forwardurl);
diff --git a/report/insights/amd/build/actions.min.js b/report/insights/amd/build/actions.min.js
new file mode 100644
index 00000000000..59d50353d90
--- /dev/null
+++ b/report/insights/amd/build/actions.min.js
@@ -0,0 +1 @@
+define(["jquery","core/ajax","core/notification"],function(a,b,c){return{init:function(d){a("a[data-prediction-methodname][data-prediction-id="+d+"]").on("click",function(e){e.preventDefault();var f=a(e.currentTarget),g=f.attr("data-prediction-methodname"),h=f.closest("tr");if(h.length>0){var i=b.call([{methodname:g,args:{predictionid:d}}])[0];i.done(function(){h[0].remove(),0===a(".insights-list tr").length&&window.history.back()}).fail(c.exception)}})}}});
\ No newline at end of file
diff --git a/report/insights/amd/src/actions.js b/report/insights/amd/src/actions.js
new file mode 100644
index 00000000000..2ffe7f27509
--- /dev/null
+++ b/report/insights/amd/src/actions.js
@@ -0,0 +1,67 @@
+// 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 .
+
+/**
+ * Module to manage report insights actions that are executed using AJAX.
+ *
+ * @package report_insights
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/**
+ * This module manages prediction actions that require AJAX requests.
+ *
+ * @module report_insights/actions
+ */
+define(['jquery', 'core/ajax', 'core/notification'], function($, Ajax, Notification) {
+
+ return {
+
+ /**
+ * Attach on click handlers to hide predictions.
+ *
+ * @param {Number} predictionId The prediction id.
+ * @access public
+ */
+ init: function(predictionId) {
+
+ // 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) {
+ e.preventDefault();
+ var action = $(e.currentTarget);
+ var methodname = action.attr('data-prediction-methodname');
+ var predictionContainers = action.closest('tr');
+
+ if (predictionContainers.length > 0) {
+ var promise = Ajax.call([
+ {
+ methodname: methodname,
+ args: {predictionid: predictionId}
+ }
+ ])[0];
+ promise.done(function() {
+ predictionContainers[0].remove();
+
+ // Move back if no remaining predictions.
+ if ($('.insights-list tr').length === 0) {
+ window.history.back();
+ }
+ }).fail(Notification.exception);
+ }
+ });
+ }
+ };
+});
diff --git a/report/insights/classes/external.php b/report/insights/classes/external.php
new file mode 100644
index 00000000000..44e61232b8f
--- /dev/null
+++ b/report/insights/classes/external.php
@@ -0,0 +1,155 @@
+.
+
+/**
+ * This is the external API for this component.
+ *
+ * @package report_insights
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace report_insights;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once("$CFG->libdir/externallib.php");
+
+use external_api;
+use external_function_parameters;
+use external_value;
+use external_single_structure;
+use external_warnings;
+
+/**
+ * This is the external API for this component.
+ *
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class external extends external_api {
+
+ /**
+ * set_notuseful_prediction parameters.
+ *
+ * @return external_function_parameters
+ * @since Moodle 3.4
+ */
+ public static function set_notuseful_prediction_parameters() {
+ return new external_function_parameters(
+ array(
+ 'predictionid' => new external_value(PARAM_INT, 'The prediction id', VALUE_REQUIRED)
+ )
+ );
+ }
+
+ /**
+ * Flags a prediction as fixed so no need to display it any more.
+ *
+ * @param int $predictionid
+ * @return array an array of warnings and a boolean
+ * @since Moodle 3.4
+ */
+ public static function set_notuseful_prediction($predictionid) {
+
+ $params = self::validate_parameters(self::set_notuseful_prediction_parameters(), array('predictionid' => $predictionid));
+
+ list($model, $prediction, $context) = self::validate_prediction($params['predictionid']);
+
+ $prediction->action_executed(\core_analytics\prediction::ACTION_NOT_USEFUL, $model->get_target());
+
+ $success = true;
+ return array('success' => $success, 'warnings' => array());
+ }
+
+ /**
+ * set_notuseful_prediction return
+ *
+ * @return external_description
+ * @since Moodle 3.4
+ */
+ public static function set_notuseful_prediction_returns() {
+ return new external_single_structure(
+ array(
+ 'success' => new external_value(PARAM_BOOL, 'True if the prediction was successfully flagged as not useful.'),
+ 'warnings' => new external_warnings(),
+ )
+ );
+ }
+
+ /**
+ * set_fixed_prediction parameters.
+ *
+ * @return external_function_parameters
+ * @since Moodle 3.4
+ */
+ public static function set_fixed_prediction_parameters() {
+ return new external_function_parameters(
+ array(
+ 'predictionid' => new external_value(PARAM_INT, 'The prediction id', VALUE_REQUIRED)
+ )
+ );
+ }
+
+ /**
+ * Flags a prediction as fixed so no need to display it any more.
+ *
+ * @param int $predictionid
+ * @return array an array of warnings and a boolean
+ * @since Moodle 3.4
+ */
+ public static function set_fixed_prediction($predictionid) {
+
+ $params = self::validate_parameters(self::set_fixed_prediction_parameters(), array('predictionid' => $predictionid));
+
+ list($model, $prediction, $context) = self::validate_prediction($params['predictionid']);
+
+ $prediction->action_executed(\core_analytics\prediction::ACTION_FIXED, $model->get_target());
+
+ $success = true;
+ return array('success' => $success, 'warnings' => array());
+ }
+
+ /**
+ * set_fixed_prediction return
+ *
+ * @return external_description
+ * @since Moodle 3.4
+ */
+ public static function set_fixed_prediction_returns() {
+ return new external_single_structure(
+ array(
+ 'success' => new external_value(PARAM_BOOL, 'True if the prediction was successfully flagged as fixed.'),
+ 'warnings' => new external_warnings(),
+ )
+ );
+ }
+
+ /**
+ * Validates access to the prediction and returns it.
+ *
+ * @param int $predictionid
+ * @return array array($model, $prediction, $context)
+ */
+ protected static function validate_prediction($predictionid) {
+
+ list($model, $prediction, $context) = \core_analytics\manager::get_prediction($predictionid);
+
+ self::validate_context($context);
+
+ return array($model, $prediction, $context);
+ }
+}
diff --git a/report/insights/classes/output/insights_list.php b/report/insights/classes/output/insights_list.php
index 6596750c308..1da683eb2b7 100644
--- a/report/insights/classes/output/insights_list.php
+++ b/report/insights/classes/output/insights_list.php
@@ -93,7 +93,7 @@ class insights_list implements \renderable, \templatable {
$total = 0;
if ($this->model->uses_insights()) {
- $predictionsdata = $this->model->get_predictions($this->context, $this->page, $this->perpage);
+ $predictionsdata = $this->model->get_predictions($this->context, true, $this->page, $this->perpage);
$data->insights = array();
if ($predictionsdata) {
diff --git a/report/insights/db/services.php b/report/insights/db/services.php
new file mode 100644
index 00000000000..161954baac9
--- /dev/null
+++ b/report/insights/db/services.php
@@ -0,0 +1,47 @@
+.
+
+/**
+ * Report insights webservice definitions.
+ *
+ * @package report_insights
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$functions = array(
+
+ 'report_insights_set_notuseful_prediction' => array(
+ 'classname' => 'report_insights\external',
+ 'methodname' => 'set_notuseful_prediction',
+ 'description' => 'Flags the prediction as not useful.',
+ 'type' => 'write',
+ 'ajax' => true,
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
+ ),
+
+ 'report_insights_set_fixed_prediction' => array(
+ 'classname' => 'report_insights\external',
+ 'methodname' => 'set_fixed_prediction',
+ 'description' => 'Flags a prediction as fixed.',
+ 'type' => 'write',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
+ 'ajax' => true,
+ )
+);
+
diff --git a/report/insights/insights.php b/report/insights/insights.php
index 5e94ff5c993..452f9d9e6bd 100644
--- a/report/insights/insights.php
+++ b/report/insights/insights.php
@@ -111,4 +111,10 @@ echo $OUTPUT->header();
$renderable = new \report_insights\output\insights_list($model, $context, $othermodels, $page, $perpage);
echo $renderer->render($renderable);
+$eventdata = array (
+ 'context' => $context,
+ 'other' => array('modelid' => $model->get_id())
+);
+\core\event\insights_viewed::create($eventdata)->trigger();
+
echo $OUTPUT->footer();
diff --git a/report/insights/version.php b/report/insights/version.php
index 8529b85ced5..abcc897abfe 100644
--- a/report/insights/version.php
+++ b/report/insights/version.php
@@ -24,6 +24,6 @@
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2017051501; // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version = 2017051502; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2017050500; // Requires this Moodle version.
$plugin->component = 'report_insights'; // Full name of the plugin (used for diagnostics).
diff --git a/version.php b/version.php
index 5460083f927..803b74a757b 100644
--- a/version.php
+++ b/version.php
@@ -29,7 +29,7 @@
defined('MOODLE_INTERNAL') || die();
-$version = 2017090700.00; // YYYYMMDD = weekly release date of this DEV branch.
+$version = 2017090700.01; // YYYYMMDD = weekly release date of this DEV branch.
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.