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.