Merge branch 'MDL-59759-master-2' of git://github.com/junpataleta/moodle
authorDavid Monllao <davidm@moodle.com>
Tue, 5 Sep 2017 05:25:09 +0000 (07:25 +0200)
committerDavid Monllao <davidm@moodle.com>
Thu, 7 Sep 2017 08:52:20 +0000 (10:52 +0200)
68 files changed:
admin/registration/forms.php
admin/registration/lib.php
admin/registration/register.php
admin/settings/analytics.php
admin/tool/analytics/classes/output/models_list.php
admin/tool/analytics/cli/guess_course_start_and_end.php
admin/tool/analytics/lang/en/tool_analytics.php
admin/tool/analytics/model.php
admin/tool/analytics/templates/models_list.mustache
analytics/classes/local/analyser/base.php
analytics/classes/model.php
backup/moodle2/restore_stepslib.php
calendar/amd/build/calendar.min.js
calendar/amd/build/event_form.min.js
calendar/amd/build/modal_event_form.min.js
calendar/amd/src/calendar.js
calendar/amd/src/event_form.js
calendar/amd/src/modal_event_form.js
calendar/classes/local/event/forms/create.php
calendar/lib.php
calendar/renderer.php
calendar/templates/event_summary_modal.mustache
calendar/tests/behat/calendar.feature
lang/en/analytics.php
lang/en/calendar.php
lang/en/hub.php
lib/amd/build/permissionmanager.min.js
lib/amd/src/permissionmanager.js
lib/classes/analytics/target/course_dropout.php
lib/classes/analytics/target/no_teaching.php
lib/classes/task/manager.php
lib/coursecatlib.php
lib/db/access.php
lib/ddl/tests/ddl_test.php
lib/dml/moodle_database.php
lib/dml/tests/dml_test.php
lib/filestorage/file_system.php
lib/filestorage/file_system_filedir.php
lib/templates/url_select.mustache
lib/tests/adhoc_task_test.php
message/classes/search/base_message.php
message/tests/search_test_received.php
message/tests/search_test_sent.php
mod/lti/locallib.php
mod/lti/tests/locallib_test.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/less/moodle/forms.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 4cb0e40..0d70879 100644 (file)
@@ -257,7 +257,7 @@ class site_registration_form extends moodleform {
         $modulenumberaveragecfg = get_config('hub', 'site_modulenumberaverage_' . $cleanhuburl);
         // Mobile related information.
         $mobileservicesenabled = get_config('hub', 'site_mobileservicesenabled_' . $cleanhuburl);
-        $mobilenotificacionsenabled = get_config('hub', 'site_mobilenotificacionsenabled_' . $cleanhuburl);
+        $mobilenotificationsenabled = get_config('hub', 'site_mobilenotificationsenabled_' . $cleanhuburl);
         $registereduserdevices = get_config('hub', 'site_registereduserdevices_' . $cleanhuburl);
         $registeredactiveuserdevices = get_config('hub', 'site_registeredactiveuserdevices_' . $cleanhuburl);
 
@@ -474,10 +474,10 @@ class site_registration_form extends moodleform {
             $mform->setType('mobileservicesenabled', PARAM_INT);
 
             $mobilenotificationsstatus = $aremobilenotificationsenabled ? 'yes' : 'no';
-            $mform->addElement('checkbox', 'mobilenotificacionsenabled', '',
-                    " " . get_string('mobilenotificacionsenabled', 'hub', $mobilenotificationsstatus));
-            $mform->setDefault('mobilenotificacionsenabled', $mobilenotificacionsenabled != -1);
-            $mform->setType('mobilenotificacionsenabled', PARAM_INT);
+            $mform->addElement('checkbox', 'mobilenotificationsenabled', '',
+                    " " . get_string('mobilenotificationsenabled', 'hub', $mobilenotificationsstatus));
+            $mform->setDefault('mobilenotificationsenabled', $mobilenotificationsenabled != -1);
+            $mform->setType('mobilenotificationsenabled', PARAM_INT);
 
             $mform->addElement('checkbox', 'registereduserdevices', '',
                     " " . get_string('registereduserdevices', 'hub', $registereduserdevicescount));
@@ -547,10 +547,10 @@ class site_registration_form extends moodleform {
             $mform->setType('mobileservicesenabled', PARAM_INT);
 
             $mobilenotificationsstatus = $aremobilenotificationsenabled ? 'yes' : 'no';
-            $mform->addElement('static', 'mobilenotificacionsenabledlabel', '',
-                    " " . get_string('mobilenotificacionsenabled', 'hub', $mobilenotificationsstatus));
-            $mform->addElement('hidden', 'mobilenotificacionsenabled', 1);
-            $mform->setType('mobilenotificacionsenabled', PARAM_INT);
+            $mform->addElement('static', 'mobilenotificationsenabledlabel', '',
+                    " " . get_string('mobilenotificationsenabled', 'hub', $mobilenotificationsstatus));
+            $mform->addElement('hidden', 'mobilenotificationsenabled', 1);
+            $mform->setType('mobilenotificationsenabled', PARAM_INT);
 
             $mform->addElement('static', 'registereduserdeviceslabel', '',
                     " " . get_string('registereduserdevices', 'hub', $registereduserdevicescount));
index 1e48bc8..8d1f8db 100644 (file)
@@ -282,7 +282,7 @@ class registration_manager {
         $siteinfo['url'] = $CFG->wwwroot;
         // Mobile related information.
         $siteinfo['mobileservicesenabled'] = 0;
-        $siteinfo['mobilenotificacionsenabled'] = 0;
+        $siteinfo['mobilenotificationsenabled'] = 0;
         $siteinfo['registereduserdevices'] = 0;
         $siteinfo['registeredactiveuserdevices'] = 0;
         if (!empty($CFG->enablewebservices) && !empty($CFG->enablemobilewebservice)) {
@@ -291,7 +291,7 @@ class registration_manager {
             $airnotifierextpath = $CFG->dirroot . '/message/output/airnotifier/externallib.php';
             if (file_exists($airnotifierextpath)) { // Maybe some one uninstalled the plugin.
                 require_once($airnotifierextpath);
-                $siteinfo['mobilenotificacionsenabled'] = message_airnotifier_external::is_system_configured();
+                $siteinfo['mobilenotificationsenabled'] = message_airnotifier_external::is_system_configured();
                 $siteinfo['registeredactiveuserdevices'] = $DB->count_records('message_airnotifier_devices', array('enable' => 1));
             }
         }
index 43d2e42..2b7cf96 100644 (file)
@@ -69,7 +69,7 @@ if (!empty($fromform) and confirm_sesskey()) {
     // The function get_site_info() will not calculate the optional data if config is set to -1.
     $inputnames = array('courses', 'users', 'roleassignments', 'posts', 'questions', 'resources',
         'badges', 'issuedbadges', 'modulenumberaverage', 'participantnumberaverage',
-        'mobileservicesenabled', 'mobilenotificacionsenabled', 'registereduserdevices', 'registeredactiveuserdevices');
+        'mobileservicesenabled', 'mobilenotificationsenabled', 'registereduserdevices', 'registeredactiveuserdevices');
     foreach ($inputnames as $inputname) {
         if (empty($fromform->{$inputname})) {
             $fromform->{$inputname} = -1;
@@ -103,7 +103,7 @@ if (!empty($fromform) and confirm_sesskey()) {
     set_config('site_modulenumberaverage_' . $cleanhuburl, $fromform->modulenumberaverage, 'hub');
     set_config('site_participantnumberaverage_' . $cleanhuburl, $fromform->participantnumberaverage, 'hub');
     set_config('site_mobileservicesenabled_' . $cleanhuburl, $fromform->mobileservicesenabled, 'hub');
-    set_config('site_mobilenotificacionsenabled_' . $cleanhuburl, $fromform->mobilenotificacionsenabled, 'hub');
+    set_config('site_mobilenotificationsenabled_' . $cleanhuburl, $fromform->mobilenotificationsenabled, 'hub');
     set_config('site_registereduserdevices_' . $cleanhuburl, $fromform->registereduserdevices, 'hub');
     set_config('site_registeredactiveuserdevices_' . $cleanhuburl, $fromform->registeredactiveuserdevices, 'hub');
 }
@@ -149,7 +149,7 @@ if (!empty($fromform) and empty($update) and confirm_sesskey()) {
         $fromform->participantnumberaverage = $siteinfo['participantnumberaverage'];
         $fromform->street = $siteinfo['street'];
         $fromform->mobileservicesenabled = $siteinfo['mobileservicesenabled'];
-        $fromform->mobilenotificacionsenabled = $siteinfo['mobilenotificacionsenabled'];
+        $fromform->mobilenotificationsenabled = $siteinfo['mobilenotificationsenabled'];
         $fromform->registereduserdevices = $siteinfo['registereduserdevices'];
         $fromform->registeredactiveuserdevices = $siteinfo['registeredactiveuserdevices'];
 
index 17724a3..3d28719 100644 (file)
@@ -79,7 +79,7 @@ if ($hassiteconfig) {
 
         $timesplittingoptions = array();
         $timesplittingdefaults = array('\core\analytics\time_splitting\quarters_accum',
-            '\core\analytics\time_splitting\quarters', '\core\analytics\time_splitting\no_splitting');
+            '\core\analytics\time_splitting\quarters', '\core\analytics\time_splitting\single_range');
         foreach ($alltimesplittings as $key => $timesplitting) {
             $timesplittingoptions[$key] = $timesplitting->get_name();
         }
@@ -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 f8273e1..404b010 100644 (file)
@@ -32,6 +32,8 @@ require_once($CFG->dirroot . '/course/format/weeks/lib.php');
 
 $help = "Guesses course start and end dates based on activity logs.
 
+IMPORTANT: Don't use this script if you keep previous academic years users enrolled in courses. Guesses would not be accurate.
+
 Options:
 --guessstart           Guess the course start date (default to true)
 --guessend             Guess the course end date (default to true)
@@ -183,10 +185,14 @@ function tool_analytics_calculate_course_dates($course, $options) {
 
                 $course->enddate = $guessedenddate;
 
-                if ($course->enddate > $course->startdate) {
-                    $notification .= PHP_EOL . '  ' . get_string('enddate') . ': ' . userdate($course->enddate);
-                } else {
+                $updateit = false;
+                if ($course->enddate < $course->startdate) {
                     $notification .= PHP_EOL . '  ' . get_string('errorendbeforestart', 'analytics', userdate($course->enddate));
+                } else if ($course->startdate + (YEARSECS + (WEEKSECS * 4)) > $course->enddate) {
+                    $notification .= PHP_EOL . '  ' . get_string('coursetoolong', 'analytics');
+                } else {
+                    $notification .= PHP_EOL . '  ' . get_string('enddate') . ': ' . userdate($course->enddate);
+                    $updateit = true;
                 }
 
                 if ($options['update']) {
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 b179dcb..ab2f52e 100644 (file)
@@ -469,8 +469,8 @@ abstract class base {
             return $result;
         }
 
-        // Add target metadata.
-        $this->add_target_metadata($data);
+        // Add extra metadata.
+        $this->add_model_metadata($data);
 
         // Write all calculated data to a file.
         $file = $dataset->store($data);
@@ -646,21 +646,25 @@ abstract class base {
      * @param array $data
      * @return void
      */
-    protected function add_target_metadata(&$data) {
-        $data[0][] = 'targetcolumn';
-        $data[1][] = $this->analysabletarget->get_id();
+    protected function add_model_metadata(&$data) {
+        global $CFG;
+
+        $metadata = array(
+            'moodleversion' => $CFG->version,
+            'targetcolumn' => $this->analysabletarget->get_id()
+        );
         if ($this->analysabletarget->is_linear()) {
-            $data[0][] = 'targettype';
-            $data[1][] = 'linear';
-            $data[0][] = 'targetmin';
-            $data[1][] = $this->analysabletarget::get_min_value();
-            $data[0][] = 'targetmax';
-            $data[1][] = $this->analysabletarget::get_max_value();
+            $metadata['targettype'] = 'linear';
+            $metadata['targetmin'] = $this->analysabletarget::get_min_value();
+            $metadata['targetmax'] = $this->analysabletarget::get_max_value();
         } else {
-            $data[0][] = 'targettype';
-            $data[1][] = 'discrete';
-            $data[0][] = 'targetclasses';
-            $data[1][] = json_encode($this->analysabletarget::get_classes());
+            $metadata['targettype'] = 'discrete';
+            $metadata['targetclasses'] = json_encode($this->analysabletarget::get_classes());
+        }
+
+        foreach ($metadata as $varname => $value) {
+            $data[0][] = $varname;
+            $data[1][] = $value;
         }
     }
 }
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 dfcf545..10872f8 100644 (file)
@@ -5560,6 +5560,6 @@ class restore_calendar_action_events extends restore_execution_step {
         // The task will automatically populate all data.
         $task = new \core\task\refresh_mod_calendar_events_task();
         $task->set_custom_data(array('courseid' => $this->get_courseid()));
-        \core\task\manager::queue_adhoc_task($task);
+        \core\task\manager::queue_adhoc_task($task, true);
     }
 }
index 9d29445..a96ce0b 100644 (file)
Binary files a/calendar/amd/build/calendar.min.js and b/calendar/amd/build/calendar.min.js differ
index d90e7c8..3a3c976 100644 (file)
Binary files a/calendar/amd/build/event_form.min.js and b/calendar/amd/build/event_form.min.js differ
index f29703f..b27c823 100644 (file)
Binary files a/calendar/amd/build/modal_event_form.min.js and b/calendar/amd/build/modal_event_form.min.js differ
index ea10975..8a74ee4 100644 (file)
@@ -64,7 +64,8 @@ define([
         NEW_EVENT_BUTTON: "[data-action='new-event-button']",
         DAY_CONTENT: "[data-region='day-content']",
         LOADING_ICON: '.loading-icon',
-        VIEW_DAY_LINK: "[data-action='view-day-link']"
+        VIEW_DAY_LINK: "[data-action='view-day-link']",
+        CALENDAR_MONTH_WRAPPER: ".calendarwrapper"
     };
 
     /**
@@ -140,7 +141,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
                 }
             };
 
@@ -254,7 +257,8 @@ define([
      * @param {object} eventFormModalPromise A promise reolved with the event form modal
      */
     var registerCalendarEventListeners = function(root, eventFormModalPromise) {
-        var body = $('body');
+        var body = $('body'),
+            courseId = $(root).find(SELECTORS.CALENDAR_MONTH_WRAPPER).data('courseid');
 
         body.on(CalendarEvents.created, function() {
             CalendarViewManager.reloadCurrentMonth(root);
@@ -283,7 +287,7 @@ define([
                 modal.setEventId(eventId);
                 modal.show();
             });
-
+            modal.setCourseId(courseId);
             return;
         });
     };
index e6abfd8..ea3e417 100644 (file)
@@ -35,13 +35,6 @@ define(['jquery', 'core/templates'], function($, Templates) {
         MORELESS_TOGGLE: '.moreless-actions'
     };
 
-    var EVENT_TYPES = {
-        USER: 'user',
-        SITE: 'site',
-        COURSE: 'course',
-        GROUP: 'group'
-    };
-
     var EVENTS = {
         SHOW_ADVANCED: 'event_form-show-advanced',
         HIDE_ADVANCED: 'event_form-hide-advanced',
@@ -140,75 +133,6 @@ define(['jquery', 'core/templates'], function($, Templates) {
             });
     };
 
-    /**
-     * Toggle the visibility of the secondary select elements based on
-     * the event type the user has selected.
-     *
-     * There are 3 secondary select elements within the form:
-     *      - course: a list of all courses a user can add course events to
-     *      - group course: a list of all courses a user can add group events to.
-     *                      this list can be different from the course list above.
-     *      - group: a list of all groups a user can add an event to. This list will
-     *               be filtered further based on the group course selected.
-     *
-     *  There are 4 event types:
-     *      - user: none of the secondary selects should be visible.
-     *      - site: none of the secondary selects should be visible.
-     *      - course: "course" select should be visible and both "group course"
-     *                and "group" should be hidden.
-     *      - group: "group course" and "group" should be visible and "course"
-     *               should be hidden.
-     *
-     * @method hideTypeSubSelects
-     * @param {object} formElement The root form element
-     */
-    var hideTypeSubSelects = function(formElement) {
-        var typeSelect = formElement.find(SELECTORS.EVENT_TYPE);
-        var eventType = typeSelect.val();
-        var courseIdSelect = formElement.find(SELECTORS.EVENT_COURSE_ID)
-            .closest(SELECTORS.FORM_GROUP)
-            .removeClass('hidden');
-        var groupCourseIdSelect = formElement.find(SELECTORS.EVENT_GROUP_COURSE_ID)
-            .closest(SELECTORS.FORM_GROUP)
-            .removeClass('hidden');
-        var groupIdSelect = formElement.find(SELECTORS.EVENT_GROUP_ID)
-            .closest(SELECTORS.FORM_GROUP)
-            .removeClass('hidden');
-
-        // Hide the unreleated selectors for the given event type.
-        switch (eventType) {
-            case EVENT_TYPES.COURSE:
-                groupCourseIdSelect.addClass('hidden');
-                groupIdSelect.addClass('hidden');
-                break;
-            case EVENT_TYPES.GROUP:
-                courseIdSelect.addClass('hidden');
-                break;
-            default:
-                courseIdSelect.addClass('hidden');
-                groupCourseIdSelect.addClass('hidden');
-                groupIdSelect.addClass('hidden');
-        }
-    };
-
-    /**
-     * Listen for when the user changes the event type select in the
-     * form and then toggle the visibility of the appropriate secondary
-     * select elements.
-     *
-     * See: hideTypeSubSelects.
-     *
-     * @method addTypeSelectListeners
-     * @param {object} formElement The root form element
-     */
-    var addTypeSelectListeners = function(formElement) {
-        var typeSelect = formElement.find(SELECTORS.EVENT_TYPE);
-
-        typeSelect.on('change', function() {
-            hideTypeSubSelects(formElement);
-        });
-    };
-
     /**
      * Listen for when the user changes the group course when configuring
      * a group event and filter the options in the group select to only
@@ -260,9 +184,7 @@ define(['jquery', 'core/templates'], function($, Templates) {
 
         listenForShowHideEvents(formElement);
         destroyOldMoreLessToggle(formElement);
-        hideTypeSubSelects(formElement);
         parseGroupSelect(formElement);
-        addTypeSelectListeners(formElement);
         addCourseGroupSelectListeners(formElement);
 
         // If we know that the form has been rendered with server side
index cee039a..034e98d 100644 (file)
@@ -67,6 +67,7 @@ define([
         Modal.call(this, root);
         this.eventId = null;
         this.startTime = null;
+        this.courseId = null;
         this.reloadingBody = false;
         this.reloadingTitle = false;
         this.saveButton = this.getFooter().find(SELECTORS.SAVE_BUTTON);
@@ -77,6 +78,36 @@ define([
     ModalEventForm.prototype = Object.create(Modal.prototype);
     ModalEventForm.prototype.constructor = ModalEventForm;
 
+    /**
+     * Set the course id to the given value.
+     *
+     * @method setCourseId
+     * @param {int} id The event id
+     */
+    ModalEventForm.prototype.setCourseId = function(id) {
+        this.courseId = id;
+    };
+
+    /**
+     * Retrieve the current course id, if any.
+     *
+     * @method getCourseId
+     * @return {int|null} The event id
+     */
+    ModalEventForm.prototype.getCourseId = function() {
+        return this.courseId;
+    };
+
+    /**
+     * Check if the modal has an course id.
+     *
+     * @method hasCourseId
+     * @return {bool}
+     */
+    ModalEventForm.prototype.hasCourseId = function() {
+        return this.courseId !== null;
+    };
+
     /**
      * Set the event id to the given value.
      *
@@ -279,6 +310,10 @@ define([
             args.starttime = this.getStartTime();
         }
 
+        if (this.hasCourseId()) {
+            args.courseid = this.getCourseId();
+        }
+
         if (typeof formData !== 'undefined') {
             args.formdata = formData;
         }
index e6c025f..165b145 100644 (file)
@@ -214,7 +214,7 @@ class create extends \moodleform {
             }
 
             $mform->addElement('select', 'courseid', get_string('course'), $courseoptions);
-            $mform->disabledIf('courseid', 'eventtype', 'noteq', 'course');
+            $mform->hideIf('courseid', 'eventtype', 'noteq', 'course');
         }
 
         if (isset($eventtypes['group'])) {
@@ -225,7 +225,7 @@ class create extends \moodleform {
             }
 
             $mform->addElement('select', 'groupcourseid', get_string('course'), $courseoptions);
-            $mform->disabledIf('groupcourseid', 'eventtype', 'noteq', 'group');
+            $mform->hideIf('groupcourseid', 'eventtype', 'noteq', 'group');
 
             $groupoptions = [];
             foreach ($eventtypes['group'] as $group) {
@@ -237,7 +237,7 @@ class create extends \moodleform {
             }
 
             $mform->addElement('select', 'groupid', get_string('group'), $groupoptions);
-            $mform->disabledIf('groupid', 'eventtype', 'noteq', 'group');
+            $mform->hideIf('groupid', 'eventtype', 'noteq', 'group');
         }
     }
 
index 3ad809b..f574472 100644 (file)
@@ -3537,6 +3537,7 @@ function calendar_output_fragment_event_form($args) {
     $data = null;
     $eventid = isset($args['eventid']) ? clean_param($args['eventid'], PARAM_INT) : null;
     $starttime = isset($args['starttime']) ? clean_param($args['starttime'], PARAM_INT) : null;
+    $courseid = isset($args['courseid']) ? clean_param($args['courseid'], PARAM_INT) : null;
     $event = null;
     $hasformdata = isset($args['formdata']) && !empty($args['formdata']);
     $formoptions = [];
@@ -3563,6 +3564,12 @@ function calendar_output_fragment_event_form($args) {
             true,
             $data
         );
+        if ($courseid != SITEID) {
+            $data['eventtype'] = 'course';
+            $data['courseid'] = $courseid;
+            $data['groupcourseid'] = $courseid;
+        }
+        $mform->set_data($data);
     } else {
         $event = calendar_event::load($eventid);
         $event->count_repeats();
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 45e964f..f1d6153 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..29e1736 100644 (file)
@@ -30,10 +30,11 @@ $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';
-$string['errorendbeforestart'] = 'The guessed end date ({$a}) is before the course start date.';
+$string['errorendbeforestart'] = 'The end date ({$a}) is before the course start date.';
 $string['errorinvalidindicator'] = 'Invalid {$a} indicator';
 $string['errorinvalidtimesplitting'] = 'Invalid time splitting, please ensure you added the class fully qualified class name';
 $string['errornoindicators'] = 'This model does not have any indicator';
@@ -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 0d168b8..e179229 100644 (file)
@@ -117,7 +117,7 @@ $string['licence'] = 'Licence';
 $string['licence_help'] = 'Select the licence you want to distribute your course under.';
 $string['licence_link'] = 'licenses';
 $string['logourl'] = 'Logo URL';
-$string['mobilenotificacionsenabled'] = 'Mobile notifications enabled ({$a})';
+$string['mobilenotificationsenabled'] = 'Mobile notifications enabled ({$a})';
 $string['mobileservicesenabled'] = 'Mobile services enabled ({$a})';
 $string['modulenumberaverage'] = 'Average number of course modules ({$a})';
 $string['moodleorg'] = 'Moodle';
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 855be38..b773632 100644 (file)
@@ -115,7 +115,7 @@ class course_dropout extends \core_analytics\local\target\binary {
     }
 
     /**
-     * is_valid_analysable
+     * Discards courses that are not yet ready to be used for training or prediction.
      *
      * @param \core_analytics\analysable $course
      * @param bool $fortraining
@@ -147,13 +147,22 @@ class course_dropout extends \core_analytics\local\target\binary {
             return get_string('nocourseendtime');
         }
 
+        if ($course->get_end() < $course->get_start()) {
+            return get_string('errorendbeforestart', 'analytics');
+        }
+
+        // A course that lasts longer than 1 year probably have wrong start or end dates.
+        if ($course->get_end() - $course->get_start() > (YEARSECS + (WEEKSECS * 4))) {
+            return get_string('coursetoolong', 'analytics');
+        }
+
         // Ongoing courses data can not be used to train.
         if ($fortraining && !$course->is_finished()) {
             return get_string('coursenotyetfinished');
         }
 
         if ($fortraining) {
-            // Not a valid target for training if there are not enough course accesses.
+            // Not a valid target for training if there are not enough course accesses between the course start and end dates.
 
             $params = array('courseid' => $course->get_id(), 'anonymous' => 0, 'start' => $course->get_start(),
                 'end' => $course->get_end());
@@ -178,14 +187,39 @@ class course_dropout extends \core_analytics\local\target\binary {
     }
 
     /**
-     * All student enrolments are valid.
+     * Discard student enrolments that are invalid.
      *
      * @param int $sampleid
      * @param \core_analytics\analysable $course
      * @param bool $fortraining
-     * @return true|string
+     * @return bool
      */
     public function is_valid_sample($sampleid, \core_analytics\analysable $course, $fortraining = true) {
+
+        $userenrol = $this->retrieve('user_enrolments', $sampleid);
+        if ($userenrol->timeend && $course->get_start() > $userenrol->timeend) {
+            // Discard enrolments which time end is prior to the course start. This should get rid of
+            // old user enrolments that remain on the course.
+            return false;
+        }
+
+        $limit = $course->get_start() - (YEARSECS + (WEEKSECS * 4));
+        if (($userenrol->timestart && $userenrol->timestart < $limit) ||
+                (!$userenrol->timestart && $userenrol->timecreated < $limit)) {
+            // Following what we do in is_valid_sample, we will discard enrolments that last more than 1 academic year
+            // because they have incorrect start and end dates or because they are reused along multiple years
+            // without removing previous academic years students. This may not be very accurate because some courses
+            // can last just some months, but it is better than nothing and they will be flagged as drop out anyway
+            // in most of the cases.
+            return false;
+        }
+
+        if (($userenrol->timestart && $userenrol->timestart > $course->get_end()) ||
+                (!$userenrol->timestart && $userenrol->timecreated > $course->get_end())) {
+            // Discard user enrolments that starts after the analysable official end.
+            return false;
+        }
+
         return true;
     }
 
index ef54503..7d3714f 100644 (file)
@@ -134,7 +134,7 @@ class no_teaching extends \core_analytics\local\target\binary {
      * @param int $sampleid
      * @param \core_analytics\analysable $analysable
      * @param bool $fortraining
-     * @return true|string
+     * @return bool
      */
     public function is_valid_sample($sampleid, \core_analytics\analysable $analysable, $fortraining = true) {
 
index 106878b..b19b689 100644 (file)
@@ -125,13 +125,30 @@ class manager {
         $DB->delete_records_select('task_scheduled', $sql, $params);
     }
 
+    /**
+     * Checks if the task with the same classname, component and customdata is already scheduled
+     *
+     * @param adhoc_task $task
+     * @return bool
+     */
+    protected static function task_is_scheduled($task) {
+        global $DB;
+        $record = self::record_from_adhoc_task($task);
+        $params = [$record->classname, $record->component, $record->customdata];
+        $sql = 'classname = ? AND component = ? AND ' .
+            $DB->sql_compare_text('customdata', \core_text::strlen($record->customdata) + 1) . ' = ?';
+        return $DB->record_exists_select('task_adhoc', $sql, $params);
+    }
+
     /**
      * Queue an adhoc task to run in the background.
      *
      * @param \core\task\adhoc_task $task - The new adhoc task information to store.
+     * @param bool $checkforexisting - If set to true and the task with the same classname, component and customdata
+     *     is already scheduled then it will not schedule a new task. Can be used only for ASAP tasks.
      * @return boolean - True if the config was saved.
      */
-    public static function queue_adhoc_task(adhoc_task $task) {
+    public static function queue_adhoc_task(adhoc_task $task, $checkforexisting = false) {
         global $DB;
 
         $record = self::record_from_adhoc_task($task);
@@ -139,6 +156,13 @@ class manager {
         if (!$task->get_next_run_time()) {
             $record->nextruntime = time() - 1;
         }
+
+        // Check if the same task is already scheduled.
+        if ($checkforexisting && self::task_is_scheduled($task)) {
+            return false;
+        }
+
+        // Queue the task.
         $result = $DB->insert_record('task_adhoc', $record);
 
         return $result;
index e5edde1..d8c8ef1 100644 (file)
@@ -1839,6 +1839,7 @@ class coursecat implements renderable, cacheable_object, IteratorAggregate {
         }
 
         if ($coursesids) {
+            require_once($CFG->dirroot.'/course/lib.php');
             if (!move_courses($coursesids, $newparentid)) {
                 if ($showfeedback) {
                     echo $OUTPUT->notification("Error moving courses");
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 edf6258..1fbec98 100644 (file)
@@ -2115,6 +2115,55 @@ class core_ddl_testcase extends database_driver_testcase {
         }
     }
 
+    /**
+     * Data provider for test_get_enc_quoted().
+     *
+     * @return array The type-value pair fixture.
+     */
+    public function test_get_enc_quoted_provider() {
+        return array(
+            // Reserved: some examples from SQL-92.
+            [true, 'from'],
+            [true, 'table'],
+            [true, 'where'],
+            // Not reserved.
+            [false, 'my_awesome_column_name']
+        );
+    }
+
+    /**
+     * This is a test for sql_generator::getEncQuoted().
+     *
+     * @dataProvider test_get_enc_quoted_provider
+     * @param string $reserved Whether the column name is reserved or not.
+     * @param string $columnname The column name to be quoted, according to the value of $reserved.
+     **/
+    public function test_get_enc_quoted($reserved, $columnname) {
+        $DB = $this->tdb;
+        $gen = $DB->get_manager()->generator;
+
+        if (!$reserved) {
+            // No need to quote the column name.
+            $this->assertSame($columnname, $gen->getEncQuoted($columnname));
+        } else {
+            // Column name should be quoted.
+            $dbfamily = $DB->get_dbfamily();
+
+            switch ($dbfamily) {
+                case 'mysql':
+                    $this->assertSame("`$columnname`", $gen->getEncQuoted($columnname));
+                    break;
+                case 'mssql': // The Moodle connection runs under 'QUOTED_IDENTIFIER ON'.
+                case 'oracle':
+                case 'postgres':
+                case 'sqlite':
+                default:
+                    $this->assertSame('"' . $columnname . '"', $gen->getEncQuoted($columnname));
+                    break;
+            }
+        }
+    }
+
     // Following methods are not supported == Do not test.
     /*
         public function testRenameIndex() {
index 3dd3586..8741823 100644 (file)
@@ -2398,7 +2398,8 @@ abstract class moodle_database {
         // NOTE: override this methods if following standard compliant SQL
         //       does not work for your driver.
 
-        $columnname = $column->name;
+        // Enclose the column name by the proper quotes if it's a reserved word.
+        $columnname = $this->get_manager()->generator->getEncQuoted($column->name);
         $sql = "UPDATE {".$table."}
                        SET $columnname = REPLACE($columnname, ?, ?)
                      WHERE $columnname IS NOT NULL";
index 849fbf3..8786585 100644 (file)
@@ -4573,19 +4573,55 @@ class core_dml_testcase extends database_driver_testcase {
         $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
         $table->add_field('name', XMLDB_TYPE_CHAR, '20', null, null);
         $table->add_field('intro', XMLDB_TYPE_TEXT, 'big', null, null);
+        // Add a CHAR field named using a word reserved for all the supported DB servers.
+        $table->add_field('where', XMLDB_TYPE_CHAR, '20', null, null, null, 'localhost');
+        // Add a TEXT field named using a word reserved for all the supported DB servers.
+        $table->add_field('from', XMLDB_TYPE_TEXT, 'big', null, null);
         $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
         $dbman->create_table($table);
 
-        $id1 = (string)$DB->insert_record($tablename, array('name' => null, 'intro' => null));
-        $id2 = (string)$DB->insert_record($tablename, array('name' => '', 'intro' => ''));
-        $id3 = (string)$DB->insert_record($tablename, array('name' => 'xxyy', 'intro' => 'vvzz'));
-        $id4 = (string)$DB->insert_record($tablename, array('name' => 'aa bb aa bb', 'intro' => 'cc dd cc aa'));
-        $id5 = (string)$DB->insert_record($tablename, array('name' => 'kkllll', 'intro' => 'kkllll'));
+        $fromfield = $dbman->generator->getEncQuoted('from');
+        $DB->execute("INSERT INTO {".$tablename."} (name,intro,$fromfield) VALUES (NULL,NULL,'localhost')");
+        $DB->execute("INSERT INTO {".$tablename."} (name,intro,$fromfield) VALUES ('','','localhost')");
+        $DB->execute("INSERT INTO {".$tablename."} (name,intro,$fromfield) VALUES ('xxyy','vvzz','localhost')");
+        $DB->execute("INSERT INTO {".$tablename."} (name,intro,$fromfield) VALUES ('aa bb aa bb','cc dd cc aa','localhost')");
+        $DB->execute("INSERT INTO {".$tablename."} (name,intro,$fromfield) VALUES ('kkllll','kkllll','localhost')");
 
         $expected = $DB->get_records($tablename, array(), 'id ASC');
+        $idx = 1;
+        $id1 = $id2 = $id3 = $id4 = $id5 = 0;
+        foreach (array_keys($expected) as $identifier) {
+            ${"id$idx"} = (string)$identifier;
+            $idx++;
+        }
 
         $columns = $DB->get_columns($tablename);
 
+        // Replace should work even with columns named using a reserved word.
+        $this->assertEquals('C', $columns['where']->meta_type);
+        $this->assertEquals('localhost', $expected[$id1]->where);
+        $this->assertEquals('localhost', $expected[$id2]->where);
+        $this->assertEquals('localhost', $expected[$id3]->where);
+        $this->assertEquals('localhost', $expected[$id4]->where);
+        $this->assertEquals('localhost', $expected[$id5]->where);
+        $DB->replace_all_text($tablename, $columns['where'], 'localhost', '::1');
+        $result = $DB->get_records($tablename, array(), 'id ASC');
+        $expected[$id1]->where = '::1';
+        $expected[$id2]->where = '::1';
+        $expected[$id3]->where = '::1';
+        $expected[$id4]->where = '::1';
+        $expected[$id5]->where = '::1';
+        $this->assertEquals($expected, $result);
+        $this->assertEquals('X', $columns['from']->meta_type);
+        $DB->replace_all_text($tablename, $columns['from'], 'localhost', '127.0.0.1');
+        $result = $DB->get_records($tablename, array(), 'id ASC');
+        $expected[$id1]->from = '127.0.0.1';
+        $expected[$id2]->from = '127.0.0.1';
+        $expected[$id3]->from = '127.0.0.1';
+        $expected[$id4]->from = '127.0.0.1';
+        $expected[$id5]->from = '127.0.0.1';
+        $this->assertEquals($expected, $result);
+
         $DB->replace_all_text($tablename, $columns['name'], 'aa', 'o');
         $result = $DB->get_records($tablename, array(), 'id ASC');
         $expected[$id4]->name = 'o bb o bb';
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 b2ace98..0904896 100644 (file)
@@ -8,6 +8,19 @@
         "formid": "url_select_form",
         "sesskey": "sesskey",
         "label": "core/url_select",
+        "helpicon": {
+            "title": "Help with something",
+            "text": "Help with something",
+            "url": "http://example.org/help",
+            "linktext": "",
+            "icon":{
+                "extraclasses": "iconhelp",
+                "attributes": [
+                    {"name": "src", "value": "../../../pix/help.svg"},
+                    {"name": "alt", "value": "Help icon"}
+                ]
+            }
+        },
         "showbutton": "Go",
         "options": [{
             "name": "Group 1", "isgroup": true, "options":
 <div class="{{classes}}">
     <form method="post" action="{{action}}" class="form-inline" id="{{formid}}">
         <input type="hidden" name="sesskey" value="{{sesskey}}">
-        <div class="form-group">
-            {{#label}}
-                <label for="{{id}}"{{#labelattributes}} {{name}}="{{value}}"{{/labelattributes}}>
-                    {{label}}
-                </label>
-            {{/label}}
-            {{#helpicon}}
-                {{>core/help_icon}}
-            {{/helpicon}}
-            <select {{#attributes}}{{name}}="{{value}}" {{/attributes}} id="{{id}}" class="custom-select {{classes}}" name="jump">
-                {{#options}}
-                    {{#isgroup}}
-                        <optgroup label="{{name}}">
-                            {{#options}}
-                                <option value="{{value}}" {{#selected}}selected{{/selected}}>{{name}}</option>
-                            {{/options}}
-                        </optgroup>
-                    {{/isgroup}}
-                    {{^isgroup}}
-                        <option value="{{value}}" {{#selected}}selected{{/selected}}>{{name}}</option>
-                    {{/isgroup}}
-                {{/options}}
-            </select>
-        </div>
+        {{#label}}
+            <label for="{{id}}"{{#labelattributes}} {{name}}="{{value}}"{{/labelattributes}}>
+                {{label}}
+            </label>
+        {{/label}}
+        {{#helpicon}}
+            {{>core/help_icon}}
+        {{/helpicon}}
+        <select {{#attributes}}{{name}}="{{value}}" {{/attributes}} id="{{id}}" class="custom-select {{classes}}" name="jump">
+            {{#options}}
+                {{#isgroup}}
+                    <optgroup label="{{name}}">
+                        {{#options}}
+                            <option value="{{value}}" {{#selected}}selected{{/selected}}>{{name}}</option>
+                        {{/options}}
+                    </optgroup>
+                {{/isgroup}}
+                {{^isgroup}}
+                    <option value="{{value}}" {{#selected}}selected{{/selected}}>{{name}}</option>
+                {{/isgroup}}
+            {{/options}}
+        </select>
         {{#showbutton}}
             <input type="submit" class="btn btn-secondary" value="{{showbutton}}">
         {{/showbutton}}
index d1ec1d8..e4ebcac 100644 (file)
@@ -149,4 +149,40 @@ class core_adhoc_task_testcase extends advanced_testcase {
             $this->assertInstanceOf('\\core\\task\\adhoc_test2_task', $task);
         }
     }
+
+    /**
+     * Test queue_adhoc_task "if not scheduled".
+     */
+    public function test_queue_adhoc_task_if_not_scheduled() {
+        $this->resetAfterTest(true);
+
+        // Schedule adhoc task.
+        $task1 = new \core\task\adhoc_test_task();
+        $task1->set_custom_data(array('courseid' => 10));
+        $this->assertNotEmpty(\core\task\manager::queue_adhoc_task($task1, true));
+        $this->assertEquals(1, count(\core\task\manager::get_adhoc_tasks('core\task\adhoc_test_task')));
+
+        // Schedule same adhoc task with different custom data.
+        $task2 = new \core\task\adhoc_test_task();
+        $task2->set_custom_data(array('courseid' => 1));
+        $this->assertNotEmpty(\core\task\manager::queue_adhoc_task($task2, true));
+        $this->assertEquals(2, count(\core\task\manager::get_adhoc_tasks('core\task\adhoc_test_task')));
+
+        // Schedule same adhoc task with same custom data.
+        $task3 = new \core\task\adhoc_test_task();
+        $task3->set_custom_data(array('courseid' => 1));
+        $this->assertEmpty(\core\task\manager::queue_adhoc_task($task3, true));
+        $this->assertEquals(2, count(\core\task\manager::get_adhoc_tasks('core\task\adhoc_test_task')));
+
+        // Schedule same adhoc task without custom data.
+        $task4 = new \core\task\adhoc_test_task();
+        $this->assertNotEmpty(\core\task\manager::queue_adhoc_task($task4, true));
+        $this->assertEquals(3, count(\core\task\manager::get_adhoc_tasks('core\task\adhoc_test_task')));
+
+        // Schedule same adhoc task without custom data (again).
+        $task5 = new \core\task\adhoc_test_task();
+        $this->assertEmpty(\core\task\manager::queue_adhoc_task($task5, true));
+        $this->assertEquals(3, count(\core\task\manager::get_adhoc_tasks('core\task\adhoc_test_task')));
+
+    }
 }
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 81e4a44..f1e5762 100644 (file)
@@ -1165,7 +1165,7 @@ function lti_get_enabled_capabilities($tool) {
  * @param string    $customstr      String containing the parameters
  * @param boolean   $islti2         True if an LTI 2 tool is being launched
  *
- * @return Array of custom parameters
+ * @return array of custom parameters
  */
 function lti_split_custom_parameters($toolproxy, $tool, $params, $customstr, $islti2 = false) {
     $customstr = str_replace("\r\n", "\n", $customstr);
@@ -1179,11 +1179,12 @@ function lti_split_custom_parameters($toolproxy, $tool, $params, $customstr, $is
             continue;
         }
         $key = trim(core_text::substr($line, 0, $pos));
+        $key = lti_map_keyname($key, false);
         $val = trim(core_text::substr($line, $pos + 1, strlen($line)));
         $val = lti_parse_custom_parameter($toolproxy, $tool, $params, $val, $islti2);
         $key2 = lti_map_keyname($key);
         $retval['custom_'.$key2] = $val;
-        if ($islti2 && ($key != $key2)) {
+        if ($key != $key2) {
             $retval['custom_'.$key] = $val;
         }
     }
@@ -1269,14 +1270,16 @@ function lti_parse_custom_parameter($toolproxy, $tool, $params, $value, $islti2)
  * Used for building the names of the different custom parameters
  *
  * @param string $key   Parameter name
- *
+ * @param bool $tolower Do we want to convert the key into lower case?
  * @return string       Processed name
  */
-function lti_map_keyname($key) {
+function lti_map_keyname($key, $tolower = true) {
     $newkey = "";
-    $key = core_text::strtolower(trim($key));
+    if ($tolower) {
+        $key = core_text::strtolower(trim($key));
+    }
     foreach (str_split($key) as $ch) {
-        if ( ($ch >= 'a' && $ch <= 'z') || ($ch >= '0' && $ch <= '9') ) {
+        if ( ($ch >= 'a' && $ch <= 'z') || ($ch >= '0' && $ch <= '9') || (!$tolower && ($ch >= 'A' && $ch <= 'Z'))) {
             $newkey .= $ch;
         } else {
             $newkey .= '_';
index 5a471c0..cdc3160 100644 (file)
@@ -71,14 +71,20 @@ class mod_lti_locallib_testcase extends advanced_testcase {
         $this->assertEquals(lti_split_custom_parameters(null, $tool, array(), "x=1\ny=2", false),
             array('custom_x' => '1', 'custom_y' => '2'));
 
+        // Check params with caps.
+        $this->assertEquals(lti_split_custom_parameters(null, $tool, array(), "X=1", false),
+            array('custom_x' => '1', 'custom_X' => '1'));
+
         // Removed repeat of previous test with a semicolon separator.
 
         $this->assertEquals(lti_split_custom_parameters(null, $tool, array(), 'Review:Chapter=1.2.56', false),
-            array('custom_review_chapter' => '1.2.56'));
+            array('custom_review_chapter' => '1.2.56', 'custom_Review_Chapter' => '1.2.56'));
 
         $this->assertEquals(lti_split_custom_parameters(null, $tool, array(),
             'Complex!@#$^*(){}[]KEY=Complex!@#$^*;(){}[]½Value', false),
-            array('custom_complex____________key' => 'Complex!@#$^*;(){}[]½Value'));
+            array(
+                'custom_complex____________key' => 'Complex!@#$^*;(){}[]½Value',
+                'custom_Complex____________KEY' => 'Complex!@#$^*;(){}[]½Value'));
 
         // Test custom parameter that returns $USER property.
         $user = $this->getDataGenerator()->create_user(array('middlename' => 'SOMETHING'));
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 dcaad6d..940a181 100644 (file)
@@ -122,6 +122,15 @@ form {
     margin-right: .3em;
 }
 
+.custom-select {
+    max-width: 100%;
+}
+
+// Make the submit button align with the .custom-select element.
+.custom-select + input[type="submit"] {
+    margin: 0 0 0 5px;
+}
+
 #portfolio-add-button {
     display: inline;
 }
index 65c23c7..b6f4cfd 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;
 }
@@ -16559,6 +16559,12 @@ form {
 .singleselect label {
   margin-right: .3em;
 }
+.custom-select {
+  max-width: 100%;
+}
+.custom-select + input[type="submit"] {
+  margin: 0 0 0 5px;
+}
 #portfolio-add-button {
   display: inline;
 }
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