Merge branch 'MDL-59840_master' of git://github.com/dmonllao/moodle
authorDan Poltawski <dan@moodle.com>
Mon, 4 Sep 2017 10:24:54 +0000 (11:24 +0100)
committerDavid Monllao <davidm@moodle.com>
Thu, 7 Sep 2017 08:50:13 +0000 (10:50 +0200)
43 files changed:
admin/settings/analytics.php
admin/tool/analytics/classes/output/models_list.php
admin/tool/analytics/lang/en/tool_analytics.php
admin/tool/analytics/model.php
admin/tool/analytics/templates/models_list.mustache
analytics/classes/model.php
calendar/amd/build/calendar.min.js
calendar/amd/src/calendar.js
calendar/renderer.php
calendar/templates/event_summary_modal.mustache
calendar/tests/behat/calendar.feature
lang/en/analytics.php
lang/en/calendar.php
lib/amd/build/permissionmanager.min.js
lib/amd/src/permissionmanager.js
lib/db/access.php
lib/filestorage/file_system.php
lib/filestorage/file_system_filedir.php
message/classes/search/base_message.php
message/tests/search_test_received.php
message/tests/search_test_sent.php
report/completion/index.php
report/insights/action.php
report/insights/classes/output/insight.php
report/insights/classes/output/insights_list.php
report/insights/classes/output/renderer.php
report/insights/insights.php
report/insights/lang/en/report_insights.php
report/insights/lib.php
report/insights/prediction.php
report/insights/settings.php [new file with mode: 0644]
report/insights/templates/insight.mustache
report/insights/templates/insight_details.mustache
report/insights/templates/insights_list.mustache
report/insights/version.php
report/log/index.php
theme/bootstrapbase/less/moodle/calendar.less
theme/bootstrapbase/style/moodle.css
theme/bootstrapbase/templates/report_insights/insight.mustache [new file with mode: 0644]
theme/bootstrapbase/templates/report_insights/insight_details.mustache [new file with mode: 0644]
theme/bootstrapbase/templates/report_insights/insights_list.mustache [new file with mode: 0644]
version.php
webservice/lib.php

index d55506f..3d28719 100644 (file)
@@ -97,5 +97,9 @@ if ($hassiteconfig) {
         }
         $settings->add(new admin_setting_configdirectory('analytics/modeloutputdir', new lang_string('modeloutputdir', 'analytics'),
             new lang_string('modeloutputdirinfo', 'analytics'), $defaultmodeloutputdir));
+
+        // Disable web interface evaluation and get predictions.
+        $settings->add(new admin_setting_configcheckbox('analytics/onlycli', new lang_string('onlycli', 'analytics'),
+            new lang_string('onlycliinfo', 'analytics'), 1));
     }
 }
index c068b18..b05e102 100644 (file)
@@ -62,6 +62,12 @@ class models_list implements \renderable, \templatable {
 
         $data = new \stdClass();
 
+        $onlycli = get_config('analytics', 'onlycli');
+        if ($onlycli === false) {
+            // Default applied if no config found.
+            $onlycli = 1;
+        }
+
         $data->models = array();
         foreach ($this->models as $model) {
             $modeldata = $model->export();
@@ -115,7 +121,9 @@ class models_list implements \renderable, \templatable {
             }
 
             // Model predictions list.
-            if ($model->uses_insights()) {
+            if (!$model->is_enabled()) {
+                $modeldata->noinsights = get_string('disabledmodel', 'analytics');
+            } else if ($model->uses_insights()) {
                 $predictioncontexts = $model->get_predictions_contexts();
                 if ($predictioncontexts) {
 
@@ -180,7 +188,7 @@ class models_list implements \renderable, \templatable {
             $actionsmenu->add($icon);
 
             // Evaluate machine-learning-based models.
-            if ($model->get_indicators() && !$model->is_static()) {
+            if (!$onlycli && $model->get_indicators() && !$model->is_static()) {
                 $url = new \moodle_url('model.php', array('action' => 'evaluate', 'id' => $model->get_id()));
                 $icon = new \action_menu_link_secondary($url, new \pix_icon('i/calc', get_string('evaluate', 'tool_analytics')),
                     get_string('evaluate', 'tool_analytics'));
@@ -188,7 +196,7 @@ class models_list implements \renderable, \templatable {
             }
 
             // Get predictions.
-            if ($modeldata->enabled && !empty($modeldata->timesplitting)) {
+            if (!$onlycli && $modeldata->enabled && !empty($modeldata->timesplitting)) {
                 $url = new \moodle_url('model.php', array('action' => 'getpredictions', 'id' => $model->get_id()));
                 $icon = new \action_menu_link_secondary($url, new \pix_icon('i/notifications',
                     get_string('getpredictions', 'tool_analytics')), get_string('getpredictions', 'tool_analytics'));
@@ -216,9 +224,18 @@ class models_list implements \renderable, \templatable {
             $data->models[] = $modeldata;
         }
 
-        $data->warnings = array(
-            (object)array('message' => get_string('bettercli', 'tool_analytics'), 'closebutton' => true)
-        );
+        if (!$onlycli) {
+            $data->warnings = array(
+                (object)array('message' => get_string('bettercli', 'tool_analytics'), 'closebutton' => true)
+            );
+        } else {
+            $url = new \moodle_url('/admin/settings.php', array('section' => 'analyticssettings'),
+                'id_s_analytics_onlycli');
+            $data->infos = array(
+                (object)array('message' => get_string('clievaluationandpredictions', 'tool_analytics', $url->out()),
+                    'closebutton' => true)
+            );
+        }
 
         return $data;
     }
index 4922a51..7ab2785 100644 (file)
@@ -30,6 +30,7 @@ $string['bettercli'] = 'Evaluating models and generating predictions may involve
 $string['cantguessstartdate'] = 'Can\'t guess the start date';
 $string['cantguessenddate'] = 'Can\'t guess the end date';
 $string['clienablemodel'] = 'You can enable the model by selecting a time splitting method by its id. Note that you can also enable it later using the web interface (\'none\' to exit)';
+$string['clievaluationandpredictions'] = 'A cron task iterates through enabled models and gets predictions. Models evaluation via command line is disabled. You can allow these processes to be executed manually via web interface by enabling <a href="{$a}">\'onlycli\' analytics setting</a>';
 $string['editmodel'] = 'Edit "{$a}" model';
 $string['edittrainedwarning'] = 'This model has already been trained, note that changing its indicators or its time splitting method will delete its previous predictions and start generating the new ones';
 $string['enabled'] = 'Enabled';
@@ -40,6 +41,7 @@ $string['errornoexport'] = 'Only trained models can be exported';
 $string['errornostaticedit'] = 'Models based on assumptions can not be edited';
 $string['errornostaticevaluated'] = 'Models based on assumptions can not be evaluated, they are always 100% correct according to how they were defined';
 $string['errornostaticlog'] = 'Models based on assumptions can not be evaluated, there is no preformance log';
+$string['erroronlycli'] = 'Execution only allowed via command line';
 $string['errortrainingdataexport'] = 'The model training data could not be exported';
 $string['evaluate'] = 'Evaluate';
 $string['evaluatemodel'] = 'Evaluate model';
index daf62cf..b2268f1 100644 (file)
@@ -71,6 +71,12 @@ $PAGE->set_pagelayout('report');
 $PAGE->set_title($title);
 $PAGE->set_heading($title);
 
+$onlycli = get_config('analytics', 'onlycli');
+if ($onlycli === false) {
+    // Default applied if no config found.
+    $onlycli = 1;
+}
+
 switch ($action) {
 
     case 'enable':
@@ -131,6 +137,10 @@ switch ($action) {
             throw new moodle_exception('errornostaticevaluate', 'tool_analytics');
         }
 
+        if ($onlycli) {
+            throw new moodle_exception('erroronlycli', 'tool_analytics');
+        }
+
         // Web interface is used by people who can not use CLI nor code stuff, always use
         // cached stuff as they will change the model through the web interface as well
         // which invalidates the previously analysed stuff.
@@ -142,6 +152,10 @@ switch ($action) {
     case 'getpredictions':
         echo $OUTPUT->header();
 
+        if ($onlycli) {
+            throw new moodle_exception('erroronlycli', 'tool_analytics');
+        }
+
         $trainresults = $model->train();
         $trainlogs = $model->get_analyser()->get_logs();
 
index 49e4a91..9330c54 100644 (file)
 {{#warnings}}
     {{> core/notification_warning}}
 {{/warnings}}
+{{#infos}}
+    {{> core/notification_info}}
+{{/infos}}
+
 <div class="box">
     <table class="generaltable fullwidth">
         <caption>{{#str}}modelslist, tool_analytics{{/str}}</caption>
index 568dcbf..8120dd6 100644 (file)
@@ -443,7 +443,12 @@ class model {
 
             // Reset trained flag.
             $this->model->trained = 0;
+
+        } else if ($this->model->enabled != $enabled) {
+            // We purge the cached contexts with insights as some will not be visible anymore.
+            $this->purge_insights_cache();
         }
+
         $this->model->enabled = intval($enabled);
         $this->model->indicators = $indicatorsstr;
         $this->model->timesplitting = $timesplittingid;
@@ -971,6 +976,13 @@ class model {
             $this->model->timesplitting = $timesplittingid;
             $this->model->version = $now;
         }
+
+        // Purge pages with insights as this may change things.
+        if ($timesplittingid && $timesplittingid !== $this->model->timesplitting ||
+                $this->model->enabled != 1) {
+            $this->purge_insights_cache();
+        }
+
         $this->model->enabled = 1;
         $this->model->timemodified = $now;
 
@@ -1375,6 +1387,13 @@ class model {
 
         // We don't expect people to clear models regularly and the cost of filling the cache is
         // 1 db read per context.
+        $this->purge_insights_cache();
+    }
+
+    /**
+     * Purges the insights cache.
+     */
+    private function purge_insights_cache() {
         $cache = \cache::make('core', 'contextwithinsights');
         $cache->purge();
     }
index 9d29445..212a648 100644 (file)
Binary files a/calendar/amd/build/calendar.min.js and b/calendar/amd/build/calendar.min.js differ
index ea10975..a5e69c8 100644 (file)
@@ -140,7 +140,9 @@ define([
                 body: Templates.render('core_calendar/event_summary_body', eventData),
                 templateContext: {
                     canedit: eventData.canedit,
-                    candelete: eventData.candelete
+                    candelete: eventData.candelete,
+                    isactionevent: eventData.isactionevent,
+                    url: eventData.url
                 }
             };
 
index c5b8531..e3cc6fc 100644 (file)
@@ -281,7 +281,7 @@ class core_calendar_renderer extends plugin_renderer_base {
             $output .= html_writer::tag('div', $source, array('class' => 'subscription'));
         }
         if (!empty($event->courselink)) {
-            $output .= html_writer::tag('div', $event->courselink, array('class' => 'course'));
+            $output .= html_writer::tag('div', $event->courselink);
         }
         if (!empty($event->time)) {
             $output .= html_writer::tag('span', $event->time, array('class' => 'date pull-xs-right m-r-1'));
@@ -291,7 +291,8 @@ class core_calendar_renderer extends plugin_renderer_base {
         }
 
         if (!empty($event->actionurl)) {
-            $output .= html_writer::tag('div', html_writer::link(new moodle_url($event->actionurl), $event->actionname));
+            $actionlink = html_writer::link(new moodle_url($event->actionurl), $event->actionname);
+            $output .= html_writer::tag('div', $actionlink, ['class' => 'action']);
         }
 
         $output .= $this->output->box_end();
index 845331a..583f500 100644 (file)
@@ -32,5 +32,8 @@
     {{/candelete}}
     <button type="button" class="btn btn-primary" data-action="edit">{{#str}} edit {{/str}}</button>
 {{/canedit}}
+{{#isactionevent}}
+    <a href="{{url}}">{{#str}} gotoactivity, core_calendar {{/str}}</a>
+{{/isactionevent}}
 {{/footer}}
 {{/ core/modal }}
index 5f2cf5e..efc236f 100644 (file)
@@ -124,3 +124,30 @@ Feature: Perform basic calendar functionality
       | Description | Wait, this event isn't that great. |
     And I press "Save"
     Then I should see "Mediocre event"
+
+  @javascript
+  Scenario: Module events editing
+    Given I log in as "admin"
+    And I am on "Course 1" course homepage with editing mode on
+    And the following "activities" exist:
+      | activity | course | idnumber | name        | intro                   | timeopen      | timeclose     |
+      | choice   | C1     | choice1  | Test choice | Test choice description | ##today## | ##today##  |
+    When I follow "This month"
+    Then I should see "Choice Test choice open"
+    And I should see "Choice Test choice close"
+    When I click on "Choice Test choice open" "link"
+    Then "Delete" "button" should not exist
+    And "Edit" "button" should not exist
+    And I should see "Open event"
+    When I click on "Go to activity" "link"
+    And I wait to be redirected
+    Then I should see "Test choice"
+    And I am on "Course 1" course homepage
+    And I follow "This month"
+    When I click on "Choice Test choice close" "link"
+    Then "Delete" "button" should not exist
+    And "Edit" "button" should not exist
+    And I should see "Close event"
+    When I click on "Go to activity" "link"
+    And I wait to be redirected
+    Then I should see "Test choice"
index 81fa1bc..8d5dc4f 100644 (file)
@@ -30,6 +30,7 @@ $string['analyticslogstore_help'] = 'The log store that will be used by the anal
 $string['analyticssettings'] = 'Analytics settings';
 $string['coursetoolong'] = 'The course is too long';
 $string['enabledtimesplittings'] = 'Time splitting methods';
+$string['disabledmodel'] = 'Disabled model';
 $string['erroralreadypredict'] = '{$a} file has already been used to predict';
 $string['errorcannotreaddataset'] = 'Dataset file {$a} can not be read';
 $string['errorcannotwritedataset'] = 'Dataset file {$a} can not be written';
@@ -53,7 +54,6 @@ $string['errorunexistingmodel'] = 'Unexisting model {$a}';
 $string['errorunknownaction'] = 'Unknown action';
 $string['eventpredictionactionstarted'] = 'Prediction action started';
 $string['insightmessagesubject'] = 'New insight for "{$a->contextname}": {$a->insightname}';
-$string['insightinfo'] = '{$a->insightname} - {$a->contextname}';
 $string['insightinfomessage'] = 'The system generated some insights for you: {$a}';
 $string['insightinfomessagehtml'] = 'The system generated some insights for you: <a href="{$a}">{$a}</a>.';
 $string['invalidtimesplitting'] = 'Model with id {$a} needs a time splitting method before it can be used to train';
@@ -73,6 +73,8 @@ $string['noranges'] = 'No predictions yet';
 $string['notrainingbasedassumptions'] = 'Models based on assumptions do not need training';
 $string['novaliddata'] = 'No valid data available';
 $string['novalidsamples'] = 'No valid samples available';
+$string['onlycli'] = 'Analytics processes execution via command line only';
+$string['onlycliinfo'] = 'Analytics processes like evaluating models, training machine learning algorithms or getting predictions can take some time, they will run as cron tasks and they can be forced via command line. Disable this setting if you want your site managers to be able to run these processes manually via web interface';
 $string['predictionsprocessor'] = 'Predictions processor';
 $string['predictionsprocessor_help'] = 'Prediction processors are the machine learning backends that process the datasets generated by calculating models\' indicators and targets.';
 $string['processingsitecontents'] = 'Processing site contents';
index eccd233..405046d 100644 (file)
@@ -130,6 +130,7 @@ $string['generateurlbutton'] = 'Get calendar URL';
 $string['global'] = 'Global';
 $string['globalevent'] = 'Global event';
 $string['globalevents'] = 'Global events';
+$string['gotoactivity'] = 'Go to activity';
 $string['gotocalendar'] = 'Go to calendar';
 $string['group'] = 'Group';
 $string['groupevent'] = 'Group event';
index 2464ba2..4a6c31b 100644 (file)
Binary files a/lib/amd/build/permissionmanager.min.js and b/lib/amd/build/permissionmanager.min.js differ
index 6c76ec4..dcd8712 100644 (file)
@@ -104,11 +104,13 @@ define(['jquery', 'core/config', 'core/notification', 'core/templates', 'core/yu
                         templatedata.spanclass = 'allowed';
                         templatedata.linkclass = 'preventlink';
                         templatedata.action = 'prevent';
+                        templatedata.icon = 't/delete';
                         break;
                     case 'prohibit':
                         templatedata.spanclass = 'forbidden';
                         templatedata.linkclass = 'unprohibitlink';
                         templatedata.action = 'unprohibit';
+                        templatedata.icon = 't/delete';
                         break;
                     case 'prevent':
                         row.find('a[data-role-id="' + roleid + '"]').first().closest('.allowed').remove();
index 86c450c..7b3a37a 100644 (file)
@@ -500,7 +500,7 @@ $capabilities = array(
 
     'moodle/user:delete' => array(
 
-        'riskbitmask' => RISK_PERSONAL, RISK_DATALOSS,
+        'riskbitmask' => RISK_PERSONAL | RISK_DATALOSS,
 
         'captype' => 'write',
         'contextlevel' => CONTEXT_SYSTEM,
index 3da4171..1ec6554 100644 (file)
@@ -443,6 +443,60 @@ abstract class file_system {
         return xsendfile($this->get_remote_path_from_hash($contenthash));
     }
 
+    /**
+     * Validate that the content hash matches the content hash of the file on disk.
+     *
+     * @param string $contenthash The current content hash to validate
+     * @param string $pathname The path to the file on disk
+     * @return array The content hash (it might change) and file size
+     */
+    protected function validate_hash_and_file_size($contenthash, $pathname) {
+        global $CFG;
+
+        if (!is_readable($pathname)) {
+            throw new file_exception('storedfilecannotread', '', $pathname);
+        }
+
+        $filesize = filesize($pathname);
+        if ($filesize === false) {
+            throw new file_exception('storedfilecannotread', '', $pathname);
+        }
+
+        if (is_null($contenthash)) {
+            $contenthash = file_storage::hash_from_path($pathname);
+        } else if ($CFG->debugdeveloper) {
+            $filehash = file_storage::hash_from_path($pathname);
+            if ($filehash === false) {
+                throw new file_exception('storedfilecannotread', '', $pathname);
+            }
+            if ($filehash !== $contenthash) {
+                // Hopefully this never happens, if yes we need to fix calling code.
+                debugging("Invalid contenthash submitted for file $pathname", DEBUG_DEVELOPER);
+                $contenthash = $filehash;
+            }
+        }
+        if ($contenthash === false) {
+            throw new file_exception('storedfilecannotread', '', $pathname);
+        }
+
+        if ($filesize > 0 and $contenthash === file_storage::hash_from_string('')) {
+            // Did the file change or is file_storage::hash_from_path() borked for this file?
+            clearstatcache();
+            $contenthash = file_storage::hash_from_path($pathname);
+            $filesize    = filesize($pathname);
+
+            if ($contenthash === false or $filesize === false) {
+                throw new file_exception('storedfilecannotread', '', $pathname);
+            }
+            if ($filesize > 0 and $contenthash === file_storage::hash_from_string('')) {
+                // This is very weird...
+                throw new file_exception('storedfilecannotread', '', $pathname);
+            }
+        }
+
+        return [$contenthash, $filesize];
+    }
+
     /**
      * Add the supplied file to the file system.
      *
index 816f7d6..f3840ca 100644 (file)
@@ -344,48 +344,8 @@ class file_system_filedir extends file_system {
      * @return array (contenthash, filesize, newfile)
      */
     public function add_file_from_path($pathname, $contenthash = null) {
-        global $CFG;
-
-        if (!is_readable($pathname)) {
-            throw new file_exception('storedfilecannotread', '', $pathname);
-        }
-
-        $filesize = filesize($pathname);
-        if ($filesize === false) {
-            throw new file_exception('storedfilecannotread', '', $pathname);
-        }
-
-        if (is_null($contenthash)) {
-            $contenthash = file_storage::hash_from_path($pathname);
-        } else if ($CFG->debugdeveloper) {
-            $filehash = file_storage::hash_from_path($pathname);
-            if ($filehash === false) {
-                throw new file_exception('storedfilecannotread', '', $pathname);
-            }
-            if ($filehash !== $contenthash) {
-                // Hopefully this never happens, if yes we need to fix calling code.
-                debugging("Invalid contenthash submitted for file $pathname", DEBUG_DEVELOPER);
-                $contenthash = $filehash;
-            }
-        }
-        if ($contenthash === false) {
-            throw new file_exception('storedfilecannotread', '', $pathname);
-        }
-
-        if ($filesize > 0 and $contenthash === file_storage::hash_from_string('')) {
-            // Did the file change or is file_storage::hash_from_path() borked for this file?
-            clearstatcache();
-            $contenthash = file_storage::hash_from_path($pathname);
-            $filesize = filesize($pathname);
 
-            if ($contenthash === false or $filesize === false) {
-                throw new file_exception('storedfilecannotread', '', $pathname);
-            }
-            if ($filesize > 0 and $contenthash === file_storage::hash_from_string('')) {
-                // This is very weird...
-                throw new file_exception('storedfilecannotread', '', $pathname);
-            }
-        }
+        list($contenthash, $filesize) = $this->validate_hash_and_file_size($contenthash, $pathname);
 
         $hashpath = $this->get_fulldir_from_hash($contenthash);
         $hashfile = $this->get_local_path_from_hash($contenthash, false);
index 435bc54..f6ff548 100644 (file)
@@ -51,6 +51,14 @@ abstract class base_message extends \core_search\base {
      * @return \core_search\document
      */
     public function get_document($record, $options = array()) {
+
+        // Check if user still exists, before proceeding.
+        $user = \core_user::get_user($options['user1id'], 'deleted');
+        if ($user->deleted == 1) {
+            return false;
+        }
+
+        // Get user context.
         try {
             $usercontext = \context_user::instance($options['user1id']);
         } catch (\moodle_exception $ex) {
index 191850e..7bcde69 100644 (file)
@@ -226,4 +226,49 @@ class message_received_search_testcase extends advanced_testcase {
         $this->assertEquals(\core_search\manager::ACCESS_DELETED, $searcharea->check_access($messageid));
 
     }
+
+    /**
+     * Test received deleted user.
+     * Tests the case where a received message for a deleted user
+     * is attempted to be added to the index.
+     *
+     * @return void
+     */
+    public function test_message_received_deleted_user() {
+
+        // Returns the instance as long as the area is supported.
+        $searcharea = \core_search\manager::get_search_area($this->messagereceivedareaid);
+        $this->assertInstanceOf('\core_message\search\message_received', $searcharea);
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        $this->preventResetByRollback();
+        $sink = $this->redirectMessages();
+
+        $message = new \core\message\message();
+        $message->courseid = SITEID;
+        $message->userfrom = $user1;
+        $message->userto = $user2;
+        $message->subject = "Test Subject";
+        $message->smallmessage = "Test small messsage";
+        $message->fullmessage = "Test full messsage";
+        $message->fullmessageformat = 0;
+        $message->fullmessagehtml = null;
+        $message->notification = 0;
+        $message->component = "moodle";
+        $message->name = "instantmessage";
+
+        message_send($message);
+
+        $messages = $sink->get_messages();
+        $message = $messages[0];
+
+        // Delete user.
+        delete_user($user2);
+
+        $doc = $searcharea->get_document($message);
+
+        $this->assertFalse($doc);
+    }
 }
index 7e9c887..75a3735 100644 (file)
@@ -226,4 +226,50 @@ class message_sent_search_testcase extends advanced_testcase {
         $this->assertEquals(\core_search\manager::ACCESS_DELETED, $searcharea->check_access($messageid));
 
     }
+
+    /**
+     * Test sent deleted user.
+     * Tests the case where a sent message for a deleted user
+     * is attempted to be added to the index.
+     *
+     * @return void
+     */
+    public function test_message_sent_deleted_user() {
+
+        // Returns the instance as long as the area is supported.
+        $searcharea = \core_search\manager::get_search_area($this->messagesentareaid);
+        $this->assertInstanceOf('\core_message\search\message_sent', $searcharea);
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        $this->preventResetByRollback();
+        $sink = $this->redirectMessages();
+
+        $message = new \core\message\message();
+        $message->courseid = SITEID;
+        $message->userfrom = $user1;
+        $message->userto = $user2;
+        $message->subject = "Test Subject";
+        $message->smallmessage = "Test small messsage";
+        $message->fullmessage = "Test full messsage";
+        $message->fullmessageformat = 0;
+        $message->fullmessagehtml = null;
+        $message->notification = 0;
+        $message->component = "moodle";
+        $message->name = "instantmessage";
+
+        message_send($message);
+
+        $messages = $sink->get_messages();
+        $message = $messages[0];
+
+        // Delete user.
+        delete_user($user1);
+
+        $doc = $searcharea->get_document($message);
+
+        $this->assertFalse($doc);
+
+    }
 }
\ No newline at end of file
index b4479b9..f8b6cce 100644 (file)
@@ -157,7 +157,7 @@ if ($csv) {
     echo $OUTPUT->header();
 
     // Handle groups (if enabled)
-    groups_print_course_menu($course, $CFG->wwwroot.'/report/completion/?course='.$course->id);
+    groups_print_course_menu($course, $CFG->wwwroot.'/report/completion/index.php?course='.$course->id);
 }
 
 if ($sifirst !== 'all') {
@@ -222,7 +222,7 @@ if ($total) {
 }
 
 // Build link for paging
-$link = $CFG->wwwroot.'/report/completion/?course='.$course->id;
+$link = $CFG->wwwroot.'/report/completion/index.php?course='.$course->id;
 if (strlen($sort)) {
     $link .= '&amp;sort='.$sort;
 }
@@ -428,10 +428,10 @@ if (!$csv) {
 
     if ($firstnamesort) {
         print
-            get_string('firstname')." / <a href=\"./?course={$course->id}{$sistring}\">".
+            get_string('firstname')." / <a href=\"./index.php?course={$course->id}{$sistring}\">".
             get_string('lastname').'</a>';
     } else {
-        print "<a href=\"./?course={$course->id}&amp;sort=firstname{$sistring}\">".
+        print "<a href=\"./index.php?course={$course->id}&amp;sort=firstname{$sistring}\">".
             get_string('firstname').'</a> / '.
             get_string('lastname');
     }
index 718e2ba..b9f0491 100644 (file)
@@ -45,7 +45,7 @@ if (!isset($actions[$actionname])) {
 }
 
 $modelready = $model->is_enabled() && $model->is_trained() && $model->predictions_exist($context);
-if (!$modelready && !has_capability('moodle/analytics:managemodels', $context)) {
+if (!$modelready) {
 
     $PAGE->set_pagelayout('report');
 
@@ -53,7 +53,7 @@ if (!$modelready && !has_capability('moodle/analytics:managemodels', $context))
     $PAGE->set_title($context->get_context_name());
     $PAGE->set_heading($context->get_context_name());
     echo $OUTPUT->header();
-    echo $OUTPUT->notification(get_string('disabledmodel', 'analytics'), \core\output\notification::NOTIFY_INFO);
+    echo $OUTPUT->notification(get_string('disabledmodel', 'report_insights'), \core\output\notification::NOTIFY_INFO);
     echo $OUTPUT->footer();
     exit(0);
 }
index 134283e..51c966d 100644 (file)
@@ -73,20 +73,22 @@ class insight implements \renderable, \templatable {
     public function export_for_template(\renderer_base $output) {
 
         $data = new \stdClass();
+        $data->insightname = format_string($this->model->get_target()->get_name());
 
         // Sample info (determined by the analyser).
         list($data->sampledescription, $samplerenderable) = $this->model->prediction_sample_description($this->prediction);
 
         // Sampleimage is a renderable we should pass it to HTML.
         if ($samplerenderable) {
-            $data->samplelink = $output->render($samplerenderable);
+            $data->sampleimage = $output->render($samplerenderable);
         }
 
         // Prediction info.
         $predictedvalue = $this->prediction->get_prediction_data()->prediction;
         $predictionid = $this->prediction->get_prediction_data()->id;
         $data->predictiondisplayvalue = $this->model->get_target()->get_display_value($predictedvalue);
-        $data->predictionstyle = $this->get_calculation_style($this->model->get_target(), $predictedvalue);
+        list($data->style, $data->outcomeicon) = $this->get_calculation_display($this->model->get_target(), $predictedvalue,
+            $output);
 
         $actions = $this->model->get_target()->prediction_actions($this->prediction, $this->includedetailsaction);
         if ($actions) {
@@ -122,39 +124,58 @@ class insight implements \renderable, \templatable {
             $obj = new \stdClass();
             $obj->name = call_user_func(array($calculation->indicator, 'get_name'));
             $obj->displayvalue = $calculation->indicator->get_display_value($calculation->value, $calculation->subtype);
-            $obj->style = $this->get_calculation_style($calculation->indicator, $calculation->value, $calculation->subtype);
+            list($obj->style, $obj->outcomeicon) = $this->get_calculation_display($calculation->indicator, $calculation->value,
+                $output, $calculation->subtype);
 
             $data->calculations[] = $obj;
         }
 
+        if (empty($data->calculations)) {
+            $data->nocalculations = (object)array(
+                'message' => get_string('nodetailsavailable', 'report_insights'),
+                'closebutton' => false
+            );
+        }
+
         return $data;
     }
 
     /**
-     * Returns a CSS class from the calculated value outcome.
+     * Returns display info for the calculated value outcome.
      *
      * @param \core_analytics\calculable $calculable
      * @param float $value
+     * @param \renderer_base $output
      * @param string|false $subtype
-     * @return string
+     * @return array The style as 'success', 'info', 'warning' or 'danger' and pix_icon
      */
-    protected function get_calculation_style(\core_analytics\calculable $calculable, $value, $subtype = false) {
+    protected function get_calculation_display(\core_analytics\calculable $calculable, $value, $output, $subtype = false) {
         $outcome = $calculable->get_calculation_outcome($value, $subtype);
         switch ($outcome) {
             case \core_analytics\calculable::OUTCOME_NEUTRAL:
                 $style = '';
+                $text = get_string('outcomeneutral', 'report_insights');
+                $icon = 't/check';
                 break;
             case \core_analytics\calculable::OUTCOME_VERY_POSITIVE:
-                $style = 'alert alert-success';
+                $style = 'success';
+                $text = get_string('outcomeverypositive', 'report_insights');
+                $icon = 't/approve';
                 break;
             case \core_analytics\calculable::OUTCOME_OK:
-                $style = 'alert alert-info';
+                $style = 'info';
+                $text = get_string('outcomeok', 'report_insights');
+                $icon = 't/check';
                 break;
             case \core_analytics\calculable::OUTCOME_NEGATIVE:
-                $style = 'alert alert-warning';
+                $style = 'warning';
+                $text = get_string('outcomenegative', 'report_insights');
+                $icon = 'i/warning';
                 break;
             case \core_analytics\calculable::OUTCOME_VERY_NEGATIVE:
-                $style = 'alert alert-danger';
+                $style = 'danger';
+                $text = get_string('outcomeverynegative', 'report_insights');
+                $icon = 'i/warning';
                 break;
             default:
                 throw new \coding_exception('The outcome returned by ' . get_class($calculable) . '::get_calculation_outcome is ' .
@@ -162,6 +183,7 @@ class insight implements \renderable, \templatable {
                     '\core_analytics\calculable::OUTCOME_OK, \core_analytics\calculable::OUTCOME_NEGATIVE, ' .
                     '\core_analytics\calculable::OUTCOME_VERY_NEGATIVE or \core_analytics\calculable::OUTCOME_NEUTRAL');
         }
-        return $style;
+        $icon = new \pix_icon($icon, $text);
+        return array($style, $icon->export_for_template($output));
     }
 }
index 3f67c4a..6596750 100644 (file)
@@ -88,6 +88,8 @@ class insights_list implements \renderable, \templatable {
         global $PAGE;
 
         $data = new \stdClass();
+        $data->insightname = format_string($this->model->get_target()->get_name());
+
         $total = 0;
 
         if ($this->model->uses_insights()) {
index 219d95a..b8fefdc 100644 (file)
@@ -75,7 +75,7 @@ class renderer extends plugin_renderer_base {
         $PAGE->set_heading($insightinfo->contextname);
 
         $output = $OUTPUT->header();
-        $output .= $OUTPUT->notification(get_string('disabledmodel', 'analytics'), \core\output\notification::NOTIFY_INFO);
+        $output .= $OUTPUT->notification(get_string('disabledmodel', 'report_insights'), \core\output\notification::NOTIFY_INFO);
         $output .= $OUTPUT->footer();
 
         return $output;
index 8ef763b..5e94ff5 100644 (file)
@@ -23,6 +23,7 @@
  */
 
 require_once(__DIR__ . '/../../config.php');
+require_once($CFG->libdir . '/adminlib.php');
 
 $contextid = required_param('contextid', PARAM_INT);
 $modelid = optional_param('modelid', false, PARAM_INT);
@@ -52,8 +53,13 @@ if ($modelid) {
     unset($othermodels[$modelid]);
 }
 
+// The URL in navigation only contains the contextid.
 $params = array('contextid' => $contextid);
-$url = new \moodle_url('/report/insights/insights.php', $params);
+$navurl = new \moodle_url('/report/insights/insights.php', $params);
+
+// This is the real page url, we need it to include the modelid so pagination and
+// other stuff works as expected.
+$url = clone $navurl;
 if ($modelid) {
     $url->param('modelid', $modelid);
 }
@@ -61,6 +67,18 @@ if ($modelid) {
 $PAGE->set_url($url);
 $PAGE->set_pagelayout('report');
 
+if ($context->contextlevel === CONTEXT_SYSTEM) {
+    admin_externalpage_setup('reportinsights', '', null, '', array('pagelayout' => 'report'));
+} else if ($context->contextlevel === CONTEXT_USER) {
+    $user = \core_user::get_user($context->instanceid, '*', MUST_EXIST);
+    $PAGE->navigation->extend_for_user($user);
+    $PAGE->add_report_nodes($user->id, array(
+        'name' => get_string('insights', 'report_insights'),
+        'url' => $url
+    ));
+}
+$PAGE->navigation->override_active_url($navurl);
+
 $renderer = $PAGE->get_renderer('report_insights');
 
 // No models with insights available at this context level.
@@ -74,9 +92,8 @@ $model = new \core_analytics\model($modelid);
 $insightinfo = new stdClass();
 $insightinfo->contextname = $context->get_context_name();
 $insightinfo->insightname = $model->get_target()->get_name();
-$title = get_string('insightinfo', 'analytics', $insightinfo);
 
-if (!$model->is_enabled() && !has_capability('moodle/analytics:managemodels', $context)) {
+if (!$model->is_enabled()) {
     echo $renderer->render_model_disabled($insightinfo);
     exit(0);
 }
@@ -86,8 +103,8 @@ if (!$model->uses_insights()) {
     exit(0);
 }
 
-$PAGE->set_title($title);
-$PAGE->set_heading($title);
+$PAGE->set_title($insightinfo->insightname);
+$PAGE->set_heading($insightinfo->contextname);
 
 echo $OUTPUT->header();
 
index 17af8e9..a69600b 100644 (file)
  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-
+$string['calculatedvalue'] = 'Calculated value';
 $string['disabledmodel'] = 'Sorry, this model has been disabled by the administrator';
+$string['indicator'] = 'Indicator';
+$string['insightprediction'] = '{$a} prediction';
+$string['insight'] = 'Insight';
 $string['insights'] = 'Insights';
+$string['outcome'] = 'Outcome';
+$string['outcomenegative'] = 'Negative outcome';
+$string['outcomeneutral'] = 'Neutral outcome';
+$string['outcomeok'] = 'Ok outcome';
+$string['outcomepositive'] = 'Positive outcome';
+$string['outcomeverypositive'] = 'Very positive outcome';
+$string['outcomeverynegative'] = 'Very negative outcome';
 $string['pluginname'] = 'Insights';
 $string['prediction'] = 'Prediction';
+$string['predictioncalculations'] = 'Indicator calculations';
 $string['predictiondetails'] = 'Prediction details';
+$string['nodetailsavailable'] = 'No prediction details are relevant.';
 $string['selectotherinsights'] = 'Select other insights...';
index 6ebc7a1..e63bc79 100644 (file)
@@ -36,20 +36,85 @@ function report_insights_extend_navigation_course($navigation, $course, $context
 
     if (has_capability('moodle/analytics:listinsights', $context)) {
 
-        $cache = \cache::make('core', 'contextwithinsights');
-        $modelids = $cache->get($context->id);
-        if ($modelids === false) {
-            // They will be full unless a model has been cleared.
-            $models = \core_analytics\manager::get_models_with_insights($context);
-            $modelids = array_keys($models);
-            $cache->set($context->id, $modelids);
+        $modelids = report_insights_context_insights($context);
+        if (!empty($modelids)) {
+            $url = new moodle_url('/report/insights/insights.php', array('contextid' => $context->id));
+            $node = navigation_node::create(get_string('insights', 'report_insights'), $url, navigation_node::TYPE_SETTING,
+                null, null, new pix_icon('i/report', get_string('insights', 'report_insights')));
+            $navigation->add_node($node);
         }
+    }
+}
+
+/**
+ * Add nodes to myprofile page.
+ *
+ * @param \core_user\output\myprofile\tree $tree Tree object
+ * @param stdClass $user user object
+ * @param bool $iscurrentuser
+ * @param stdClass $course Course object
+ *
+ * @return bool
+ */
+function report_insights_myprofile_navigation(core_user\output\myprofile\tree $tree, $user, $iscurrentuser, $course) {
 
+    $context = \context_user::instance($user->id);
+    if (has_capability('moodle/analytics:listinsights', $context)) {
+
+        $modelids = report_insights_context_insights($context);
         if (!empty($modelids)) {
             $url = new moodle_url('/report/insights/insights.php', array('contextid' => $context->id));
-            $settingsnode = navigation_node::create(get_string('insights', 'report_insights'), $url, navigation_node::TYPE_SETTING,
-                null, null, new pix_icon('i/settings', ''));
-            $navigation->add_node($settingsnode);
+            $node = new core_user\output\myprofile\node('reports', 'insights', get_string('insights', 'report_insights'),
+                null, $url);
+            $tree->add_node($node);
         }
     }
 }
+
+/**
+ * Adds nodes to category navigation
+ *
+ * @param navigation_node $navigation The navigation node to extend
+ * @param context $context The context of the course
+ * @return void|null return null if we don't want to display the node.
+ */
+function report_insights_extend_navigation_category_settings($navigation, $context) {
+
+    if (has_capability('moodle/analytics:listinsights', $context)) {
+
+        $modelids = report_insights_context_insights($context);
+        if (!empty($modelids)) {
+            $url = new moodle_url('/report/insights/insights.php', array('contextid' => $context->id));
+
+            $node = navigation_node::create(
+                get_string('insights', 'report_insights'),
+                $url,
+                navigation_node::NODETYPE_LEAF,
+                null,
+                'insights',
+                new pix_icon('i/report', get_string('insights', 'report_insights'))
+            );
+
+            $navigation->add_node($node);
+        }
+    }
+}
+
+/**
+ * Returns the models that generated insights in the provided context.
+ *
+ * @param \context $context
+ * @return int[]
+ */
+function report_insights_context_insights(\context $context) {
+
+    $cache = \cache::make('core', 'contextwithinsights');
+    $modelids = $cache->get($context->id);
+    if ($modelids === false) {
+        // They will be full unless a model has been cleared.
+        $models = \core_analytics\manager::get_models_with_insights($context);
+        $modelids = array_keys($models);
+        $cache->set($context->id, $modelids);
+    }
+    return $modelids;
+}
index 7e53d83..bb67a02 100644 (file)
@@ -23,6 +23,7 @@
  */
 
 require_once(__DIR__ . '/../../config.php');
+require_once($CFG->libdir . '/adminlib.php');
 
 $predictionid = required_param('id', PARAM_INT);
 
@@ -37,15 +38,30 @@ $url = new \moodle_url('/report/insights/prediction.php', $params);
 $PAGE->set_url($url);
 $PAGE->set_pagelayout('report');
 
+$navurl = new \moodle_url('/report/insights/insights.php', array('contextid' => $context->id));
+if ($context->contextlevel === CONTEXT_SYSTEM) {
+    admin_externalpage_setup('reportinsights', '', null, '', array('pagelayout' => 'report'));
+} else if ($context->contextlevel === CONTEXT_USER) {
+    $user = \core_user::get_user($context->instanceid, '*', MUST_EXIST);
+    $PAGE->navigation->extend_for_user($user);
+
+    $modelinsightsurl = clone $navurl;
+    $modelinsightsurl->param('modelid', $model->get_id());
+    $PAGE->add_report_nodes($user->id, array(
+        'name' => get_string('insights', 'report_insights'),
+        'url' => $url
+    ));
+}
+$PAGE->navigation->override_active_url($navurl);
+
 $renderer = $PAGE->get_renderer('report_insights');
 
 $insightinfo = new stdClass();
 $insightinfo->contextname = $context->get_context_name();
 $insightinfo->insightname = $model->get_target()->get_name();
-$title = get_string('insightinfo', 'analytics', $insightinfo);
 
 $modelready = $model->is_enabled() && $model->is_trained() && $model->predictions_exist($context);
-if (!$modelready && !has_capability('moodle/analytics:managemodels', $context)) {
+if (!$modelready) {
     echo $renderer->render_model_disabled($insightinfo);
     exit(0);
 }
@@ -55,8 +71,8 @@ if (!$model->uses_insights()) {
     exit(0);
 }
 
-$PAGE->set_title($title);
-$PAGE->set_heading($title);
+$PAGE->set_title($insightinfo->insightname);
+$PAGE->set_heading($insightinfo->contextname);
 
 echo $OUTPUT->header();
 
diff --git a/report/insights/settings.php b/report/insights/settings.php
new file mode 100644 (file)
index 0000000..49a4559
--- /dev/null
@@ -0,0 +1,34 @@
+<?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/>.
+
+/**
+ * Links and settings
+ *
+ * Contains settings used by insights report.
+ *
+ * @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;
+
+// Just a link to course report.
+$ADMIN->add('reports', new admin_externalpage('reportinsights', get_string('insights', 'report_insights'),
+        $CFG->wwwroot . "/report/insights/insights.php?contextid=" . SYSCONTEXTID, 'moodle/analytics:listinsights'));
+
+// No report settings.
+$settings = null;
index c7cad23..ad24102 100644 (file)
 
     Example context (json):
     {
-        "samplelink": "<a href=\"#\">Link</a>",
+        "sampleimage": "<a href=\"#\">Link</a>",
         "sampledescription": "Sample description",
-        "predictionstyle": "alert alert-success",
+        "style": "success",
+        "outcomeicon": {
+            "attributes": [
+                {"name": "src", "value": "https://moodle.org/logo/moodle-logo.svg" }
+            ]
+        },
         "predictiondisplayvalue": "This dev will understand it"
     }
 }}
-<div class="card">
-    <div class="row m-a-1">
-        {{#samplelink}}
-            <div class="col-sm-1 span1 m-b-1">
-                {{{samplelink}}}
-            </div>
-            <div class="col-sm-3 span3 m-b-1">
-                {{{sampledescription}}}
-            </div>
-        {{/samplelink}}
-        {{^samplelink}}
-            <div class="col-sm-4 span4 m-b-1">
-                {{{sampledescription}}}
-            </div>
-        {{/samplelink}}
-        <div class="col-sm-4 span4">
-            <div class="{{predictionstyle}}">{{predictiondisplayvalue}}</div>
-        </div>
-        <div class="col-sm-4 span4">
-            {{#actions}}
-                    {{> core/action_menu}}
-            {{/actions}}
-        </div>
-    </div>
-</div>
+<tr>
+    <td class="col-sm-6">
+        {{#sampleimage}}
+            {{{sampleimage}}}
+        {{/sampleimage}}
+        {{{sampledescription}}}
+    </td>
+    <td class="{{#style}}table-{{style}}{{/style}} col-sm-4">
+        {{#outcomeicon}}
+            {{> core/pix_icon}}
+        {{/outcomeicon}}
+        <span>{{predictiondisplayvalue}}</span>
+    </td>
+    <td class="col-sm-2">
+    {{#actions}}
+        {{> core/action_menu}}
+    {{/actions}}
+    </td>
+</tr>
index 0cc241c..72940c9 100644 (file)
 
     Example context (json):
     {
-        "samplelink": "<a href=\"#\">Link</a>",
+        "insightname": "Best insight ever",
+        "sampleimage": "<a href=\"#\">Link</a>",
         "sampledescription": "Sample description",
-        "predictionstyle": "alert alert-success",
-        "predictiondisplayvalue": "This dev will success",
+        "style": "success",
+        "outcomeicon": {
+            "attributes": [
+                {"name": "src", "value": "https://moodle.org/logo/moodle-logo.svg" }
+            ]
+        },
+        "predictiondisplayvalue": "This dev will understand it",
         "calculations": [
             {
-                "style": "alert alert-success",
+                "style": "success",
+                "outcomeicon": {
+                    "attributes": [
+                        {"name": "src", "value": "https://moodle.org/logo/moodle-logo.svg" }
+                    ]
+                },
                 "name": "Indicator 1",
                 "displayvalue": "yes"
             }, {
-                "style": "alert alert-warning",
+                "style": "info",
+                "name": "Indicator 2",
+                "displayvalue": "20%"
+            }, {
+                "style": "",
+                "name": "Indicator 2",
+                "displayvalue": "20%"
+            }, {
+                "style": "warning",
+                "name": "Indicator 2",
+                "displayvalue": "20%"
+            }, {
+                "style": "danger",
                 "name": "Indicator 2",
                 "displayvalue": "20%"
             }
     }
 }}
 
-<h2>{{#str}}prediction, report_insights{{/str}}</h2>
-{{> report_insights/insight}}
+<h2 class="m-b-2">{{#str}}insightprediction, report_insights, {{insightname}} {{/str}}</h2>
+<table class="generaltable insights-list">
+    <caption>{{#str}}insight, report_insights{{/str}}</caption>
+    <thead>
+        <tr>
+            <th scope="col" class="col-sm-6">{{#str}}name{{/str}}</th>
+            <th scope="col" class="col-sm-4">{{#str}}prediction, report_insights{{/str}}</th>
+            <th scope="col" class="col-sm-2">{{#str}}actions{{/str}}</th>
+        </tr>
+    </thead>
+    <tbody>
+        {{> report_insights/insight}}
+    </tbody>
+</table>
 
-<h3>{{#str}} predictiondetails, report_insights {{/str}}</h3>
-<div class="container prediction-calculations m-t-2">
+<table class="generaltable prediction-calculations">
+    <caption>{{#str}}predictiondetails, report_insights{{/str}}</caption>
+    <thead>
+        <tr>
+            <th scope="col" class="col-sm-8">{{#str}}indicator, report_insights{{/str}}</th>
+            <th scope="col" class="col-sm-4">{{#str}}calculatedvalue, report_insights{{/str}}</th>
+        </tr>
+    </thead>
+    <tbody>
     {{#calculations}}
-        <div class="{{style}}">{{name}} - {{displayvalue}}</div>
+        <tr>
+            <td class="{{#style}}table-{{style}}{{/style}} col-sm-8">{{name}}</td>
+            <td class="{{#style}}table-{{style}}{{/style}} col-sm-4">{{#outcomeicon}}{{> core/pix_icon}}{{/outcomeicon}} {{displayvalue}}</td>
+        </td>
     {{/calculations}}
-</div>
+    </tbody>
+</table>
+{{#nocalculations}}
+    {{> core/notification_info}}
+{{/nocalculations}}
index 0150ef3..ecb2689 100644 (file)
 
     Example context (json):
     {
+        "insightname": "Best insight ever",
         "insights": [
             {
-                "samplelink": "<a href=\"#\">Link</a>",
+                "sampleimage": "<a href=\"#\">Link</a>",
                 "sampledescription": "Sample description",
-                "predictionstyle": "alert alert-success",
+                "style": "success",
+                "outcomeicon": {
+                    "attributes": [
+                        {"name": "src", "value": "https://moodle.org/logo/moodle-logo.svg" }
+                    ]
+                },
                 "predictiondisplayvalue": "This dev will understand it"
             }, {
-                "samplelink": "<a href=\"#\">Any renderable</a>",
+                "sampleimage": "<a href=\"#\">Any renderable</a>",
                 "sampledescription": "Another sample description",
-                "predictionstyle": "alert alert-danger",
+                "style": "danger",
+                "outcomeicon": {
+                    "attributes": [
+                        {"name": "src", "value": "https://moodle.org/logo/moodle-logo.svg" }
+                    ]
+                },
                 "predictiondisplayvalue": "This dev will not understand it"
             }
         ],
     </div>
 {{/modelselector}}
 
-<h3>{{#str}} insights, report_insights {{/str}}</h3>
+<h2 class="m-b-2">{{{insightname}}}</h2>
 {{^noinsights}}
 {{{ pagingbar }}}
-<div class="insights-list">
-    {{#insights}}
-        {{> report_insights/insight}}
-    {{/insights}}
-</div>
+<table class="generaltable insights-list">
+    <caption>{{#str}}insights, report_insights{{/str}}</caption>
+    <thead>
+        <tr>
+            <th scope="col" class="col-sm-6">{{#str}}name{{/str}}</th>
+            <th scope="col" class="col-sm-4">{{#str}}prediction, report_insights{{/str}}</th>
+            <th scope="col" class="col-sm-2">{{#str}}actions{{/str}}</th>
+        </tr>
+    </thead>
+    <tbody>
+        {{#insights}}
+            {{> report_insights/insight}}
+        {{/insights}}
+    </tbody>
+</table>
 {{{ pagingbar }}}
 {{/noinsights}}
 {{#noinsights}}
index d7ab0df..8529b85 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2017051500; // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version   = 2017051501; // 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).
index bb3b514..3c47b1a 100644 (file)
@@ -90,7 +90,9 @@ if ($logreader !== '') {
 if (($edulevel != -1)) {
     $params['edulevel'] = $edulevel;
 }
-
+if ($origin !== '') {
+    $params['origin'] = $origin;
+}
 // Legacy store hack, as edulevel is not supported.
 if ($logreader == 'logstore_legacy') {
     $params['edulevel'] = -1;
index 578f8bb..0e84a08 100644 (file)
                     margin-right: 0.5rem;
                 }
                 .name,
-                .course {
+                .action {
                     margin-bottom: 5px;
                 }
                 .date {
                     float: right;
                 }
-                .course,
-                .subscription {
+                .subscription,
+                .action {
                     float: left;
                     clear: left;
                 }
index 65c23c7..98f8b92 100644 (file)
@@ -5718,14 +5718,14 @@ img.iconsmall {
   margin-right: 0.5rem;
 }
 .path-calendar .maincalendar .eventlist .event .name,
-.path-calendar .maincalendar .eventlist .event .course {
+.path-calendar .maincalendar .eventlist .event .action {
   margin-bottom: 5px;
 }
 .path-calendar .maincalendar .eventlist .event .date {
   float: right;
 }
-.path-calendar .maincalendar .eventlist .event .course,
-.path-calendar .maincalendar .eventlist .event .subscription {
+.path-calendar .maincalendar .eventlist .event .subscription,
+.path-calendar .maincalendar .eventlist .event .action {
   float: left;
   clear: left;
 }
diff --git a/theme/bootstrapbase/templates/report_insights/insight.mustache b/theme/bootstrapbase/templates/report_insights/insight.mustache
new file mode 100644 (file)
index 0000000..3e152a6
--- /dev/null
@@ -0,0 +1,62 @@
+{{!
+    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
+
+    Template for a insight.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * none
+
+    Example context (json):
+    {
+        "sampleimage": "<a href=\"#\">Link</a>",
+        "sampledescription": "Sample description",
+        "style": "success",
+        "outcomeicon": {
+            "attributes": [
+                {"name": "src", "value": "https://moodle.org/logo/moodle-logo.svg" }
+            ]
+        },
+        "predictiondisplayvalue": "This dev will understand it"
+    }
+}}
+<tr>
+    <td class="col-sm-6">
+        {{#sampleimage}}
+            {{{sampleimage}}}
+        {{/sampleimage}}
+        {{{sampledescription}}}
+    </td>
+    <td class="col-sm-4">
+        {{#outcomeicon}}
+            {{> core/pix_icon}}
+        {{/outcomeicon}}
+        <span>{{predictiondisplayvalue}}</span>
+    </td>
+    <td class="col-sm-2">
+    {{#actions}}
+        {{> core/action_menu}}
+    {{/actions}}
+    </td>
+</tr>
diff --git a/theme/bootstrapbase/templates/report_insights/insight_details.mustache b/theme/bootstrapbase/templates/report_insights/insight_details.mustache
new file mode 100644 (file)
index 0000000..65dda0f
--- /dev/null
@@ -0,0 +1,108 @@
+{{!
+    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):
+    {
+        "insightname": "Best insight ever",
+        "sampleimage": "<a href=\"#\">Link</a>",
+        "sampledescription": "Sample description",
+        "style": "success",
+        "outcomeicon": {
+            "attributes": [
+                {"name": "src", "value": "https://moodle.org/logo/moodle-logo.svg" }
+            ]
+        },
+        "predictiondisplayvalue": "This dev will understand it",
+        "calculations": [
+            {
+                "style": "success",
+                "outcomeicon": {
+                    "attributes": [
+                        {"name": "src", "value": "https://moodle.org/logo/moodle-logo.svg" }
+                    ]
+                },
+                "name": "Indicator 1",
+                "displayvalue": "yes"
+            }, {
+                "style": "info",
+                "name": "Indicator 2",
+                "displayvalue": "20%"
+            }, {
+                "style": "",
+                "name": "Indicator 2",
+                "displayvalue": "20%"
+            }, {
+                "style": "warning",
+                "name": "Indicator 2",
+                "displayvalue": "20%"
+            }, {
+                "style": "danger",
+                "name": "Indicator 2",
+                "displayvalue": "20%"
+            }
+        ]
+    }
+}}
+
+<h2 class="m-b-2">{{#str}}insightprediction, report_insights, {{insightname}} {{/str}}</h2>
+<table class="generaltable insights-list">
+    <caption>{{#str}}insight, report_insights{{/str}}</caption>
+    <thead>
+        <tr>
+            <th scope="col" class="col-sm-6">{{#str}}name{{/str}}</th>
+            <th scope="col" class="col-sm-4">{{#str}}prediction, report_insights{{/str}}</th>
+            <th scope="col" class="col-sm-2">{{#str}}actions{{/str}}</th>
+        </tr>
+    </thead>
+    <tbody>
+        {{> report_insights/insight}}
+    </tbody>
+</table>
+
+<table class="generaltable prediction-calculations">
+    <caption>{{#str}}predictiondetails, report_insights{{/str}}</caption>
+    <thead>
+        <tr>
+            <th scope="col" class="col-sm-8">{{#str}}indicator, report_insights{{/str}}</th>
+            <th scope="col" class="col-sm-4">{{#str}}calculatedvalue, report_insights{{/str}}</th>
+        </tr>
+    </thead>
+    <tbody>
+    {{#calculations}}
+        <tr>
+            <td class="col-sm-8">{{name}}</td>
+            <td class="col-sm-4">{{#outcomeicon}}{{> core/pix_icon}}{{/outcomeicon}} {{displayvalue}}</td>
+        </td>
+    {{/calculations}}
+    </tbody>
+</table>
+{{#nocalculations}}
+    {{> core/notification_info}}
+{{/nocalculations}}
diff --git a/theme/bootstrapbase/templates/report_insights/insights_list.mustache b/theme/bootstrapbase/templates/report_insights/insights_list.mustache
new file mode 100644 (file)
index 0000000..ecb2689
--- /dev/null
@@ -0,0 +1,91 @@
+{{!
+    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/insights_list
+
+    Template for the insights list.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * none
+
+    Example context (json):
+    {
+        "insightname": "Best insight ever",
+        "insights": [
+            {
+                "sampleimage": "<a href=\"#\">Link</a>",
+                "sampledescription": "Sample description",
+                "style": "success",
+                "outcomeicon": {
+                    "attributes": [
+                        {"name": "src", "value": "https://moodle.org/logo/moodle-logo.svg" }
+                    ]
+                },
+                "predictiondisplayvalue": "This dev will understand it"
+            }, {
+                "sampleimage": "<a href=\"#\">Any renderable</a>",
+                "sampledescription": "Another sample description",
+                "style": "danger",
+                "outcomeicon": {
+                    "attributes": [
+                        {"name": "src", "value": "https://moodle.org/logo/moodle-logo.svg" }
+                    ]
+                },
+                "predictiondisplayvalue": "This dev will not understand it"
+            }
+        ],
+        "noinsights": false
+    }
+}}
+
+{{#modelselector}}
+    <div class="m-b-2">
+        {{> core/single_select}}
+    </div>
+{{/modelselector}}
+
+<h2 class="m-b-2">{{{insightname}}}</h2>
+{{^noinsights}}
+{{{ pagingbar }}}
+<table class="generaltable insights-list">
+    <caption>{{#str}}insights, report_insights{{/str}}</caption>
+    <thead>
+        <tr>
+            <th scope="col" class="col-sm-6">{{#str}}name{{/str}}</th>
+            <th scope="col" class="col-sm-4">{{#str}}prediction, report_insights{{/str}}</th>
+            <th scope="col" class="col-sm-2">{{#str}}actions{{/str}}</th>
+        </tr>
+    </thead>
+    <tbody>
+        {{#insights}}
+            {{> report_insights/insight}}
+        {{/insights}}
+    </tbody>
+</table>
+{{{ pagingbar }}}
+{{/noinsights}}
+{{#noinsights}}
+    <div class="m-t-2">
+        {{> core/notification_info}}
+    </div>
+{{/noinsights}}
index 65f2207..74e9a69 100644 (file)
@@ -29,7 +29,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2017090100.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2017090400.01;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.
 
index 2117e8b..2b0098c 100644 (file)
@@ -913,7 +913,6 @@ abstract class webservice_server implements webservice_server_interface {
         }
 
         $loginfaileddefaultparams = array(
-            'context' => context_system::instance(),
             'other' => array(
                 'method' => $this->authmethod,
                 'reason' => null
@@ -1062,7 +1061,6 @@ abstract class webservice_server implements webservice_server_interface {
         global $DB;
 
         $loginfaileddefaultparams = array(
-            'context' => context_system::instance(),
             'other' => array(
                 'method' => $this->authmethod,
                 'reason' => null