Merge branch 'master_MDL-65116' of https://github.com/yao9394/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Wed, 28 Aug 2019 14:39:49 +0000 (16:39 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Wed, 28 Aug 2019 14:39:49 +0000 (16:39 +0200)
147 files changed:
admin/classes/local/settings/filesize.php [new file with mode: 0644]
admin/roles/classes/define_role_table_advanced.php
admin/settings/security.php
admin/templates/setting_configfilesize.mustache [new file with mode: 0644]
admin/tests/behat/manage_tokens.feature
admin/tool/lp/amd/build/competencies.min.js
admin/tool/lp/amd/build/competencies.min.js.map
admin/tool/lp/amd/build/competencyactions.min.js
admin/tool/lp/amd/build/competencyactions.min.js.map
admin/tool/lp/amd/build/competencypicker.min.js
admin/tool/lp/amd/build/competencypicker.min.js.map
admin/tool/lp/amd/build/course_competency_settings.min.js
admin/tool/lp/amd/build/course_competency_settings.min.js.map
admin/tool/lp/amd/build/grade_dialogue.min.js
admin/tool/lp/amd/build/grade_dialogue.min.js.map
admin/tool/lp/amd/build/grade_user_competency_inline.min.js
admin/tool/lp/amd/build/grade_user_competency_inline.min.js.map
admin/tool/lp/amd/src/competencies.js
admin/tool/lp/amd/src/competencyactions.js
admin/tool/lp/amd/src/competencypicker.js
admin/tool/lp/amd/src/course_competency_settings.js
admin/tool/lp/amd/src/grade_dialogue.js
admin/tool/lp/amd/src/grade_user_competency_inline.js
admin/tool/lp/tests/behat/framework_crud.feature
admin/tool/lp/tests/behat/template_crud.feature
admin/tool/mobile/classes/api.php
admin/tool/uploaduser/index.php
admin/tool/uploaduser/locallib.php
admin/tool/uploaduser/tests/behat/upload_users.feature
admin/tool/usertours/classes/privacy/provider.php
admin/tool/usertours/tests/privacy_provider_test.php
analytics/classes/local/analyser/by_course.php
analytics/classes/local/analysis/result_file.php
analytics/tests/behat/manage_models.feature
analytics/tests/fixtures/test_target_course_users.php
analytics/tests/prediction_test.php
blocks/myoverview/amd/build/view.min.js
blocks/myoverview/amd/build/view.min.js.map
blocks/myoverview/amd/src/view.js
blocks/myoverview/classes/output/main.php
blocks/myoverview/lang/en/block_myoverview.php
blocks/myoverview/lib.php
blocks/myoverview/settings.php
blocks/myoverview/styles.css [new file with mode: 0644]
blocks/myoverview/templates/nav-grouping-selector.mustache
blocks/myoverview/tests/behat/block_myoverview_adminsettings.feature [new file with mode: 0644]
blocks/myoverview/tests/behat/block_myoverview_dashboard.feature
blocks/myoverview/tests/behat/block_myoverview_hidden.feature
blocks/myoverview/tests/behat/block_myoverview_pagelimit_persistence.feature
blocks/myoverview/tests/behat/block_myoverview_progress.feature
blocks/myoverview/tests/privacy_test.php
blocks/myoverview/version.php
blocks/recentlyaccesseditems/db/upgrade.php
blog/rsslib.php
cache/renderer.php
calendar/renderer.php
comment/classes/external.php
comment/lib.php
comment/tests/externallib_test.php
comment/upgrade.txt [new file with mode: 0644]
composer.json
composer.lock
course/classes/analytics/indicator/activities_due.php
course/externallib.php
course/lib.php
course/management.php
course/templates/activity_navigation.mustache
course/tests/indicators_test.php
course/upgrade.txt
grade/grading/form/rubric/tests/behat/grade_calculation.feature
grade/grading/form/rubric/tests/behat/negative_points.feature
grade/report/history/tests/behat/basic_functionality.feature
grade/report/singleview/classes/local/ui/dropdown_attribute.php
grade/report/singleview/templates/dropdown_attribute.mustache
grade/report/singleview/tests/behat/singleview.feature
install/lang/ro_wp/moodle.php
lang/en/access.php
lang/en/admin.php
lang/en/cache.php
lang/en/deprecated.txt
lib/accesslib.php
lib/adminlib.php
lib/amd/build/form-autocomplete.min.js
lib/amd/build/form-autocomplete.min.js.map
lib/amd/src/form-autocomplete.js
lib/behat/behat_base.php
lib/behat/classes/partial_named_selector.php
lib/classes/event/context_locked.php [new file with mode: 0644]
lib/classes/event/context_unlocked.php [new file with mode: 0644]
lib/classes/output/mustache_template_source_loader.php
lib/db/services.php
lib/dml/moodle_database.php
lib/dml/mysqli_native_moodle_database.php
lib/dml/pgsql_native_moodle_database.php
lib/editor/tinymce/tests/behat/disablecontrol.feature
lib/filelib.php
lib/templates/filemanager_modal_generallayout.mustache
lib/tests/behat/behat_general.php
lib/tests/event_context_locked_test.php [new file with mode: 0644]
lib/tests/filelib_test.php
lib/tests/fixtures/upload_users_enrol_date_period.csv [new file with mode: 0644]
lib/upgrade.txt
message/classes/api.php
message/tests/api_test.php
mod/assign/amd/build/override_form.min.js [new file with mode: 0644]
mod/assign/amd/build/override_form.min.js.map [new file with mode: 0644]
mod/assign/amd/src/override_form.js [new file with mode: 0644]
mod/assign/gradingtable.php
mod/assign/lang/en/assign.php
mod/assign/lib.php
mod/assign/locallib.php
mod/assign/override_form.php
mod/assign/overrideedit.php
mod/assign/styles.css
mod/assign/submission/file/locallib.php
mod/assign/templates/override_form_user_defaults.mustache [new file with mode: 0644]
mod/assign/upgrade.txt
mod/book/lib.php
mod/book/tests/lib_test.php
mod/book/upgrade.txt
mod/data/locallib.php
mod/feedback/lib.php
mod/feedback/upgrade.txt
mod/forum/deprecatedlib.php
mod/forum/lib.php
mod/forum/post.php
mod/forum/upgrade.txt
mod/forum/view.php
mod/glossary/lib.php
mod/glossary/tests/behat/categories.feature
mod/glossary/upgrade.txt
mod/lti/lib.php
mod/lti/upgrade.txt
mod/wiki/lib.php
mod/wiki/upgrade.txt
mod/workshop/lib.php
mod/workshop/upgrade.txt
question/tests/generator/lib.php
report/participation/index.php
search/tests/behat/search_by_user.feature
theme/boost/scss/moodle/modules.scss
theme/boost/style/moodle.css
theme/classic/style/moodle.css
user/filters/date.php
user/index.php
user/tests/behat/behat_user.php
version.php

diff --git a/admin/classes/local/settings/filesize.php b/admin/classes/local/settings/filesize.php
new file mode 100644 (file)
index 0000000..e5a6edb
--- /dev/null
@@ -0,0 +1,194 @@
+<?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/>.
+
+/**
+ * File size admin setting.
+ *
+ * @package    core_admin
+ * @copyright  2019 Shamim Rezaie <shamim@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_admin\local\settings;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir . '/adminlib.php');
+
+/**
+ * An admin setting to support entering and displaying of file sizes in Bytes, KB, MB or GB.
+ *
+ * @copyright   2019 Shamim Rezaie <shamim@moodle.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class filesize extends \admin_setting {
+
+    /** @var int The byte unit. Number of bytes in a byte */
+    const UNIT_B = 1;
+
+    /** @var int The kilobyte unit (number of bytes in a kilobyte) */
+    const UNIT_KB = 1024;
+
+    /** @var int The megabyte unit (number of bytes in a megabyte) */
+    const UNIT_MB = 1048576;
+
+    /** @var int The gigabyte unit (number of bytes in a gigabyte) */
+    const UNIT_GB = 1073741824;
+
+    /** @var int default size unit */
+    protected $defaultunit;
+
+    /**
+     * Constructor
+     *
+     * @param string    $name           unique ascii name, either 'mysetting' for settings that in config,
+     *                                  or 'myplugin/mysetting' for ones in config_plugins.
+     * @param string    $visiblename    localised name
+     * @param string    $description    localised long description
+     * @param int|null  $defaultvalue   Value of the settings in bytes
+     * @param int|null  $defaultunit    GB, MB, etc. (in bytes)
+     */
+    public function __construct(string $name, string $visiblename, string $description,
+            int $defaultvalue = null, int $defaultunit = null) {
+
+        $defaultsetting = self::parse_bytes($defaultvalue);
+
+        if ($defaultunit && array_key_exists($defaultunit, self::get_units())) {
+            $this->defaultunit = $defaultunit;
+        } else {
+            $this->defaultunit = self::UNIT_MB;
+        }
+        parent::__construct($name, $visiblename, $description, $defaultsetting);
+    }
+
+    /**
+     * Returns selectable units.
+     *
+     * @return  array
+     */
+    protected static function get_units(): array {
+        return [
+            self::UNIT_GB => get_string('sizegb'),
+            self::UNIT_MB => get_string('sizemb'),
+            self::UNIT_KB => get_string('sizekb'),
+            self::UNIT_B  => get_string('sizeb'),
+        ];
+    }
+
+    /**
+     * Converts bytes to some more user friendly string.
+     *
+     * @param   int     $bytes  The number of bytes we want to convert from
+     * @return  string
+     */
+    protected static function get_size_text(int $bytes): string {
+        if (empty($bytes)) {
+            return get_string('none');
+        }
+        return display_size($bytes);
+    }
+
+    /**
+     * Finds suitable units for given file size.
+     *
+     * @param   int     $bytes  The number of bytes
+     * @return  array           Parsed file size in the format of ['v' => value, 'u' => unit]
+     */
+    protected static function parse_bytes(int $bytes): array {
+        foreach (self::get_units() as $unit => $unused) {
+            if ($bytes % $unit === 0) {
+                return ['v' => (int)($bytes / $unit), 'u' => $unit];
+            }
+        }
+        return ['v' => (int)$bytes, 'u' => self::UNIT_B];
+    }
+
+    /**
+     * Get the selected file size as array.
+     *
+     * @return  array|null  An array containing 'v' => xx, 'u' => xx, or null if not set
+     */
+    public function get_setting(): ?array {
+        $bytes = $this->config_read($this->name);
+        if (is_null($bytes)) {
+            return null;
+        }
+
+        return self::parse_bytes($bytes);
+    }
+
+    /**
+     * Store the file size as bytes.
+     *
+     * @param   array   $data   Must be form 'h' => xx, 'm' => xx
+     * @return  string          The error string if any
+     */
+    public function write_setting($data): string {
+        if (!is_array($data)) {
+            return '';
+        }
+
+        if (!is_numeric($data['v']) || $data['v'] < 0) {
+            return get_string('errorsetting', 'admin');
+        }
+
+        $bytes = $data['v'] * $data['u'];
+
+        $result = $this->config_write($this->name, $bytes);
+        return ($result ? '' : get_string('errorsetting', 'admin'));
+    }
+
+    /**
+     * Returns file size text+select fields.
+     *
+     * @param   array   $data   The current setting value. Must be form 'v' => xx, 'u' => xx.
+     * @param   string  $query  Admin search query to be highlighted.
+     * @return  string          File size text+select fields and wrapping div(s).
+     */
+    public function output_html($data, $query = ''): string {
+        global $OUTPUT;
+
+        $default = $this->get_defaultsetting();
+        if (is_number($default)) {
+            $defaultinfo = self::get_size_text($default);
+        } else if (is_array($default)) {
+            $defaultinfo = self::get_size_text($default['v'] * $default['u']);
+        } else {
+            $defaultinfo = null;
+        }
+
+        $inputid = $this->get_id() . 'v';
+        $units = self::get_units();
+        $defaultunit = $this->defaultunit;
+
+        $context = (object) [
+            'id' => $this->get_id(),
+            'name' => $this->get_full_name(),
+            'value' => $data['v'],
+            'options' => array_map(function($unit, $title) use ($data, $defaultunit) {
+                return [
+                    'value' => $unit,
+                    'name' => $title,
+                    'selected' => ($data['v'] == 0 && $unit == $defaultunit) || $unit == $data['u']
+                ];
+            }, array_keys($units), $units)
+        ];
+
+        $element = $OUTPUT->render_from_template('core_admin/setting_configfilesize', $context);
+
+        return format_admin_setting($this, $this->visiblename, $element, $this->description, $inputid, '', $defaultinfo, $query);
+    }
+}
index 265119f..f4ab562 100644 (file)
@@ -652,7 +652,7 @@ class core_role_define_role_table_advanced extends core_role_capability_table_wi
             echo "</label>\n";
         }
         if ($helpicon) {
-            echo '<span class="pull-xs-right text-nowrap">'.$helpicon.'</span>';
+            echo '<span class="float-sm-right text-nowrap">'.$helpicon.'</span>';
         }
         echo '</div>';
         if (isset($this->errors[$name])) {
index 564845b..1b91d6a 100644 (file)
@@ -1,4 +1,29 @@
 <?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/>.
+
+/**
+ * Adds security related settings links for security category to admin tree.
+ *
+ * @copyright  1999 Martin Dougiamas  http://dougiamas.com
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+use core_admin\local\settings\filesize;
 
 if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
 
@@ -36,12 +61,9 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
     // maxbytes set to 0 will allow the maximum server limit for uploads
     $temp->add(new admin_setting_configselect('maxbytes', new lang_string('maxbytes', 'admin'), new lang_string('configmaxbytes', 'admin'), 0, $max_upload_choices));
     // 100MB
-    $defaultuserquota = 104857600;
-    $params = new stdClass();
-    $params->bytes = $defaultuserquota;
-    $params->displaysize = display_size($defaultuserquota);
-    $temp->add(new admin_setting_configtext('userquota', new lang_string('userquota', 'admin'),
-                new lang_string('configuserquota', 'admin', $params), $defaultuserquota, PARAM_INT, 30));
+    $defaultuserquota = 100 * filesize::UNIT_MB;
+    $temp->add(new filesize('userquota', new lang_string('userquota', 'admin'),
+            new lang_string('userquota_desc', 'admin'), $defaultuserquota));
 
     $temp->add(new admin_setting_configcheckbox('allowobjectembed', new lang_string('allowobjectembed', 'admin'), new lang_string('configallowobjectembed', 'admin'), 0));
     $temp->add(new admin_setting_configcheckbox('enabletrusttext', new lang_string('enabletrusttext', 'admin'), new lang_string('configenabletrusttext', 'admin'), 0));
diff --git a/admin/templates/setting_configfilesize.mustache b/admin/templates/setting_configfilesize.mustache
new file mode 100644 (file)
index 0000000..4716c6e
--- /dev/null
@@ -0,0 +1,50 @@
+{{!
+    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 core_admin/setting_configfilesize
+
+    Admin file size setting template.
+
+    Context variables required for this template:
+    * name - form element name
+    * options - list of options for units containing name, value, selected
+    * value - yes
+    * id - element id
+
+    Example context (json):
+    {
+        "name": "test",
+        "value": "5",
+        "id": "test0",
+        "options": [ { "name": "KB", "value": "1024", "selected": true } ]
+    }
+}}
+{{!
+    Setting configfilesize.
+}}
+<div class="form-filesize defaultsnext">
+    <div class="form-inline">
+        <input type="text" size="5" id="{{id}}v" name="{{name}}[v]" value="{{value}}" class="form-control text-ltr">
+        <label class="sr-only" for="{{id}}u">{{#str}}filesizeunits, admin{{/str}}</label>
+        <select id="{{id}}u" name="{{name}}[u]" class="form-control custom-select">
+            {{#options}}
+                <option value="{{value}}" {{#selected}}selected{{/selected}}>{{name}}</option>
+            {{/options}}
+        </select>
+    </div>
+</div>
+
index d30e230..dc1cd94 100644 (file)
@@ -9,6 +9,7 @@ Feature: Manage tokens
     | username  | password  | firstname | lastname |
     | testuser  | testuser  | Joe | Bloggs |
     | testuser2 | testuser2 | TestFirstname | TestLastname |
+    And I change window size to "small"
     And I log in as "admin"
     And I am on site homepage
 
index fd8cb9c..867ec94 100644 (file)
Binary files a/admin/tool/lp/amd/build/competencies.min.js and b/admin/tool/lp/amd/build/competencies.min.js differ
index aa66f0b..b7aa8c0 100644 (file)
Binary files a/admin/tool/lp/amd/build/competencies.min.js.map and b/admin/tool/lp/amd/build/competencies.min.js.map differ
index 5314ff2..838c624 100644 (file)
Binary files a/admin/tool/lp/amd/build/competencyactions.min.js and b/admin/tool/lp/amd/build/competencyactions.min.js differ
index baafad8..3bdbdc7 100644 (file)
Binary files a/admin/tool/lp/amd/build/competencyactions.min.js.map and b/admin/tool/lp/amd/build/competencyactions.min.js.map differ
index e9f3dff..ec38fee 100644 (file)
Binary files a/admin/tool/lp/amd/build/competencypicker.min.js and b/admin/tool/lp/amd/build/competencypicker.min.js differ
index 822d4ea..44b0812 100644 (file)
Binary files a/admin/tool/lp/amd/build/competencypicker.min.js.map and b/admin/tool/lp/amd/build/competencypicker.min.js.map differ
index dce39b1..496c31d 100644 (file)
Binary files a/admin/tool/lp/amd/build/course_competency_settings.min.js and b/admin/tool/lp/amd/build/course_competency_settings.min.js differ
index 329c557..9ab4d54 100644 (file)
Binary files a/admin/tool/lp/amd/build/course_competency_settings.min.js.map and b/admin/tool/lp/amd/build/course_competency_settings.min.js.map differ
index 9c0ecbd..806eeeb 100644 (file)
Binary files a/admin/tool/lp/amd/build/grade_dialogue.min.js and b/admin/tool/lp/amd/build/grade_dialogue.min.js differ
index 6f0202f..6af7b51 100644 (file)
Binary files a/admin/tool/lp/amd/build/grade_dialogue.min.js.map and b/admin/tool/lp/amd/build/grade_dialogue.min.js.map differ
index 4d52c20..ade6899 100644 (file)
Binary files a/admin/tool/lp/amd/build/grade_user_competency_inline.min.js and b/admin/tool/lp/amd/build/grade_user_competency_inline.min.js differ
index 4459d47..1095c0f 100644 (file)
Binary files a/admin/tool/lp/amd/build/grade_user_competency_inline.min.js.map and b/admin/tool/lp/amd/build/grade_user_competency_inline.min.js.map differ
index e3a5176..82052a4 100644 (file)
@@ -27,8 +27,9 @@ define(['jquery',
         'core/templates',
         'core/str',
         'tool_lp/competencypicker',
-        'tool_lp/dragdrop-reorder'],
-       function($, notification, ajax, templates, str, Picker, dragdrop) {
+        'tool_lp/dragdrop-reorder',
+        'core/pending'],
+       function($, notification, ajax, templates, str, Picker, dragdrop, Pending) {
 
     /**
      * Constructor
@@ -117,6 +118,7 @@ define(['jquery',
      * Pick a competency
      *
      * @method pickCompetency
+     * @return {Promise}
      */
     competencies.prototype.pickCompetency = function() {
         var self = this;
@@ -132,6 +134,7 @@ define(['jquery',
             self.pickerInstance = new Picker(self.pageContextId, false, pageContextIncludes);
             self.pickerInstance.on('save', function(e, data) {
                 var compIds = data.competencyIds;
+                var pendingPromise = new Pending();
 
                 if (self.itemtype === "course") {
                     requests = [];
@@ -181,17 +184,20 @@ define(['jquery',
                     pagerender = 'tool_lp/plan_page';
                     pageregion = 'plan-page';
                 }
-                ajax.call(requests)[requests.length - 1].then(function(context) {
+                ajax.call(requests)[requests.length - 1]
+                .then(function(context) {
                     return templates.render(pagerender, context);
-                }).then(function(html, js) {
-                    $('[data-region="' + pageregion + '"]').replaceWith(html);
-                    templates.runTemplateJS(js);
+                })
+                .then(function(html, js) {
+                    templates.replaceNode($('[data-region="' + pageregion + '"]'), html, js);
                     return;
-                }).catch(notification.exception);
+                })
+                .then(pendingPromise.resolve)
+                .catch(notification.exception);
             });
         }
 
-        self.pickerInstance.display();
+        return self.pickerInstance.display();
     };
 
     /**
@@ -302,6 +308,7 @@ define(['jquery',
         if (localthis.itemtype == 'course') {
             // Course completion rule handling.
             $('[data-region="coursecompetenciespage"]').on('change', 'select[data-field="ruleoutcome"]', function(e) {
+                var pendingPromise = new Pending();
                 var requests = [];
                 var pagerender = 'tool_lp/course_competencies_page';
                 var pageregion = 'coursecompetenciespage';
@@ -314,18 +321,24 @@ define(['jquery',
                       args: {courseid: localthis.itemid, moduleid: 0}}
                 ]);
 
-                requests[1].done(function(context) {
-                    templates.render(pagerender, context).done(function(html, js) {
-                        $('[data-region="' + pageregion + '"]').replaceWith(html);
-                        templates.runTemplateJS(js);
-                    }).fail(notification.exception);
-                }).fail(notification.exception);
+                requests[1].then(function(context) {
+                    return templates.render(pagerender, context);
+                })
+                .then(function(html, js) {
+                    return templates.replaceNode($('[data-region="' + pageregion + '"]'), html, js);
+                })
+                .then(pendingPromise.resolve)
+                .catch(notification.exception);
             });
         }
 
         $('[data-region="actions"] button').click(function(e) {
+            var pendingPromise = new Pending();
             e.preventDefault();
-            localthis.pickCompetency();
+
+            localthis.pickCompetency()
+                .then(pendingPromise.resolve)
+                .catch();
         });
         $('[data-action="delete-competency-link"]').click(function(e) {
             e.preventDefault();
index 60c04da..78a7cd3 100644 (file)
@@ -33,8 +33,12 @@ define(['jquery',
         'tool_lp/menubar',
         'tool_lp/competencypicker',
         'tool_lp/competency_outcomes',
-        'tool_lp/competencyruleconfig'],
-       function($, url, templates, notification, str, ajax, dragdrop, Ariatree, Dialogue, menubar, Picker, Outcomes, RuleConfig) {
+        'tool_lp/competencyruleconfig',
+        'core/pending',
+        ],
+       function(
+            $, url, templates, notification, str, ajax, dragdrop, Ariatree, Dialogue, menubar, Picker, Outcomes, RuleConfig, Pending
+        ) {
 
     // Private variables and functions.
     /** @var {Object} treeModel - This is an object representing the nodes in the tree. */
@@ -412,6 +416,7 @@ define(['jquery',
         if (!pickerInstance) {
             pickerInstance = new Picker(pageContextId, relatedTarget.competencyframeworkid);
             pickerInstance.on('save', function(e, data) {
+                var pendingPromise = new Pending();
                 var compIds = data.competencyIds;
 
                 var calls = [];
@@ -436,7 +441,9 @@ define(['jquery',
                     templates.runTemplateJS(js);
                     updatedRelatedCompetencies();
                     return;
-                }).catch(notification.exception);
+                })
+                .then(pendingPromise.resolve)
+                .catch(notification.exception);
             });
         }
 
index 23dce8a..c3cb0b1 100644 (file)
@@ -31,8 +31,10 @@ define(['jquery',
         'core/templates',
         'tool_lp/dialogue',
         'core/str',
-        'tool_lp/tree'],
-        function($, Notification, Ajax, Templates, Dialogue, Str, Tree) {
+        'tool_lp/tree',
+        'core/pending'
+        ],
+        function($, Notification, Ajax, Templates, Dialogue, Str, Tree, Pending) {
 
     /**
      * Competency picker class.
@@ -157,6 +159,7 @@ define(['jquery',
         // Add listener for add.
         self._find('[data-region="competencylinktree"] [data-action="add"]').click(function(e) {
             e.preventDefault();
+            var pendingPromise = new Pending();
             if (!self._selectedCompetencies.length) {
                 return;
             }
@@ -168,7 +171,10 @@ define(['jquery',
                 self._trigger('save', {competencyId: self._selectedCompetencies[0]});
             }
 
+            // The dialogue here is a YUI dialogue and doesn't support Promises at all.
+            // However, it is typically synchronous so this shoudl suffice.
             self.close();
+            pendingPromise.resolve();
         });
 
         // The list of selected competencies will be modified while looping (because of the listeners above).
index ff21ade..fd497d2 100644 (file)
@@ -26,8 +26,10 @@ define(['jquery',
         'tool_lp/dialogue',
         'core/str',
         'core/ajax',
-        'core/templates'],
-       function($, notification, Dialogue, str, ajax, templates) {
+        'core/templates',
+        'core/pending'
+        ],
+       function($, notification, Dialogue, str, ajax, templates, Pending) {
 
     /**
      * Constructor
@@ -48,6 +50,7 @@ define(['jquery',
      * @method configureSettings
      */
     settingsMod.prototype.configureSettings = function(e) {
+        var pendingPromise = new Pending();
         var courseid = $(e.target).closest('a').data('courseid');
         var currentValue = $(e.target).closest('a').data('pushratingstouserplans');
         var context = {
@@ -56,16 +59,21 @@ define(['jquery',
         };
         e.preventDefault();
 
-        templates.render('tool_lp/course_competency_settings', context).done(function(html) {
-            str.get_string('configurecoursecompetencysettings', 'tool_lp').done(function(title) {
-                this._dialogue = new Dialogue(
-                    title,
-                    html,
-                    this.addListeners.bind(this)
-                );
-            }.bind(this)).fail(notification.exception);
-        }.bind(this)).fail(notification.exception);
-
+        $.when(
+            str.get_string('configurecoursecompetencysettings', 'tool_lp'),
+            templates.render('tool_lp/course_competency_settings', context),
+        )
+        .then(function(title, templateResult) {
+            this._dialogue = new Dialogue(
+                title,
+                templateResult[0],
+                this.addListeners.bind(this)
+            );
+
+            return this._dialogue;
+        }.bind(this))
+        .then(pendingPromise.resolve)
+        .catch(notification.exception);
     };
 
     /**
@@ -108,6 +116,7 @@ define(['jquery',
      * @method saveSettings
      */
     settingsMod.prototype.saveSettings = function(e) {
+        var pendingPromise = new Pending();
         e.preventDefault();
 
         var newValue = this._find('input[name="pushratingstouserplans"]:checked').val();
@@ -117,9 +126,12 @@ define(['jquery',
         ajax.call([
             {methodname: 'core_competency_update_course_competency_settings',
               args: {courseid: courseId, settings: settings}}
-        ])[0].done(function() {
-            this.refreshCourseCompetenciesPage();
-        }.bind(this)).fail(notification.exception);
+        ])[0]
+        .then(function() {
+            return this.refreshCourseCompetenciesPage();
+        }.bind(this))
+        .then(pendingPromise.resolve)
+        .catch(notification.exception);
 
     };
 
@@ -131,18 +143,23 @@ define(['jquery',
      */
     settingsMod.prototype.refreshCourseCompetenciesPage = function() {
         var courseId = this._find('input[name="courseid"]').val();
+        var pendingPromise = new Pending();
 
         ajax.call([
             {methodname: 'tool_lp_data_for_course_competencies_page',
               args: {courseid: courseId, moduleid: 0}}
-        ])[0].done(function(context) {
-            templates.render('tool_lp/course_competencies_page', context).done(function(html, js) {
-                $('[data-region="coursecompetenciespage"]').replaceWith(html);
-                templates.runTemplateJS(js);
-                this._dialogue.close();
-            }.bind(this)).fail(notification.exception);
-        }.bind(this)).fail(notification.exception);
-
+        ])[0]
+        .then(function(context) {
+            return templates.render('tool_lp/course_competencies_page', context);
+        })
+        .then(function(html, js) {
+            templates.replaceNode($('[data-region="coursecompetenciespage"]'), html, js);
+            this._dialogue.close();
+
+            return;
+        }.bind(this))
+        .then(pendingPromise.resolve)
+        .catch(notification.exception);
     };
 
     return /** @alias module:tool_lp/configurecoursecompetencysettings */ settingsMod;
index 576b511..80ef97c 100644 (file)
@@ -102,15 +102,20 @@ define(['jquery',
      * @return {Promise}
      */
     Grade.prototype.display = function() {
-        return this._render().then(function(html) {
-            return Str.get_string('rate', 'tool_lp').then(function(title) {
-                this._popup = new Dialogue(
-                    title,
-                    html,
-                    this._afterRender.bind(this)
-                );
-            }.bind(this));
-        }.bind(this)).fail(Notification.exception);
+        return $.when(
+            Str.get_string('rate', 'tool_lp'),
+            this._render()
+        )
+        .then(function(title, templateResult) {
+            this._popup = new Dialogue(
+                title,
+                templateResult[0],
+                this._afterRender.bind(this)
+            );
+
+            return this._popup;
+        }.bind(this))
+        .catch(Notification.exception);
     };
 
     /**
index 7a030b6..81e4f35 100644 (file)
@@ -95,7 +95,7 @@ define(['jquery',
             self = this;
 
         var promise = ScaleValues.get_values(self._scaleId);
-        promise.done(function(scalevalues) {
+        promise.then(function(scalevalues) {
             options.push({
                 value: '',
                 name: self._chooseStr
@@ -109,8 +109,13 @@ define(['jquery',
                 });
             }
 
-            self._dialogue = new GradeDialogue(options);
-            self._dialogue.on('rated', function(e, data) {
+            return options;
+        })
+        .then(function(options) {
+            return new GradeDialogue(options);
+        })
+        .then(function(dialogue) {
+            dialogue.on('rated', function(e, data) {
                 var args = self._args;
                 args.grade = data.rating;
                 args.note = data.note;
@@ -123,7 +128,15 @@ define(['jquery',
                     fail: notification.exception
                 }]);
             });
-        }).fail(notification.exception);
+
+            return dialogue;
+        })
+        .then(function(dialogue) {
+            self._dialogue = dialogue;
+
+            return;
+        })
+        .fail(notification.exception);
     };
 
     /** @type {Number} The scale id for this competency. */
index df34c22..168a028 100644 (file)
@@ -6,6 +6,7 @@ Feature: Manage competency frameworks
 
   Background:
     Given I log in as "admin"
+    And I change window size to "small"
     And I am on site homepage
 
   Scenario: Create a new framework
index 0156243..a5386ff 100644 (file)
@@ -6,6 +6,7 @@ Feature: Manage plearning plan templates
 
   Background:
     Given I log in as "admin"
+    And I change window size to "small"
     And I am on site homepage
 
   Scenario: Create a new learning plan template
index 02027bd..978f950 100644 (file)
@@ -414,6 +414,7 @@ class api {
                 'NoDelegate_CoreRating' => new lang_string('ratings', 'rating'),
                 'NoDelegate_CoreTag' => new lang_string('tags'),
                 '$mmLoginEmailSignup' => new lang_string('startsignup'),
+                'NoDelegate_ForgottenPassword' => new lang_string('forgotten'),
                 'NoDelegate_ResponsiveMainMenuItems' => new lang_string('responsivemainmenuitems', 'tool_mobile'),
             ),
             "$mainmenu" => array(
index ec37326..dcf16e4 100644 (file)
@@ -1039,6 +1039,7 @@ if ($formdata = $mform2->is_cancelled()) {
                 if ($roleid) {
                     // Find duration and/or enrol status.
                     $timeend = 0;
+                    $timestart = $today;
                     $status = null;
 
                     if (isset($user->{'enrolstatus'.$i})) {
@@ -1054,16 +1055,23 @@ if ($formdata = $mform2->is_cancelled()) {
                         }
                     }
 
+                    if (!empty($user->{'enroltimestart'.$i})) {
+                        $parsedtimestart = strtotime($user->{'enroltimestart'.$i});
+                        if ($parsedtimestart !== false) {
+                            $timestart = $parsedtimestart;
+                        }
+                    }
+
                     if (!empty($user->{'enrolperiod'.$i})) {
                         $duration = (int)$user->{'enrolperiod'.$i} * 60*60*24; // convert days to seconds
                         if ($duration > 0) { // sanity check
-                            $timeend = $today + $duration;
+                            $timeend = $timestart + $duration;
                         }
                     } else if ($manualcache[$courseid]->enrolperiod > 0) {
-                        $timeend = $today + $manualcache[$courseid]->enrolperiod;
+                        $timeend = $timestart + $manualcache[$courseid]->enrolperiod;
                     }
 
-                    $manual->enrol_user($manualcache[$courseid], $user->id, $roleid, $today, $timeend, $status);
+                    $manual->enrol_user($manualcache[$courseid], $user->id, $roleid, $timestart, $timeend, $status);
 
                     $a = new stdClass();
                     $a->course = $shortname;
index e389f80..46b8bc4 100644 (file)
@@ -204,7 +204,7 @@ function uu_validate_user_upload_columns(csv_import_reader $cir, $stdfields, $pr
             // hack: somebody wrote uppercase in csv file, but the system knows only lowercase profile field
             $newfield = $lcfield;
 
-        } else if (preg_match('/^(sysrole|cohort|course|group|type|role|enrolperiod|enrolstatus)\d+$/', $lcfield)) {
+        } else if (preg_match('/^(sysrole|cohort|course|group|type|role|enrolperiod|enrolstatus|enroltimestart)\d+$/', $lcfield)) {
             // special fields for enrolments
             $newfield = $lcfield;
 
index ffd9140..7ac75ba 100644 (file)
@@ -145,3 +145,29 @@ Feature: Upload users
     And I should see "Users created: 4"
     And I press "Continue"
     And I log out
+
+  @javascript
+  Scenario: Upload users setting their enrol date and period
+    Given the following "courses" exist:
+      | fullname | shortname | category |
+      | Maths    | math102   | 0        |
+    # Upload the users.
+    And I change window size to "large"
+    And I log in as "admin"
+    And I navigate to "Users > Accounts > Upload users" in site administration
+    When I upload "lib/tests/fixtures/upload_users_enrol_date_period.csv" file to "File" filemanager
+    And I press "Upload users"
+    Then I should see "Upload users preview"
+    And I press "Upload users"
+    # Check user enrolment start date and period
+    And I am on "Maths" course homepage
+    Then I navigate to course participants
+    And I click on "Manual enrolments" "link" in the "Student One" "table_row"
+    Then I should see "1 January 2019" in the "Enrolment starts" "table_row"
+    And I should not see "Enrolment ends"
+    And I click on "Close" "button"
+    And I click on "Manual enrolments" "link" in the "Student Two" "table_row"
+    Then I should see "2 January 2020" in the "Enrolment starts" "table_row"
+    And I should see "12 January 2020" in the "Enrolment ends" "table_row"
+    And I click on "Close" "button"
+    And I log out
index 4441ba8..3713d07 100644 (file)
@@ -77,18 +77,22 @@ class provider implements
             }
 
             if ($descriptionidentifier !== null) {
-                $time = transform::datetime($value);
-                $tour = \tool_usertours\tour::instance($tourid);
+                try {
+                    $tour = \tool_usertours\tour::instance($tourid);
+                    $time = transform::datetime($value);
 
-                writer::export_user_preference(
-                    'tool_usertours',
-                    $name,
-                    $time,
-                    get_string($descriptionidentifier, 'tool_usertours', (object) [
-                        'name' => $tour->get_name(),
-                        'time' => $time,
-                    ])
-                );
+                    writer::export_user_preference(
+                        'tool_usertours',
+                        $name,
+                        $time,
+                        get_string($descriptionidentifier, 'tool_usertours', (object) [
+                            'name' => $tour->get_name(),
+                            'time' => $time,
+                        ])
+                    );
+                } catch (\dml_missing_record_exception $ex) {
+                    // The tour related to this user preference no longer exists.
+                }
             }
         }
     }
index 8cfc81a..db0139d 100644 (file)
@@ -112,4 +112,37 @@ class tool_usertours_privacy_testcase extends \core_privacy\tests\provider_testc
 
         $this->assertCount(2, (array) $prefs);
     }
+
+    /**
+     * Ensure that export_user_preferences excludes deleted tours.
+     */
+    public function test_export_user_preferences_deleted_tour() {
+        global $DB;
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $user = \core_user::get_user_by_username('admin');
+
+        $alltours = $DB->get_records('tool_usertours_tours');
+
+        $tour1 = tour::instance(array_shift($alltours)->id);
+        $tour1->mark_user_completed();
+
+        $tour2 = tour::instance(array_shift($alltours)->id);
+        $tour2->mark_user_completed();
+        $tour2->remove();
+
+        $writer = writer::with_context(\context_system::instance());
+
+        provider::export_user_preferences($user->id);
+        $this->assertTrue($writer->has_any_data());
+
+        // We should have one preference.
+        $prefs = $writer->get_user_preferences('tool_usertours');
+        $this->assertCount(1, (array) $prefs);
+
+        // The preference should be related to the first tour.
+        $this->assertContains($tour1->get_name(), reset($prefs)->description);
+    }
 }
index 99e70c1..c0d308b 100644 (file)
@@ -50,12 +50,11 @@ abstract class by_course extends base {
         if (!empty($this->options['filter'])) {
             $courses = array();
             foreach ($this->options['filter'] as $courseid) {
-                $courses[$courseid] = new \stdClass();
-                $courses[$courseid]->id = $courseid;
+                $courses[$courseid] = intval($courseid);
             }
 
             list($coursesql, $courseparams) = $DB->get_in_or_equal($courses, SQL_PARAMS_NAMED);
-            $sql .= " AND c.id IN $coursesql";
+            $sql .= " AND c.id $coursesql";
             $params = $params + $courseparams;
         }
 
index 7a6b61e..16ef9f1 100644 (file)
@@ -78,9 +78,9 @@ class result_file extends result {
         // if this analyser was analysed less that 1 week ago we skip generating a new one. This
         // helps scale the evaluation process as sites with tons of courses may need a lot of time to
         // complete an evaluation.
-        if (!empty($options['evaluation']) && !empty($options['reuseprevanalysed'])) {
+        if (!empty($this->options['evaluation']) && !empty($this->options['reuseprevanalysed'])) {
 
-            $previousanalysis = \core_analytics\dataset_manager::get_evaluation_analysable_file($this->analyser->get_modelid(),
+            $previousanalysis = \core_analytics\dataset_manager::get_evaluation_analysable_file($this->modelid,
                 $analysable->get_id(), $timesplitting->get_id());
             // 1 week is a partly random time interval, no need to worry about DST.
             $boundary = time() - WEEKSECS;
index 8a668c9..f8f4826 100644 (file)
@@ -51,8 +51,8 @@ Feature: Manage analytics models
     And I navigate to "Analytics > Analytics models" in site administration
 
   Scenario: Create a model
-    When I click on "New model" "link"
-    And I click on "Create model" "link"
+    When I open the action menu in ".top-nav" "css_element"
+    And I choose "Create model" in the open action menu
     And I set the field "Enabled" to "Enable"
     And I select "__core_course__analytics__target__course_completion" from the "target" singleselect
     And I open the autocomplete suggestions list
@@ -84,22 +84,22 @@ Feature: Manage analytics models
     And I click on "Save changes" "button"
     And I am on site homepage
     And I navigate to "Analytics > Analytics models" in site administration
-    And I click on "Actions" "link" in the "Students at risk of not meeting the course completion conditions" "table_row"
-    And I click on "Evaluate" "link" in the "Students at risk of not meeting the course completion conditions" "table_row"
+    And I open the action menu in "Students at risk of not meeting the course completion conditions" "table_row"
+    And I choose "Evaluate" in the open action menu
     And I press "Evaluate"
     And I should see "Evaluate model"
     And I press "Continue"
     # Evaluation log
-    And I click on "Actions" "link" in the "Students at risk of not meeting the course completion conditions" "table_row"
-    And I click on "Evaluation log" "link" in the "Students at risk of not meeting the course completion conditions" "table_row"
+    And I open the action menu in "Students at risk of not meeting the course completion conditions" "table_row"
+    And I choose "Evaluation log" in the open action menu
     And I should see "Configuration"
     And I click on "View" "link"
     And I should see "Log extra info"
     And I click on "Close" "button"
     And I click on "Analytics models" "link"
     # Execute scheduled analysis
-    And I click on "Actions" "link" in the "Students at risk of not meeting the course completion conditions" "table_row"
-    And I click on "Execute scheduled analysis" "link" in the "Students at risk of not meeting the course completion conditions" "table_row"
+    And I open the action menu in "Students at risk of not meeting the course completion conditions" "table_row"
+    And I choose "Execute scheduled analysis" in the open action menu
     And I should see "Training results"
     And I press "Continue"
     # Check notifications
@@ -111,53 +111,52 @@ Feature: Manage analytics models
     And I navigate to "Analytics > Analytics models" in site administration
     # View predictions
     When I select "C3" from the "contextid" singleselect
-    #And I click on "#dropdown-3" "css_element"
-    And I click on "Actions" "link" in the "Student 6" "table_row"
-    And I click on "View prediction details" "link"
+    And I open the action menu in "Student 6" "table_row"
+    And I choose "View prediction details" in the open action menu
     And I should see "Prediction details"
     And I should see "Any write action"
     And I should see "Read actions amount"
-    And I click on "Actions" "link"
-    And I click on "Acknowledged" "link"
-    And I click on "Actions" "link"
-    And I click on "View prediction details" "link"
-    And I click on "Actions" "link"
-    And I click on "Not useful" "link"
+    And I open the action menu in "Student 6" "table_row"
+    And I choose "Acknowledged" in the open action menu
+    And I open the action menu in "Student 5" "table_row"
+    And I choose "View prediction details" in the open action menu
+    And I open the action menu in "Student 5" "table_row"
+    And I choose "Not useful" in the open action menu
     And I should see "No insights reported"
     # Clear predictions
     When I am on site homepage
     And I navigate to "Analytics > Analytics models" in site administration
     And I should see "No insights reported" in the "Students at risk of not meeting the course completion conditions" "table_row"
-    And I click on "Actions" "link" in the "Students at risk of not meeting the course completion conditions" "table_row"
-    And I click on "Clear predictions" "link" in the "Students at risk of not meeting the course completion conditions" "table_row"
+    And I open the action menu in "Students at risk of not meeting the course completion conditions" "table_row"
+    And I choose "Clear predictions" in the open action menu
     And I press "Clear predictions"
     Then I should see "No predictions available yet" in the "Students at risk of not meeting the course completion conditions" "table_row"
 
   Scenario: Edit a model
-    When I click on "Actions" "link" in the "Students at risk of not meeting the course completion conditions" "table_row"
-    And I click on "Edit" "link" in the "Students at risk of not meeting the course completion conditions" "table_row"
+    When I open the action menu in "Students at risk of not meeting the course completion conditions" "table_row"
+    And I choose "Edit" in the open action menu
     And I click on "Read actions amount" "text" in the ".form-autocomplete-selection" "css_element"
     And I press "Save changes"
     And I should not see "Read actions amount"
 
   Scenario: Disable a model
-    When I click on "Actions" "link" in the "Students at risk of not meeting the course completion conditions" "table_row"
-    And I click on "Disable" "link" in the "Students at risk of not meeting the course completion conditions" "table_row"
+    When I open the action menu in "Students at risk of not meeting the course completion conditions" "table_row"
+    And I choose "Disable" in the open action menu
     Then I should see "Disabled model" in the "Students at risk of not meeting the course completion conditions" "table_row"
 
   Scenario: Export model
-    When I click on "Actions" "link" in the "Students at risk of not meeting the course completion conditions" "table_row"
-    And I click on "Export" "link" in the "Students at risk of not meeting the course completion conditions" "table_row"
+    When I open the action menu in "Students at risk of not meeting the course completion conditions" "table_row"
+    And I choose "Export" in the open action menu
     And I click on "Actions" "link" in the "Students at risk of not meeting the course completion conditions" "table_row"
     And following "Export" should download between "100" and "500" bytes
 
   Scenario: Check invalid site elements
-    When I click on "Actions" "link" in the "Students at risk of not meeting the course completion conditions" "table_row"
-    And I click on "Invalid site elements" "link" in the "Students at risk of not meeting the course completion conditions" "table_row"
+    When I open the action menu in "Students at risk of not meeting the course completion conditions" "table_row"
+    And I choose "Invalid site elements" in the open action menu
     Then I should see "Invalid analysable elements"
 
   Scenario: Delete model
-    When I click on "Actions" "link" in the "Students at risk of not meeting the course completion conditions" "table_row"
-    And I click on "Delete" "link" in the "Students at risk of not meeting the course completion conditions" "table_row"
+    When I open the action menu in "Students at risk of not meeting the course completion conditions" "table_row"
+    And I choose "Delete" in the open action menu
     And I click on "Delete" "button" in the "Confirm" "dialogue"
     Then I should not see "Students at risk of not meeting the course completion conditions"
index 8907a3c..7be0a87 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-require_once(__DIR__ . '/test_target_shortname.php');
+require_once(__DIR__ . '/test_target_site_users.php');
 
 /**
  * Test target.
index 4294cad..7fbbdc6 100644 (file)
@@ -478,6 +478,13 @@ class core_analytics_prediction_testcase extends advanced_testcase {
             $message = 'The returned status code ' . $result->status . ' should include ' . $expected[$timesplitting];
             $filtered = $result->status & $expected[$timesplitting];
             $this->assertEquals($expected[$timesplitting], $filtered, $message);
+
+            $options = ['evaluation' => true, 'reuseprevanalysed' => true];
+            $result = new \core_analytics\local\analysis\result_file($model->get_id(), true, $options);
+            $timesplittingobj = \core_analytics\manager::get_time_splitting($timesplitting);
+            $analysable = new \core_analytics\site();
+            $cachedanalysis = $result->retrieve_cached_result($timesplittingobj, $analysable);
+            $this->assertInstanceOf(\stored_file::class, $cachedanalysis);
         }
 
         set_config('enabled_stores', '', 'tool_log');
index f58ff47..01b71bc 100644 (file)
Binary files a/blocks/myoverview/amd/build/view.min.js and b/blocks/myoverview/amd/build/view.min.js differ
index ea5cedd..5de1f87 100644 (file)
Binary files a/blocks/myoverview/amd/build/view.min.js.map and b/blocks/myoverview/amd/build/view.min.js.map differ
index 3e53ea0..40d48b9 100644 (file)
@@ -67,6 +67,16 @@ function(
         NOCOURSES: 'core_course/no-courses'
     };
 
+    var GROUPINGS = {
+        GROUPING_ALLINCLUDINGHIDDEN: 'allincludinghidden',
+        GROUPING_ALL: 'all',
+        GROUPING_INPROGRESS: 'inprogress',
+        GROUPING_FUTURE: 'future',
+        GROUPING_PAST: 'past',
+        GROUPING_FAVOURITES: 'favourites',
+        GROUPING_HIDDEN: 'hidden'
+    };
+
     var NUMCOURSES_PERPAGE = [12, 24, 48, 96, 0];
 
     var loadedPages = [];
@@ -252,15 +262,104 @@ function(
         }).catch(Notification.exception);
     };
 
+    /**
+     * Get the action menu item
+     *
+     * @param {Object} root  root The course overview container
+     * @param {Number} courseId Course id.
+     * @return {Object} The hide course menu item.
+     */
+    var getHideCourseMenuItem = function(root, courseId) {
+        return root.find('[data-action="hide-course"][data-course-id="' + courseId + '"]');
+    };
+
+    /**
+     * Get the action menu item
+     *
+     * @param {Object} root  root The course overview container
+     * @param {Number} courseId Course id.
+     * @return {Object} The show course menu item.
+     */
+    var getShowCourseMenuItem = function(root, courseId) {
+        return root.find('[data-action="show-course"][data-course-id="' + courseId + '"]');
+    };
+
+    /**
+     * Hide course
+     *
+     * @param  {Object} root The course overview container
+     * @param  {Number} courseId Course id number
+     */
+    var hideCourse = function(root, courseId) {
+        var hideAction = getHideCourseMenuItem(root, courseId);
+        var showAction = getShowCourseMenuItem(root, courseId);
+        var filters = getFilterValues(root);
+
+        setCourseHiddenState(courseId, true);
+
+        // Remove the course from this view as it is now hidden and thus not covered by this view anymore.
+        // Do only if we are not in "All" view mode where really all courses are shown.
+        if (filters.grouping != GROUPINGS.GROUPING_ALLINCLUDINGHIDDEN) {
+            hideElement(root, courseId);
+        }
+
+        hideAction.addClass('hidden');
+        showAction.removeClass('hidden');
+    };
+
+    /**
+     * Show course
+     *
+     * @param  {Object} root The course overview container
+     * @param  {Number} courseId Course id number
+     */
+    var showCourse = function(root, courseId) {
+        var hideAction = getHideCourseMenuItem(root, courseId);
+        var showAction = getShowCourseMenuItem(root, courseId);
+        var filters = getFilterValues(root);
+
+        setCourseHiddenState(courseId, null);
+
+        // Remove the course from this view as it is now shown again and thus not covered by this view anymore.
+        // Do only if we are not in "All" view mode where really all courses are shown.
+        if (filters.grouping != GROUPINGS.GROUPING_ALLINCLUDINGHIDDEN) {
+            hideElement(root, courseId);
+        }
+
+        hideAction.removeClass('hidden');
+        showAction.addClass('hidden');
+    };
+
+    /**
+     * Set the courses hidden status and push to repository
+     *
+     * @param  {Number} courseId Course id to favourite.
+     * @param  {Bool} status new hidden status.
+     * @return {Promise} Repository promise.
+     */
+    var setCourseHiddenState = function(courseId, status) {
+
+        // If the given status is not hidden, the preference has to be deleted with a null value.
+        if (status === false) {
+            status = null;
+        }
+        return Repository.updateUserPreferences({
+            preferences: [
+                {
+                    type: 'block_myoverview_hidden_course_' + courseId,
+                    value: status
+                }
+            ]
+        });
+    };
+
     /**
      * Reset the loadedPages dataset to take into account the hidden element
      *
      * @param {Object} root The course overview container
-     * @param {Object} target The course that you want to hide
+     * @param {Number} id The course id number
      */
-    var hideElement = function(root, target) {
-        var id = getCourseId(target);
-
+    var hideElement = function(root, id) {
         var pagingBar = root.find('[data-region="paging-bar"]');
         var jumpto = parseInt(pagingBar.attr('data-active-page-number'));
 
@@ -570,38 +669,15 @@ function(
 
         root.on(CustomEvents.events.activate, SELECTORS.ACTION_HIDE_COURSE, function(e, data) {
             var target = $(e.target).closest(SELECTORS.ACTION_HIDE_COURSE);
-            var id = getCourseId(target);
-
-            var request = {
-                preferences: [
-                    {
-                        type: 'block_myoverview_hidden_course_' + id,
-                        value: true
-                    }
-                ]
-            };
-            Repository.updateUserPreferences(request);
-
-            hideElement(root, target);
+            var courseId = getCourseId(target);
+            hideCourse(root, courseId);
             data.originalEvent.preventDefault();
         });
 
         root.on(CustomEvents.events.activate, SELECTORS.ACTION_SHOW_COURSE, function(e, data) {
             var target = $(e.target).closest(SELECTORS.ACTION_SHOW_COURSE);
-            var id = getCourseId(target);
-
-            var request = {
-                preferences: [
-                    {
-                        type: 'block_myoverview_hidden_course_' + id,
-                        value: null
-                    }
-                ]
-            };
-
-            Repository.updateUserPreferences(request);
-
-            hideElement(root, target);
+            var courseId = getCourseId(target);
+            showCourse(root, courseId);
             data.originalEvent.preventDefault();
         });
     };
index e1c92a9..5f4a033 100644 (file)
@@ -81,6 +81,55 @@ class main implements renderable, templatable {
      */
     private $layouts;
 
+    /**
+     * Store a course grouping option setting
+     *
+     * @var boolean
+     */
+    private $displaygroupingallincludinghidden;
+
+    /**
+     * Store a course grouping option setting.
+     *
+     * @var boolean
+     */
+    private $displaygroupingall;
+
+    /**
+     * Store a course grouping option setting.
+     *
+     * @var boolean
+     */
+    private $displaygroupinginprogress;
+
+    /**
+     * Store a course grouping option setting.
+     *
+     * @var boolean
+     */
+    private $displaygroupingfuture;
+
+    /**
+     * Store a course grouping option setting.
+     *
+     * @var boolean
+     */
+    private $displaygroupingpast;
+
+    /**
+     * Store a course grouping option setting.
+     *
+     * @var boolean
+     */
+    private $displaygroupingstarred;
+
+    /**
+     * Store a course grouping option setting.
+     *
+     * @var boolean
+     */
+    private $displaygroupinghidden;
+
     /**
      * main constructor.
      * Initialize the user preferences
@@ -92,23 +141,89 @@ class main implements renderable, templatable {
      * @throws \dml_exception
      */
     public function __construct($grouping, $sort, $view, $paging) {
-        $this->grouping = $grouping ? $grouping : BLOCK_MYOVERVIEW_GROUPING_ALL;
+        // Get plugin config.
+        $config = get_config('block_myoverview');
+
+        // Build the course grouping option name to check if the given grouping is enabled afterwards.
+        $groupingconfigname = 'displaygrouping'.$grouping;
+        // Check the given grouping and remember it if it is enabled.
+        if ($grouping && $config->$groupingconfigname == true) {
+            $this->grouping = $grouping;
+
+            // Otherwise fall back to another grouping in a reasonable order.
+            // This is done to prevent one-time UI glitches in the case when a user has chosen a grouping option previously which
+            // was then disabled by the admin in the meantime.
+        } else if ($config->displaygroupingall == true) {
+            $this->grouping = BLOCK_MYOVERVIEW_GROUPING_ALL;
+        } else if ($config->displaygroupingallincludinghidden == true) {
+            $this->grouping = BLOCK_MYOVERVIEW_GROUPING_ALLINCLUDINGHIDDEN;
+        } else if ($config->displaygroupinginprogress == true) {
+            $this->grouping = BLOCK_MYOVERVIEW_GROUPING_INPROGRESS;
+        } else if ($config->displaygroupingfuture == true) {
+            $this->grouping = BLOCK_MYOVERVIEW_GROUPING_FUTURE;
+        } else if ($config->displaygroupingpast == true) {
+            $this->grouping = BLOCK_MYOVERVIEW_GROUPING_PAST;
+        } else if ($config->displaygroupingstarred == true) {
+            $this->grouping = BLOCK_MYOVERVIEW_GROUPING_FAVOURITES;
+        } else if ($config->displaygroupinghidden == true) {
+            $this->grouping = BLOCK_MYOVERVIEW_GROUPING_HIDDEN;
+
+            // In this case, no grouping option is enabled and the grouping is not needed at all.
+            // But it's better not to leave $this->grouping unset for any unexpected case.
+        } else {
+            $this->grouping = BLOCK_MYOVERVIEW_GROUPING_ALLINCLUDINGHIDDEN;
+        }
+        unset ($groupingconfigname);
+
+        // Check and remember the given sorting.
         $this->sort = $sort ? $sort : BLOCK_MYOVERVIEW_SORTING_TITLE;
+
+        // Check and remember the given view.
+        $this->view = $view ? $view : BLOCK_MYOVERVIEW_VIEW_CARD;
+
+        // Check and remember the given page size.
         if ($paging == BLOCK_MYOVERVIEW_PAGING_ALL) {
             $this->paging = BLOCK_MYOVERVIEW_PAGING_ALL;
         } else {
             $this->paging = $paging ? $paging : BLOCK_MYOVERVIEW_PAGING_12;
         }
 
-        $config = get_config('block_myoverview');
+        // Check and remember if the course categories should be shown or not.
         if (!$config->displaycategories) {
             $this->displaycategories = BLOCK_MYOVERVIEW_DISPLAY_CATEGORIES_OFF;
         } else {
             $this->displaycategories = BLOCK_MYOVERVIEW_DISPLAY_CATEGORIES_ON;
         }
 
+        // Get and remember the available layouts.
         $this->set_available_layouts();
         $this->view = $view ? $view : reset($this->layouts);
+
+        // Check and remember if the particular grouping options should be shown or not.
+        $this->displaygroupingallincludinghidden = $config->displaygroupingallincludinghidden;
+        $this->displaygroupingall = $config->displaygroupingall;
+        $this->displaygroupinginprogress = $config->displaygroupinginprogress;
+        $this->displaygroupingfuture = $config->displaygroupingfuture;
+        $this->displaygroupingpast = $config->displaygroupingpast;
+        $this->displaygroupingstarred = $config->displaygroupingstarred;
+        $this->displaygroupinghidden = $config->displaygroupinghidden;
+
+        // Check and remember if the grouping selector should be shown at all or not.
+        // It will be shown if more than 1 grouping option is enabled.
+        $displaygroupingselectors = array($this->displaygroupingallincludinghidden,
+                $this->displaygroupingall,
+                $this->displaygroupinginprogress,
+                $this->displaygroupingfuture,
+                $this->displaygroupingpast,
+                $this->displaygroupingstarred,
+                $this->displaygroupinghidden);
+        $displaygroupingselectorscount = count(array_filter($displaygroupingselectors));
+        if ($displaygroupingselectorscount > 1) {
+            $this->displaygroupingselector = true;
+        } else {
+            $this->displaygroupingselector = false;
+        }
+        unset ($displaygroupingselectors, $displaygroupingselectorscount);
     }
 
 
@@ -204,6 +319,14 @@ class main implements renderable, templatable {
             'layouts' => $availablelayouts,
             'displaycategories' => $this->displaycategories,
             'displaydropdown' => (count($availablelayouts) > 1) ? true : false,
+            'displaygroupingallincludinghidden' => $this->displaygroupingallincludinghidden,
+            'displaygroupingall' => $this->displaygroupingall,
+            'displaygroupinginprogress' => $this->displaygroupinginprogress,
+            'displaygroupingfuture' => $this->displaygroupingfuture,
+            'displaygroupingpast' => $this->displaygroupingpast,
+            'displaygroupingstarred' => $this->displaygroupingstarred,
+            'displaygroupinghidden' => $this->displaygroupinghidden,
+            'displaygroupingselector' => $this->displaygroupingselector,
         ];
         return array_merge($defaultvariables, $preferences);
 
index 637368b..8fe7076 100644 (file)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-$string['all'] = 'All';
-$string['allexcepthidden'] = 'All (except hidden)';
+$string['allincludinghidden'] = 'All';
+$string['all'] = 'All (except hidden)';
 $string['addtofavourites'] = 'Star this course';
 $string['aria:addtofavourites'] = 'Star for';
-$string['aria:allcourses'] = 'All courses';
-$string['aria:allcoursesexcepthidden'] = 'All courses except hidden courses';
+$string['aria:allcoursesincludinghidden'] = 'All courses';
+$string['aria:allcourses'] = 'All courses except hidden courses';
 $string['aria:card'] = 'Switch to card view';
 $string['aria:controls'] = 'Course overview controls';
 $string['aria:courseactions'] = 'Actions for current course';
@@ -45,6 +45,8 @@ $string['aria:past'] = 'Show past courses';
 $string['aria:removefromfavourites'] = 'Remove star for';
 $string['aria:summary'] = 'Switch to summary view';
 $string['aria:sortingdropdown'] = 'Sorting drop-down menu';
+$string['availablegroupings'] = 'Available filters';
+$string['availablegroupings_desc'] = 'Course filters which are available for selection by users. If none are selected, all courses will be displayed.';
 $string['card'] = 'Card';
 $string['cards'] = 'Cards';
 $string['courseprogress'] = 'Course progress:';
index 7acbaff..81090ed 100644 (file)
@@ -27,6 +27,7 @@ defined('MOODLE_INTERNAL') || die();
 /**
  * Constants for the user preferences grouping options
  */
+define('BLOCK_MYOVERVIEW_GROUPING_ALLINCLUDINGHIDDEN', 'allincludinghidden');
 define('BLOCK_MYOVERVIEW_GROUPING_ALL', 'all');
 define('BLOCK_MYOVERVIEW_GROUPING_INPROGRESS', 'inprogress');
 define('BLOCK_MYOVERVIEW_GROUPING_FUTURE', 'future');
@@ -74,6 +75,7 @@ function block_myoverview_user_preferences() {
         'default' => BLOCK_MYOVERVIEW_GROUPING_ALL,
         'type' => PARAM_ALPHA,
         'choices' => array(
+            BLOCK_MYOVERVIEW_GROUPING_ALLINCLUDINGHIDDEN,
             BLOCK_MYOVERVIEW_GROUPING_ALL,
             BLOCK_MYOVERVIEW_GROUPING_INPROGRESS,
             BLOCK_MYOVERVIEW_GROUPING_FUTURE,
index b17ea4e..b633f22 100644 (file)
@@ -27,22 +27,74 @@ defined('MOODLE_INTERNAL') || die;
 if ($ADMIN->fulltree) {
     require_once($CFG->dirroot . '/blocks/myoverview/lib.php');
 
+    // Presentation options heading.
+    $settings->add(new admin_setting_heading('block_myoverview/appearance',
+            get_string('appearance', 'admin'),
+            ''));
+
     // Display Course Categories on Dashboard course items (cards, lists, summary items).
     $settings->add(new admin_setting_configcheckbox(
-        'block_myoverview/displaycategories',
-        get_string('displaycategories', 'block_myoverview'),
-        get_string('displaycategories_help', 'block_myoverview'),
-        1));
-
-       $choices = array(BLOCK_MYOVERVIEW_VIEW_CARD => get_string('card', 'block_myoverview'),
-        BLOCK_MYOVERVIEW_VIEW_LIST => get_string('list', 'block_myoverview'),
-        BLOCK_MYOVERVIEW_VIEW_SUMMARY => get_string('summary', 'block_myoverview'));
+            'block_myoverview/displaycategories',
+            get_string('displaycategories', 'block_myoverview'),
+            get_string('displaycategories_help', 'block_myoverview'),
+            1));
 
+    // Enable / Disable available layouts.
+    $choices = array(BLOCK_MYOVERVIEW_VIEW_CARD => get_string('card', 'block_myoverview'),
+            BLOCK_MYOVERVIEW_VIEW_LIST => get_string('list', 'block_myoverview'),
+            BLOCK_MYOVERVIEW_VIEW_SUMMARY => get_string('summary', 'block_myoverview'));
     $settings->add(new admin_setting_configmulticheckbox(
-        'block_myoverview/layouts',
-        get_string('layouts', 'block_myoverview'),
-        get_string('layouts_help', 'block_myoverview'),
-        $choices,
-        $choices));
+            'block_myoverview/layouts',
+            get_string('layouts', 'block_myoverview'),
+            get_string('layouts_help', 'block_myoverview'),
+            $choices,
+            $choices));
+    unset ($choices);
+
+    // Enable / Disable course filter items.
+    $settings->add(new admin_setting_heading('block_myoverview/availablegroupings',
+            get_string('availablegroupings', 'block_myoverview'),
+            get_string('availablegroupings_desc', 'block_myoverview')));
+
+    $settings->add(new admin_setting_configcheckbox(
+            'block_myoverview/displaygroupingallincludinghidden',
+            get_string('allincludinghidden', 'block_myoverview'),
+            '',
+            0));
+
+    $settings->add(new admin_setting_configcheckbox(
+            'block_myoverview/displaygroupingall',
+            get_string('all', 'block_myoverview'),
+            '',
+            1));
+
+    $settings->add(new admin_setting_configcheckbox(
+            'block_myoverview/displaygroupinginprogress',
+            get_string('inprogress', 'block_myoverview'),
+            '',
+            1));
+
+    $settings->add(new admin_setting_configcheckbox(
+            'block_myoverview/displaygroupingpast',
+            get_string('past', 'block_myoverview'),
+            '',
+            1));
+
+    $settings->add(new admin_setting_configcheckbox(
+            'block_myoverview/displaygroupingfuture',
+            get_string('future', 'block_myoverview'),
+            '',
+            1));
+
+    $settings->add(new admin_setting_configcheckbox(
+            'block_myoverview/displaygroupingstarred',
+            get_string('favourites', 'block_myoverview'),
+            '',
+            1));
 
+    $settings->add(new admin_setting_configcheckbox(
+            'block_myoverview/displaygroupinghidden',
+            get_string('hiddencourses', 'block_myoverview'),
+            '',
+            1));
 }
diff --git a/blocks/myoverview/styles.css b/blocks/myoverview/styles.css
new file mode 100644 (file)
index 0000000..76daded
--- /dev/null
@@ -0,0 +1,5 @@
+/* Hide the first dropdown-divider if no filter option element is listed before it.
+   This can happen for some subset configurations of the block_myoverview course filter. */
+.block_myoverview button#groupingdropdown + .dropdown-menu li:first-of-type.dropdown-divider:first-of-type {
+    display: none;
+}
index fb56f6d..703482f 100644 (file)
 
     Example context (json):
     {
+        "allincludinghidden": false,
         "all": true,
         "inprogress": false,
         "future": false,
-        "past": false
+        "past": false,
+        "favourites": false,
+        "hidden": false,
+        "displaygroupingallincludinghidden": false,
+        "displaygroupingall": true,
+        "displaygroupinginprogress": true,
+        "displaygroupingfuture": true,
+        "displaygroupingpast": true,
+        "displaygroupingstarred": true,
+        "displaygroupinghidden": true,
+        "displaygroupingselector": true
     }
 }}
+{{#displaygroupingselector}}
 <div class="dropdown mb-1 mr-auto">
     <button id="groupingdropdown" type="button" class="btn btn-outline-secondary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" aria-label="{{#str}} aria:groupingdropdown, block_myoverview {{/str}}">
         {{#pix}} i/filter {{/pix}}
         <span class="d-sm-inline-block" data-active-item-text>
-            {{#all}}{{#str}} allexcepthidden, block_myoverview {{/str}}{{/all}}
+            {{#allincludinghidden}}{{#str}} allincludinghidden, block_myoverview {{/str}}{{/allincludinghidden}}
+            {{#all}}{{#str}} all, block_myoverview {{/str}}{{/all}}
             {{#inprogress}}{{#str}} inprogress, block_myoverview {{/str}}{{/inprogress}}
             {{#future}}{{#str}} future, block_myoverview {{/str}}{{/future}}
             {{#past}}{{#str}} past, block_myoverview {{/str}}{{/past}}
         </span>
     </button>
     <ul class="dropdown-menu" data-show-active-item data-active-item-text aria-labelledby="groupingdropdown">
+        {{#displaygroupingallincludinghidden}}
         <li>
-            <a class="dropdown-item {{#all}}active{{/all}}" href="#" data-filter="grouping" data-value="all" data-pref="all" aria-label="{{#str}} aria:allcoursesexcepthidden, block_myoverview {{/str}}" aria-controls="courses-view-{{uniqid}}">
-                {{#str}} allexcepthidden, block_myoverview {{/str}}
+            <a class="dropdown-item {{#allincludinghidden}}active{{/allincludinghidden}}" href="#" data-filter="grouping" data-value="allincludinghidden" data-pref="allincludinghidden" aria-label="{{#str}} aria:allcoursesincludinghidden, block_myoverview {{/str}}" aria-controls="courses-view-{{uniqid}}">
+                {{#str}} allincludinghidden, block_myoverview {{/str}}
             </a>
         </li>
+        {{/displaygroupingallincludinghidden}}
+        {{#displaygroupingall}}
+        <li class="dropdown-divider" role="presentation">
+            <span class="filler">&nbsp;</span>
+        </li>
+        <li>
+            <a class="dropdown-item {{#all}}active{{/all}}" href="#" data-filter="grouping" data-value="all" data-pref="all" aria-label="{{#str}} aria:allcourses, block_myoverview {{/str}}" aria-controls="courses-view-{{uniqid}}">
+                {{#str}} all, block_myoverview {{/str}}
+            </a>
+        </li>
+        {{/displaygroupingall}}
+        {{#displaygroupinginprogress}}
         <li class="dropdown-divider" role="presentation">
             <span class="filler">&nbsp;</span>
         </li>
                 {{#str}} inprogress, block_myoverview {{/str}}
             </a>
         </li>
+        {{/displaygroupinginprogress}}
+        {{#displaygroupingfuture}}
+            {{^displaygroupinginprogress}}
+            <li class="dropdown-divider" role="presentation">
+                <span class="filler">&nbsp;</span>
+            </li>
+            {{/displaygroupinginprogress}}
         <li>
             <a class="dropdown-item {{#future}}active{{/future}}" href="#" data-filter="grouping" data-value="future" data-pref="future" aria-label="{{#str}} aria:future, block_myoverview {{/str}}" aria-controls="courses-view-{{uniqid}}">
                 {{#str}} future, block_myoverview {{/str}}
             </a>
         </li>
+        {{/displaygroupingfuture}}
+        {{#displaygroupingpast}}
+            {{^displaygroupinginprogress}}
+                {{^displaygroupingfuture}}
+                <li class="dropdown-divider" role="presentation">
+                    <span class="filler">&nbsp;</span>
+                </li>
+                {{/displaygroupingfuture}}
+            {{/displaygroupinginprogress}}
         <li>
             <a class="dropdown-item {{#past}}active{{/past}}" href="#" data-filter="grouping" data-value="past" data-pref="past" aria-label="{{#str}} aria:past, block_myoverview {{/str}}" aria-controls="courses-view-{{uniqid}}">
                 {{#str}} past, block_myoverview {{/str}}
             </a>
         </li>
+        {{/displaygroupingpast}}
+        {{#displaygroupingstarred}}
         <li class="dropdown-divider" role="presentation">
             <span class="filler">&nbsp;</span>
         </li>
             <a class="dropdown-item {{#favourites}}active{{/favourites}}" href="#" data-filter="grouping" data-value="favourites"  data-pref="favourites" aria-label="{{#str}} aria:favourites, block_myoverview {{/str}}" aria-controls="courses-view-{{uniqid}}">
                 {{#str}} favourites, block_myoverview {{/str}}
             </a>
-        </li>
+        {{/displaygroupingstarred}}
+        {{#displaygroupinghidden}}
         <li class="dropdown-divider" role="presentation">
             <span class="filler">&nbsp;</span>
         </li>
                 {{#str}} hiddencourses, block_myoverview {{/str}}
             </a>
         </li>
+        {{/displaygroupinghidden}}
     </ul>
 </div>
+{{/displaygroupingselector}}
+{{^displaygroupingselector}}
+<div class="mb-1 mr-auto">
+    <span class="filler">&nbsp;</span>
+</div>
+{{/displaygroupingselector}}
diff --git a/blocks/myoverview/tests/behat/block_myoverview_adminsettings.feature b/blocks/myoverview/tests/behat/block_myoverview_adminsettings.feature
new file mode 100644 (file)
index 0000000..5e69256
--- /dev/null
@@ -0,0 +1,226 @@
+@block @block_myoverview @javascript
+Feature: The my overview block allows admins to easily configure the students' course list
+  In order to adapt the my overview block to my users' needs
+  As an admin
+  I can configure the appearance of the my overview block
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email                | idnumber |
+      | student1 | Student   | X        | student1@example.com | S1       |
+    And the following "categories" exist:
+      | name        | category | idnumber |
+      | Category 1  | 0        | CAT1     |
+    And the following "courses" exist:
+      | fullname | shortname | category | startdate                   | enddate         |
+      | Course 1 | C1        | 0        | ##1 month ago##             | ##15 days ago## |
+      | Course 2 | C2        | 0        | ##yesterday##               | ##tomorrow## |
+      | Course 3 | C3        | 0        | ##yesterday##               | ##tomorrow## |
+      | Course 4 | C4        | CAT1     | ##yesterday##               | ##tomorrow## |
+      | Course 5 | C5        | 0        | ##first day of next month## | ##last day of next month## |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | student1 | C1 | student |
+      | student1 | C2 | student |
+      | student1 | C3 | student |
+      | student1 | C4 | student |
+      | student1 | C5 | student |
+
+  Scenario: Enable 'All' course filter option
+    Given I log in as "admin"
+    And I navigate to "Plugins > Blocks > Course overview" in site administration
+    And I set the field "All" to "1"
+    And I press "Save"
+    And I log out
+    Then I log in as "student1"
+    And I click on "All (except hidden)" "button" in the "Course overview" "block"
+    # We have to check for the data attribute instead of the list element text as we would get false positives from the "All (except hidden)" element otherwise
+    Then "[data-value='allincludinghidden']" "css_element" should exist in the ".block_myoverview .dropdown-menu" "css_element"
+    And I log out
+
+  Scenario: Disable 'All' course filter option
+    Given I log in as "admin"
+    And I navigate to "Plugins > Blocks > Course overview" in site administration
+    And I set the field "All" to "0"
+    And I press "Save"
+    And I log out
+    Then I log in as "student1"
+    And I click on "All (except hidden)" "button" in the "Course overview" "block"
+    # We have to check for the data attribute instead of the list element text as we would get false negatives "All (except hidden)" element otherwise
+    Then "[data-value='allincludinghidden']" "css_element" should not exist in the ".block_myoverview .dropdown-menu" "css_element"
+    And I log out
+
+  Scenario: Enable 'All (except hidden)' course filter option
+    Given I log in as "admin"
+    And I navigate to "Plugins > Blocks > Course overview" in site administration
+    And I set the field "All (except hidden)" to "1"
+    And I press "Save"
+    And I log out
+    Then I log in as "student1"
+    And I click on "All (except hidden)" "button" in the "Course overview" "block"
+    Then "All (except hidden)" "list_item" should exist in the ".block_myoverview .dropdown-menu" "css_element"
+    And I log out
+
+  Scenario: Disable 'All (except hidden)' course filter option
+    Given I log in as "admin"
+    And I navigate to "Plugins > Blocks > Course overview" in site administration
+    And I set the field "All (except hidden)" to "0"
+    And I press "Save"
+    And I log out
+    Then I log in as "student1"
+    # 'All (except hidden)' option has been disabled, so the button is falling back to the 'In progress' option which is the next enabled option.
+    And I click on "In progress" "button" in the "Course overview" "block"
+    Then "All (except hidden)" "list_item" should not exist in the ".block_myoverview .dropdown-menu" "css_element"
+    And I log out
+
+  Scenario: Enable 'In progress' course filter option
+    Given I log in as "admin"
+    And I navigate to "Plugins > Blocks > Course overview" in site administration
+    And I set the field "In progress" to "1"
+    And I press "Save"
+    And I log out
+    Then I log in as "student1"
+    And I click on "All (except hidden)" "button" in the "Course overview" "block"
+    Then "In progress" "list_item" should exist in the ".block_myoverview .dropdown-menu" "css_element"
+    And I log out
+
+  Scenario: Disable 'In progress' course filter option
+    Given I log in as "admin"
+    And I navigate to "Plugins > Blocks > Course overview" in site administration
+    And I set the field "In progress" to "0"
+    And I press "Save"
+    And I log out
+    Then I log in as "student1"
+    And I click on "All (except hidden)" "button" in the "Course overview" "block"
+    Then "In progress" "list_item" should not exist in the ".block_myoverview .dropdown-menu" "css_element"
+    And I log out
+
+  Scenario: Enable 'Future' course filter option
+    Given I log in as "admin"
+    And I navigate to "Plugins > Blocks > Course overview" in site administration
+    And I set the field "Future" to "1"
+    And I press "Save"
+    And I log out
+    Then I log in as "student1"
+    And I click on "All (except hidden)" "button" in the "Course overview" "block"
+    Then "Future" "list_item" should exist in the ".block_myoverview .dropdown-menu" "css_element"
+    And I log out
+
+  Scenario: Disable 'Future' course filter option
+    Given I log in as "admin"
+    And I navigate to "Plugins > Blocks > Course overview" in site administration
+    And I set the field "Future" to "0"
+    And I press "Save"
+    And I log out
+    Then I log in as "student1"
+    And I click on "All (except hidden)" "button" in the "Course overview" "block"
+    Then "Future" "list_item" should not exist in the ".block_myoverview .dropdown-menu" "css_element"
+    And I log out
+
+  Scenario: Enable 'Past' course filter option
+    Given I log in as "admin"
+    And I navigate to "Plugins > Blocks > Course overview" in site administration
+    And I set the field "Past" to "1"
+    And I press "Save"
+    And I log out
+    Then I log in as "student1"
+    And I click on "All (except hidden)" "button" in the "Course overview" "block"
+    Then "Past" "list_item" should exist in the ".block_myoverview .dropdown-menu" "css_element"
+    And I log out
+
+  Scenario: Disable 'Past' course filter option
+    Given I log in as "admin"
+    And I navigate to "Plugins > Blocks > Course overview" in site administration
+    And I set the field "Past" to "0"
+    And I press "Save"
+    And I log out
+    Then I log in as "student1"
+    And I click on "All (except hidden)" "button" in the "Course overview" "block"
+    Then "Past" "list_item" should not exist in the ".block_myoverview .dropdown-menu" "css_element"
+    And I log out
+
+  Scenario: Enable 'Starred' course filter option
+    Given I log in as "admin"
+    And I navigate to "Plugins > Blocks > Course overview" in site administration
+    And I set the field "Starred" to "1"
+    And I press "Save"
+    And I log out
+    Then I log in as "student1"
+    And I click on "All (except hidden)" "button" in the "Course overview" "block"
+    Then "Starred" "list_item" should exist in the ".block_myoverview .dropdown-menu" "css_element"
+    And I log out
+
+  Scenario: Disable 'Starred' course filter option
+    Given I log in as "admin"
+    And I navigate to "Plugins > Blocks > Course overview" in site administration
+    And I set the field "Starred" to "0"
+    And I press "Save"
+    And I log out
+    Then I log in as "student1"
+    And I click on "All (except hidden)" "button" in the "Course overview" "block"
+    Then "Starred" "list_item" should not exist in the ".block_myoverview .dropdown-menu" "css_element"
+    And I log out
+
+  Scenario: Enable 'Hidden' course filter option
+    Given I log in as "admin"
+    And I navigate to "Plugins > Blocks > Course overview" in site administration
+    And I set the field "Hidden" to "1"
+    And I press "Save"
+    And I log out
+    Then I log in as "student1"
+    And I click on "All (except hidden)" "button" in the "Course overview" "block"
+    Then "Hidden" "list_item" should exist in the ".block_myoverview .dropdown-menu" "css_element"
+    And I log out
+
+  Scenario: Disable 'Hidden' course filter option
+    Given I log in as "admin"
+    And I navigate to "Plugins > Blocks > Course overview" in site administration
+    And I set the field "Hidden" to "0"
+    And I press "Save"
+    And I log out
+    Then I log in as "student1"
+    And I click on "All (except hidden)" "button" in the "Course overview" "block"
+    Then "Hidden" "list_item" should not exist in the ".block_myoverview .dropdown-menu" "css_element"
+    And I log out
+
+  Scenario: Disable all course filter options
+    Given I log in as "admin"
+    And I navigate to "Plugins > Blocks > Course overview" in site administration
+    And I set the field "All" to "0"
+    And I set the field "All (except hidden)" to "0"
+    And I set the field "In progress" to "0"
+    And I set the field "Future" to "0"
+    And I set the field "Past" to "0"
+    And I set the field "Starred" to "0"
+    And I set the field "Hidden" to "0"
+    And I press "Save"
+    And I log out
+    And I log in as "student1"
+    Then "button#groupingdropdown" "css_element" should not exist in the ".block_myoverview" "css_element"
+    And I should see "Course 1" in the "Course overview" "block"
+    And I should see "Course 2" in the "Course overview" "block"
+    And I should see "Course 3" in the "Course overview" "block"
+    And I should see "Course 4" in the "Course overview" "block"
+    And I should see "Course 5" in the "Course overview" "block"
+    And I log out
+
+  Scenario: Disable all but one course filter option
+    Given I log in as "admin"
+    And I navigate to "Plugins > Blocks > Course overview" in site administration
+    And I set the field "All" to "0"
+    And I set the field "All (except hidden)" to "0"
+    And I set the field "In progress" to "1"
+    And I set the field "Future" to "0"
+    And I set the field "Past" to "0"
+    And I set the field "Starred" to "0"
+    And I set the field "Hidden" to "0"
+    And I press "Save"
+    And I log out
+    And I log in as "student1"
+    Then "button#groupingdropdown" "css_element" should not exist in the ".block_myoverview" "css_element"
+    And I should see "Course 2" in the "Course overview" "block"
+    And I should see "Course 3" in the "Course overview" "block"
+    And I should see "Course 4" in the "Course overview" "block"
+    And I should not see "Course 1" in the "Course overview" "block"
+    And I should not see "Course 5" in the "Course overview" "block"
+    And I log out
index b63804b..9101de8 100644 (file)
@@ -28,7 +28,7 @@ Feature: The my overview block allows users to easily access their courses
 
   Scenario: View past courses
     Given I log in as "student1"
-    And I click on "All" "button" in the "Course overview" "block"
+    And I click on "All (except hidden)" "button" in the "Course overview" "block"
     When I click on "Past" "link" in the "Course overview" "block"
     Then I should see "Course 1" in the "Course overview" "block"
     And I should not see "Course 2" in the "Course overview" "block"
@@ -39,7 +39,7 @@ Feature: The my overview block allows users to easily access their courses
 
   Scenario: View future courses
     Given I log in as "student1"
-    And I click on "All" "button" in the "Course overview" "block"
+    And I click on "All (except hidden)" "button" in the "Course overview" "block"
     When I click on "Future" "link" in the "Course overview" "block"
     Then I should see "Course 5" in the "Course overview" "block"
     And I should not see "Course 1" in the "Course overview" "block"
@@ -50,7 +50,7 @@ Feature: The my overview block allows users to easily access their courses
 
   Scenario: View inprogress courses
     Given I log in as "student1"
-    And I click on "All" "button" in the "Course overview" "block"
+    And I click on "All (except hidden)" "button" in the "Course overview" "block"
     When I click on "In progress" "link" in the "Course overview" "block"
     Then I should see "Course 2" in the "Course overview" "block"
     Then I should see "Course 3" in the "Course overview" "block"
@@ -59,10 +59,25 @@ Feature: The my overview block allows users to easily access their courses
     And I should not see "Course 5" in the "Course overview" "block"
     And I log out
 
-  Scenario: View all courses
+  Scenario: View all (except hidden) courses
     Given I log in as "student1"
-    And I click on "All" "button" in the "Course overview" "block"
-    When I click on "All" "link" in the "Course overview" "block"
+    And I click on "All (except hidden)" "button" in the "Course overview" "block"
+    When I click on "All (except hidden)" "link" in the "Course overview" "block"
+    Then I should see "Course 1" in the "Course overview" "block"
+    Then I should see "Course 2" in the "Course overview" "block"
+    Then I should see "Course 3" in the "Course overview" "block"
+    Then I should see "Course 4" in the "Course overview" "block"
+    Then I should see "Course 5" in the "Course overview" "block"
+    And I log out
+
+  Scenario: View all (including hidden) courses
+    Given the following config values are set as admin:
+      | config                            | value | plugin           |
+      | displaygroupingallincludinghidden | 1     | block_myoverview |
+    And I log in as "student1"
+    And I click on "All (except hidden)" "button" in the "Course overview" "block"
+    # We have to click on the data attribute instead of the button element text as we might risk to click on the false positive "All (except hidden)" element instead
+    When I click on "[data-value='allincludinghidden']" "css_element" in the "Course overview" "block"
     Then I should see "Course 1" in the "Course overview" "block"
     Then I should see "Course 2" in the "Course overview" "block"
     Then I should see "Course 3" in the "Course overview" "block"
@@ -72,7 +87,7 @@ Feature: The my overview block allows users to easily access their courses
 
   Scenario: View inprogress courses - test persistence
     Given I log in as "student1"
-    And I click on "All" "button" in the "Course overview" "block"
+    And I click on "All (except hidden)" "button" in the "Course overview" "block"
     And I click on "In progress" "link" in the "Course overview" "block"
     And I reload the page
     Then I should see "In progress" in the "Course overview" "block"
@@ -83,12 +98,12 @@ Feature: The my overview block allows users to easily access their courses
     And I should not see "Course 5" in the "Course overview" "block"
     And I log out
 
-  Scenario: View all courses - w/ persistence
+  Scenario: View all (except hidden) courses - w/ persistence
     Given I log in as "student1"
-    And I click on "All" "button" in the "Course overview" "block"
-    When I click on "All" "link" in the "Course overview" "block"
+    And I click on "All (except hidden)" "button" in the "Course overview" "block"
+    When I click on "All (except hidden)" "link" in the "Course overview" "block"
     And I reload the page
-    Then I should see "All" in the "Course overview" "block"
+    Then I should see "All (except hidden)" in the "Course overview" "block"
     Then I should see "Course 1" in the "Course overview" "block"
     Then I should see "Course 2" in the "Course overview" "block"
     Then I should see "Course 3" in the "Course overview" "block"
@@ -98,7 +113,7 @@ Feature: The my overview block allows users to easily access their courses
 
   Scenario: View past courses - w/ persistence
     Given I log in as "student1"
-    And I click on "All" "button" in the "Course overview" "block"
+    And I click on "All (except hidden)" "button" in the "Course overview" "block"
     When I click on "Past" "link" in the "Course overview" "block"
     And I reload the page
     Then I should see "Past" in the "Course overview" "block"
@@ -111,7 +126,7 @@ Feature: The my overview block allows users to easily access their courses
 
   Scenario: View future courses - w/ persistence
     Given I log in as "student1"
-    And I click on "All" "button" in the "Course overview" "block"
+    And I click on "All (except hidden)" "button" in the "Course overview" "block"
     When I click on "Future" "link" in the "Course overview" "block"
     And I reload the page
     Then I should see "Future" in the "Course overview" "block"
@@ -164,7 +179,7 @@ Feature: The my overview block allows users to easily access their courses
 
   Scenario: View inprogress courses with hide persistent functionality
     Given I log in as "student1"
-    And I click on "All" "button" in the "Course overview" "block"
+    And I click on "All (except hidden)" "button" in the "Course overview" "block"
     When I click on "In progress" "link" in the "Course overview" "block"
     And I click on ".coursemenubtn" "css_element" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
     And I click on "Hide from view" "link" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
@@ -178,7 +193,7 @@ Feature: The my overview block allows users to easily access their courses
 
   Scenario: View past courses with hide persistent functionality
     Given I log in as "student1"
-    And I click on "All" "button" in the "Course overview" "block"
+    And I click on "All (except hidden)" "button" in the "Course overview" "block"
     When I click on "Past" "link" in the "Course overview" "block"
     And I click on ".coursemenubtn" "css_element" in the "//div[@class='card dashboard-card' and contains(.,'Course 1')]" "xpath_element"
     And I click on "Hide from view" "link" in the "//div[@class='card dashboard-card' and contains(.,'Course 1')]" "xpath_element"
@@ -192,7 +207,7 @@ Feature: The my overview block allows users to easily access their courses
 
   Scenario: View future courses with hide persistent functionality
     Given I log in as "student1"
-    And I click on "All" "button" in the "Course overview" "block"
+    And I click on "All (except hidden)" "button" in the "Course overview" "block"
     When I click on "Future" "link" in the "Course overview" "block"
     And I click on ".coursemenubtn" "css_element" in the "//div[@class='card dashboard-card' and contains(.,'Course 5')]" "xpath_element"
     And I click on "Hide from view" "link" in the "//div[@class='card dashboard-card' and contains(.,'Course 5')]" "xpath_element"
@@ -204,6 +219,38 @@ Feature: The my overview block allows users to easily access their courses
     And I should not see "Course 4" in the "Course overview" "block"
     And I log out
 
+  Scenario: View all (except hidden) courses with hide persistent functionality
+    Given I log in as "student1"
+    And I click on "All (except hidden)" "button" in the "Course overview" "block"
+    When I click on "All (except hidden)" "link" in the "Course overview" "block"
+    And I click on ".coursemenubtn" "css_element" in the "//div[@class='card dashboard-card' and contains(.,'Course 5')]" "xpath_element"
+    And I click on "Hide from view" "link" in the "//div[@class='card dashboard-card' and contains(.,'Course 5')]" "xpath_element"
+    And I reload the page
+    Then I should not see "Course 5" in the "Course overview" "block"
+    And I should see "Course 1" in the "Course overview" "block"
+    And I should see "Course 2" in the "Course overview" "block"
+    And I should see "Course 3" in the "Course overview" "block"
+    And I should see "Course 4" in the "Course overview" "block"
+    And I log out
+
+  Scenario: View all (including hidden) courses with hide persistent functionality
+    Given the following config values are set as admin:
+      | config                            | value | plugin           |
+      | displaygroupingallincludinghidden | 1     | block_myoverview |
+    And I log in as "student1"
+    And I click on "All (except hidden)" "button" in the "Course overview" "block"
+    # We have to click on the data attribute instead of the button element text as we might risk to click on the false positive "All (except hidden)" element instead
+    When I click on "[data-value='allincludinghidden']" "css_element" in the "Course overview" "block"
+    And I click on ".coursemenubtn" "css_element" in the "//div[@class='card dashboard-card' and contains(.,'Course 5')]" "xpath_element"
+    And I click on "Hide from view" "link" in the "//div[@class='card dashboard-card' and contains(.,'Course 5')]" "xpath_element"
+    And I reload the page
+    Then I should see "Course 5" in the "Course overview" "block"
+    And I should see "Course 1" in the "Course overview" "block"
+    And I should see "Course 2" in the "Course overview" "block"
+    And I should see "Course 3" in the "Course overview" "block"
+    And I should see "Course 4" in the "Course overview" "block"
+    And I log out
+
   Scenario: Show course category in cards display
     Given I log in as "student1"
     And I click on "Display drop-down menu" "button" in the "Course overview" "block"
index c9c4845..e494c75 100644 (file)
@@ -25,7 +25,9 @@ Feature: The my overview block allows users to hide their courses
 
   Scenario: Test hide toggle functionality
     Given I log in as "student1"
-    When I click on ".coursemenubtn" "css_element" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
+    And I click on "All (except hidden)" "button" in the "Course overview" "block"
+    When I click on "All (except hidden)" "link" in the "Course overview" "block"
+    And I click on ".coursemenubtn" "css_element" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
     And I click on "Hide from view" "link" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
     And I reload the page
     Then I should not see "Course 2" in the "Course overview" "block"
@@ -33,13 +35,15 @@ Feature: The my overview block allows users to hide their courses
 
   Scenario: Test hide toggle functionality w/ favorites
     Given I log in as "student1"
+    And I click on "All (except hidden)" "button" in the "Course overview" "block"
+    When I click on "All (except hidden)" "link" in the "Course overview" "block"
     And I click on ".coursemenubtn" "css_element" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
     And I click on "Star this course" "link" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
     And I click on ".coursemenubtn" "css_element" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
     And I click on "Hide from view" "link" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
     When I reload the page
     Then I should not see "Course 2" in the "Course overview" "block"
-    And I click on "All" "button" in the "Course overview" "block"
+    And I click on "All (except hidden)" "button" in the "Course overview" "block"
     And I click on "Starred" "link" in the "Course overview" "block"
     Then I should not see "Course 2" in the "Course overview" "block"
     And I click on "Starred" "button" in the "Course overview" "block"
@@ -49,25 +53,29 @@ Feature: The my overview block allows users to hide their courses
 
   Scenario: Test show toggle functionality
     Given I log in as "student1"
+    And I click on "All (except hidden)" "button" in the "Course overview" "block"
+    When I click on "All (except hidden)" "link" in the "Course overview" "block"
     And I click on ".coursemenubtn" "css_element" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
     And I click on "Hide from view" "link" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
-    When I click on "All" "button" in the "Course overview" "block"
+    When I click on "All (except hidden)" "button" in the "Course overview" "block"
     And I click on "Hidden" "link" in the "Course overview" "block"
     When I click on ".coursemenubtn" "css_element" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
     And I click on "Show this course" "link" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
     And I reload the page
     And I click on "Hidden" "button" in the "Course overview" "block"
-    When I click on "All" "link" in the "Course overview" "block"
+    When I click on "All (except hidden)" "link" in the "Course overview" "block"
     Then I should see "Course 2" in the "Course overview" "block"
     And I log out
 
   Scenario: Test show toggle functionality w/ favorites
     Given I log in as "student1"
+    And I click on "All (except hidden)" "button" in the "Course overview" "block"
+    When I click on "All (except hidden)" "link" in the "Course overview" "block"
     And I click on ".coursemenubtn" "css_element" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
     And I click on "Star this course" "link" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
     And I click on ".coursemenubtn" "css_element" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
     And I click on "Hide from view" "link" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
-    And I click on "All" "button" in the "Course overview" "block"
+    And I click on "All (except hidden)" "button" in the "Course overview" "block"
     And I click on "Hidden" "link" in the "Course overview" "block"
     And I should see "Course 2" in the "Course overview" "block"
     And I click on ".coursemenubtn" "css_element" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
@@ -75,9 +83,39 @@ Feature: The my overview block allows users to hide their courses
     When I reload the page
     Then I should not see "Course 2" in the "Course overview" "block"
     And I click on "Hidden" "button" in the "Course overview" "block"
-    And I click on "All" "link" in the "Course overview" "block"
+    And I click on "All (except hidden)" "link" in the "Course overview" "block"
     Then I should see "Course 2" in the "Course overview" "block"
-    And I click on "All" "button" in the "Course overview" "block"
+    And I click on "All (except hidden)" "button" in the "Course overview" "block"
     And I click on "Starred" "link" in the "Course overview" "block"
     Then I should see "Course 2" in the "Course overview" "block"
+    And I log out
+
+  Scenario: Test a course is hidden directly with "All (except hidden)" courses
+    Given I log in as "student1"
+    And I click on "All (except hidden)" "button" in the "Course overview" "block"
+    When I click on "All (except hidden)" "link" in the "Course overview" "block"
+    And I click on ".coursemenubtn" "css_element" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
+    And I click on "Hide from view" "link" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
+    Then I should not see "Course 2" in the "Course overview" "block"
+    And I log out
+
+  Scenario: Test a course is never hidden with "All (including hidden)" courses
+    Given the following config values are set as admin:
+      | config                            | value | plugin           |
+      | displaygroupingallincludinghidden | 1     | block_myoverview |
+    And I log in as "student1"
+    And I click on "All (except hidden)" "button" in the "Course overview" "block"
+    # We have to click on the data attribute instead of the button element text as we might risk to click on the false positive "All (except hidden)" element instead
+    When I click on "[data-value='allincludinghidden']" "css_element" in the "Course overview" "block"
+    And I click on ".coursemenubtn" "css_element" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
+    And I click on "Hide from view" "link" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
+    Then I should see "Course 2" in the "Course overview" "block"
+    And I click on ".coursemenubtn" "css_element" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
+    And I should not see "Hide from view" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
+    And I should see "Show this course" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
+    And I click on "Show this course" "link" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
+    Then I should see "Course 2" in the "Course overview" "block"
+    And I click on ".coursemenubtn" "css_element" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
+    And I should see "Hide from view" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
+    And I should not see "Show this course" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
     And I log out
\ No newline at end of file
index 6e0e107..84f2e4e 100644 (file)
@@ -50,7 +50,7 @@ Feature: The my overview block allows users to persistence of their page limits
     Given I log in as "student1"
     When I click on "[data-toggle='dropdown']" "css_element" in the "Course overview" "block"
     And I click on "All" "link"
-    And I click on "All" "button" in the "Course overview" "block"
+    And I click on "All (except hidden)" "button" in the "Course overview" "block"
     And I click on "In progress" "link" in the "Course overview" "block"
     Then I should see "Course 13"
     And I should see "All" in the "[data-action='limit-toggle']" "css_element"
index 06813f5..3c2c1c9 100644 (file)
@@ -22,8 +22,8 @@ Feature: Course overview block show users their progress on courses
 
   Scenario: Course progress percentage should not be displayed if completion is not enabled
     Given I log in as "student1"
-    And I click on "All" "button" in the "Course overview" "block"
-    When I click on "All" "link" in the "Course overview" "block"
+    And I click on "All (except hidden)" "button" in the "Course overview" "block"
+    When I click on "All (except hidden)" "link" in the "Course overview" "block"
     Then I should not see "0%" in the "Course overview" "block"
     And I log out
 
@@ -38,12 +38,12 @@ Feature: Course overview block show users their progress on courses
     And I press "Save and return to course"
     And I log out
     When I log in as "student1"
-    And I click on "All" "button" in the "Course overview" "block"
+    And I click on "All (except hidden)" "button" in the "Course overview" "block"
     Then I should see "Course 1" in the "Course overview" "block"
     And I should see "0%" in the "Course overview" "block"
     And I am on "Course 1" course homepage
     And I follow "Test choice 1"
     And I follow "Dashboard" in the user menu
-    And I click on "All" "button" in the "Course overview" "block"
+    And I click on "All (except hidden)" "button" in the "Course overview" "block"
     And I should see "100%" in the "Course overview" "block"
     And I log out
index 5b5b488..12a7387 100644 (file)
@@ -72,6 +72,7 @@ class block_myoverview_privacy_testcase extends \core_privacy\tests\provider_tes
         return array(
             array('block_myoverview_user_sort_preference', 'lastaccessed', ''),
             array('block_myoverview_user_sort_preference', 'title', ''),
+            array('block_myoverview_user_grouping_preference', 'allincludinghidden', ''),
             array('block_myoverview_user_grouping_preference', 'all', ''),
             array('block_myoverview_user_grouping_preference', 'inprogress', ''),
             array('block_myoverview_user_grouping_preference', 'future', ''),
index 4c93e24..6b5751d 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2019070400;         // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version   = 2019070401;         // The current plugin version (Date: YYYYMMDDXX).
 $plugin->requires  = 2019051100;         // Requires this Moodle version.
 $plugin->component = 'block_myoverview'; // Full name of the plugin (used for diagnostics).
index cc895b3..8da5ad1 100644 (file)
@@ -50,8 +50,22 @@ function xmldb_block_recentlyaccesseditems_upgrade($oldversion, $block) {
     // Automatically generated Moodle v3.7.0 release upgrade line.
     // Put any upgrade step following this.
     if ($oldversion < 2019052001) {
-        $sql = "courseid NOT IN (SELECT c.id from {course} c) OR cmid NOT IN (SELECT cm.id from {course_modules} cm)";
-        $DB->delete_records_select("block_recentlyaccesseditems", $sql);
+        // Query the items to be deleted as a list of IDs. We cannot delete directly from this as a
+        // subquery because MySQL does not support delete with subqueries.
+        $fordeletion = $DB->get_fieldset_sql("
+                SELECT rai.id
+                  FROM {block_recentlyaccesseditems} rai
+             LEFT JOIN {course} c ON c.id = rai.courseid
+             LEFT JOIN {course_modules} cm ON cm.id = rai.cmid
+                 WHERE c.id IS NULL OR cm.id IS NULL");
+
+        // Delete the array in chunks of 500 (Oracle does not support more than 1000 parameters,
+        // let's leave some leeway, there are likely only one chunk anyway).
+        $chunks = array_chunk($fordeletion, 500);
+        foreach ($chunks as $chunk) {
+            $DB->delete_records_list('block_recentlyaccesseditems', 'id', $chunk);
+        }
+
         upgrade_block_savepoint(true, 2019052001, 'recentlyaccesseditems', false);
     }
 
index d671d1b..37d1968 100644 (file)
@@ -83,7 +83,7 @@ function blog_rss_print_link($context, $filtertype, $filterselect = 0, $tagid =
 
     $url = blog_rss_get_url($context->id, $userid, $filtertype, $filterselect, $tagid);
     $rsspix = $OUTPUT->pix_icon('i/rss', get_string('rss'), 'core', array('title' => $tooltiptext));
-    print '<div class="pull-xs-right"><a href="'. $url .'">' . $rsspix . '</a></div>';
+    print '<div class="float-sm-right"><a href="'. $url .'">' . $rsspix . '</a></div>';
 }
 
 /**
index b5add2d..38ef769 100644 (file)
@@ -228,7 +228,8 @@ class core_cache_renderer extends plugin_renderer_base {
             get_string('area', 'cache'),
             get_string('mappings', 'cache'),
             get_string('sharing', 'cache'),
-            get_string('actions', 'cache'),
+            get_string('canuselocalstore', 'cache'),
+            get_string('actions', 'cache')
         );
         $table->colclasses = array(
             'definition',
@@ -237,6 +238,7 @@ class core_cache_renderer extends plugin_renderer_base {
             'area',
             'mappings',
             'sharing',
+            'canuselocalstore',
             'actions'
         );
         $table->data = array();
@@ -257,6 +259,13 @@ class core_cache_renderer extends plugin_renderer_base {
                 $mapping = '<em>'.$none.'</em>';
             }
 
+            $uselocalcachecol = get_string('no');
+            if ($definition['mode'] != cache_store::MODE_REQUEST) {
+                if (isset($definition['canuselocalstore']) && $definition['canuselocalstore']) {
+                    $uselocalcachecol = get_string('yes');
+                }
+            }
+
             $row = new html_table_row(array(
                 $definition['name'],
                 get_string('mode_'.$definition['mode'], 'cache'),
@@ -264,6 +273,7 @@ class core_cache_renderer extends plugin_renderer_base {
                 $definition['area'],
                 $mapping,
                 join(', ', $definition['selectedsharingoption']),
+                $uselocalcachecol,
                 join(', ', $htmlactions)
             ));
             $row->attributes['class'] = 'definition-'.$definition['component'].'-'.$definition['area'];
index 2f2242b..a078c02 100644 (file)
@@ -161,7 +161,7 @@ class core_calendar_renderer extends plugin_renderer_base {
                 $deletelink = null;
             }
 
-            $commands  = html_writer::start_tag('div', array('class' => 'commands pull-xs-right'));
+            $commands  = html_writer::start_tag('div', array('class' => 'commands float-sm-right'));
             $commands .= html_writer::start_tag('a', array('href' => $editlink));
             $str = get_string('tt_editevent', 'calendar');
             $commands .= $this->output->pix_icon('t/edit', $str);
@@ -205,9 +205,9 @@ class core_calendar_renderer extends plugin_renderer_base {
             $output .= html_writer::tag('div', $event->courselink);
         }
         if (!empty($event->time)) {
-            $output .= html_writer::tag('span', $event->time, array('class' => 'date pull-xs-right mr-1'));
+            $output .= html_writer::tag('span', $event->time, array('class' => 'date float-sm-right mr-1'));
         } else {
-            $attrs = array('class' => 'date pull-xs-right mr-1');
+            $attrs = array('class' => 'date float-sm-right mr-1');
             $output .= html_writer::tag('span', calendar_time_representation($event->timestart), $attrs);
         }
 
index 06f312d..b76483d 100644 (file)
@@ -49,12 +49,13 @@ class core_comment_external extends external_api {
 
         return new external_function_parameters(
             array(
-                'contextlevel' => new external_value(PARAM_ALPHA, 'contextlevel system, course, user...'),
-                'instanceid'   => new external_value(PARAM_INT, 'the Instance id of item associated with the context level'),
-                'component'    => new external_value(PARAM_COMPONENT, 'component'),
-                'itemid'       => new external_value(PARAM_INT, 'associated id'),
-                'area'         => new external_value(PARAM_AREA, 'string comment area', VALUE_DEFAULT, ''),
-                'page'         => new external_value(PARAM_INT, 'page number (0 based)', VALUE_DEFAULT, 0),
+                'contextlevel'  => new external_value(PARAM_ALPHA, 'contextlevel system, course, user...'),
+                'instanceid'    => new external_value(PARAM_INT, 'the Instance id of item associated with the context level'),
+                'component'     => new external_value(PARAM_COMPONENT, 'component'),
+                'itemid'        => new external_value(PARAM_INT, 'associated id'),
+                'area'          => new external_value(PARAM_AREA, 'string comment area', VALUE_DEFAULT, ''),
+                'page'          => new external_value(PARAM_INT, 'page number (0 based)', VALUE_DEFAULT, 0),
+                'sortdirection' => new external_value(PARAM_ALPHA, 'Sort direction: ASC or DESC', VALUE_DEFAULT, 'DESC'),
             )
         );
     }
@@ -68,22 +69,33 @@ class core_comment_external extends external_api {
      * @param int $itemid the item id
      * @param string $area comment area
      * @param int $page page number
+     * @param string $sortdirection sort direction
      * @return array of comments and warnings
      * @since Moodle 2.9
      */
-    public static function get_comments($contextlevel, $instanceid, $component, $itemid, $area = '', $page = 0) {
+    public static function get_comments($contextlevel, $instanceid, $component, $itemid, $area = '', $page = 0,
+            $sortdirection = 'DESC') {
+        global $CFG;
 
         $warnings = array();
         $arrayparams = array(
-            'contextlevel' => $contextlevel,
-            'instanceid'   => $instanceid,
-            'component'    => $component,
-            'itemid'       => $itemid,
-            'area'         => $area,
-            'page'         => $page
+            'contextlevel'  => $contextlevel,
+            'instanceid'    => $instanceid,
+            'component'     => $component,
+            'itemid'        => $itemid,
+            'area'          => $area,
+            'page'          => $page,
+            'sortdirection' => $sortdirection,
         );
         $params = self::validate_parameters(self::get_comments_parameters(), $arrayparams);
 
+        $sortdirection = strtoupper($params['sortdirection']);
+        $directionallowedvalues = array('ASC', 'DESC');
+        if (!in_array($sortdirection, $directionallowedvalues)) {
+            throw new invalid_parameter_exception('Invalid value for sortdirection parameter (value: ' . $sortdirection . '),' .
+                'allowed values are: ' . implode(',', $directionallowedvalues));
+        }
+
         $context = self::get_context_from_params($params);
         self::validate_context($context);
 
@@ -96,7 +108,7 @@ class core_comment_external extends external_api {
         $args->component = $params['component'];
 
         $commentobject = new comment($args);
-        $comments = $commentobject->get_comments($params['page']);
+        $comments = $commentobject->get_comments($params['page'], $sortdirection);
 
         // False means no permissions to see comments.
         if ($comments === false) {
@@ -117,6 +129,8 @@ class core_comment_external extends external_api {
 
         $results = array(
             'comments' => $comments,
+            'count' => $commentobject->count(),
+            'perpage' => (!empty($CFG->commentsperpage)) ? $CFG->commentsperpage : 15,
             'warnings' => $warnings
         );
         return $results;
@@ -148,6 +162,8 @@ class core_comment_external extends external_api {
                         ), 'comment'
                     ), 'List of comments'
                 ),
+                'count' => new external_value(PARAM_INT,  'Total number of comments.', VALUE_OPTIONAL),
+                'perpage' => new external_value(PARAM_INT,  'Number of comments per page.', VALUE_OPTIONAL),
                 'warnings' => new external_warnings()
             )
         );
index f8e9f23..463fa00 100644 (file)
@@ -537,9 +537,10 @@ class comment {
      * Return matched comments
      *
      * @param  int $page
+     * @param  str $sortdirection sort direction, ASC or DESC
      * @return array
      */
-    public function get_comments($page = '') {
+    public function get_comments($page = '', $sortdirection = 'DESC') {
         global $DB, $CFG, $USER, $OUTPUT;
         if (!$this->can_view()) {
             return false;
@@ -557,6 +558,7 @@ class comment {
             $params['component'] = $component;
         }
 
+        $sortdirection = ($sortdirection === 'ASC') ? 'ASC' : 'DESC';
         $sql = "SELECT $ufields, c.id AS cid, c.content AS ccontent, c.format AS cformat, c.timecreated AS ctimecreated
                   FROM {comments} c
                   JOIN {user} u ON u.id = c.userid
@@ -564,7 +566,7 @@ class comment {
                        c.commentarea = :commentarea AND
                        c.itemid = :itemid AND
                        $componentwhere
-              ORDER BY c.timecreated DESC";
+              ORDER BY c.timecreated $sortdirection";
         $params['contextid'] = $this->contextid;
         $params['commentarea'] = $this->commentarea;
         $params['itemid'] = $this->itemid;
index c6a7433..e9eb0c2 100644 (file)
@@ -123,11 +123,34 @@ class core_comment_externallib_testcase extends externallib_advanced_testcase {
 
         $this->assertCount(0, $result['warnings']);
         $this->assertCount(2, $result['comments']);
+        $this->assertEquals(2, $result['count']);
+        $this->assertEquals(15, $result['perpage']);
 
         $this->assertEquals($user->id, $result['comments'][0]['userid']);
         $this->assertEquals($user->id, $result['comments'][1]['userid']);
 
-        $this->assertEquals($cmtid2, $result['comments'][0]['id']);
+        $this->assertEquals($cmtid2, $result['comments'][0]['id']); // Default ordering newer first.
         $this->assertEquals($cmtid1, $result['comments'][1]['id']);
+
+        // Test sort direction and pagination.
+        $CFG->commentsperpage = 1;
+        $result = core_comment_external::get_comments($contextlevel, $instanceid, $component, $itemid, $area, $page, 'ASC');
+        $result = external_api::clean_returnvalue(core_comment_external::get_comments_returns(), $result);
+
+        $this->assertCount(0, $result['warnings']);
+        $this->assertCount(1, $result['comments']); // Only one per page.
+        $this->assertEquals(2, $result['count']);
+        $this->assertEquals($CFG->commentsperpage, $result['perpage']);
+        $this->assertEquals($cmtid1, $result['comments'][0]['id']); // Comments order older first.
+
+        // Next page.
+        $result = core_comment_external::get_comments($contextlevel, $instanceid, $component, $itemid, $area, $page + 1, 'ASC');
+        $result = external_api::clean_returnvalue(core_comment_external::get_comments_returns(), $result);
+
+        $this->assertCount(0, $result['warnings']);
+        $this->assertCount(1, $result['comments']);
+        $this->assertEquals(2, $result['count']);
+        $this->assertEquals($CFG->commentsperpage, $result['perpage']);
+        $this->assertEquals($cmtid2, $result['comments'][0]['id']);
     }
 }
diff --git a/comment/upgrade.txt b/comment/upgrade.txt
new file mode 100644 (file)
index 0000000..2510a99
--- /dev/null
@@ -0,0 +1,7 @@
+This files describes API changes in /comment/* ,
+information provided here is intended especially for developers.
+
+=== 3.8 ===
+  * External function get_comments now returns the total count of comments and the number of comments per page.
+    It also has a new parameter to indicate the sorting direction (defaulted to DESC).
+
index 5a86efa..597a77a 100644 (file)
@@ -13,7 +13,7 @@
     "require-dev": {
         "phpunit/phpunit": "7.5.*",
         "phpunit/dbunit": "4.0.*",
-        "moodlehq/behat-extension": "3.38.1",
+        "moodlehq/behat-extension": "3.38.2",
         "mikey179/vfsstream": "^1.6",
         "instaclick/php-webdriver": "dev-local as 1.x-dev"
     }
index fef1a71..30350b9 100644 (file)
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "413289581153e5427c3f4fc277185fb0",
+    "content-hash": "f3e6814cafec1673c1fa51dea8a41306",
     "packages": [],
     "packages-dev": [
         {
             "source": {
                 "type": "git",
                 "url": "https://github.com/moodlehq/php-webdriver.git",
-                "reference": "e311e55bf2c4746db9df72707f3cf1a731ad98aa"
+                "reference": "3df827208ec104a9716aa8c30741e330da620c1e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/moodlehq/php-webdriver/zipball/e311e55bf2c4746db9df72707f3cf1a731ad98aa",
-                "reference": "e311e55bf2c4746db9df72707f3cf1a731ad98aa",
+                "url": "https://api.github.com/repos/moodlehq/php-webdriver/zipball/3df827208ec104a9716aa8c30741e330da620c1e",
+                "reference": "3df827208ec104a9716aa8c30741e330da620c1e",
                 "shasum": ""
             },
             "require": {
             "support": {
                 "source": "https://github.com/moodlehq/php-webdriver/tree/local"
             },
-            "time": "2019-06-03T22:55:37+00:00"
+            "time": "2019-08-14T02:10:24+00:00"
         },
         {
             "name": "mikey179/vfsstream",
-            "version": "v1.6.6",
+            "version": "v1.6.7",
             "source": {
                 "type": "git",
                 "url": "https://github.com/bovigo/vfsStream.git",
-                "reference": "095238a0711c974ae5b4ebf4c4534a23f3f6c99d"
+                "reference": "2b544ac3a21bcc4dde5d90c4ae8d06f4319055fb"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/bovigo/vfsStream/zipball/095238a0711c974ae5b4ebf4c4534a23f3f6c99d",
-                "reference": "095238a0711c974ae5b4ebf4c4534a23f3f6c99d",
+                "url": "https://api.github.com/repos/bovigo/vfsStream/zipball/2b544ac3a21bcc4dde5d90c4ae8d06f4319055fb",
+                "reference": "2b544ac3a21bcc4dde5d90c4ae8d06f4319055fb",
                 "shasum": ""
             },
             "require": {
                 "php": ">=5.3.0"
             },
             "require-dev": {
-                "phpunit/phpunit": "~4.5"
+                "phpunit/phpunit": "^4.5|^5.0"
             },
             "type": "library",
             "extra": {
             "authors": [
                 {
                     "name": "Frank Kleine",
-                    "homepage": "http://frankkleine.de/",
-                    "role": "Developer"
+                    "role": "Developer",
+                    "homepage": "http://frankkleine.de/"
                 }
             ],
             "description": "Virtual file system to mock the real file system in unit tests.",
             "homepage": "http://vfs.bovigo.org/",
-            "time": "2019-04-08T13:54:32+00:00"
+            "time": "2019-08-01T01:38:37+00:00"
         },
         {
             "name": "moodlehq/behat-extension",
-            "version": "v3.38.1",
+            "version": "v3.38.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/moodlehq/moodle-behat-extension.git",
-                "reference": "a3c38c2864e7259b1de834218abfe49eecc03417"
+                "reference": "ee293554b4e75b7444576bfe0c9f6779fb1c04cb"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/moodlehq/moodle-behat-extension/zipball/a3c38c2864e7259b1de834218abfe49eecc03417",
-                "reference": "a3c38c2864e7259b1de834218abfe49eecc03417",
+                "url": "https://api.github.com/repos/moodlehq/moodle-behat-extension/zipball/ee293554b4e75b7444576bfe0c9f6779fb1c04cb",
+                "reference": "ee293554b4e75b7444576bfe0c9f6779fb1c04cb",
                 "shasum": ""
             },
             "require": {
                 "behat/mink-extension": "~2.2",
                 "behat/mink-goutte-driver": "~1.2",
                 "behat/mink-selenium2-driver": "~1.3",
-                "php": ">=5.4.4",
+                "php": ">=7.1.0",
                 "symfony/process": "2.8.*"
             },
             "type": "library",
             "authors": [
                 {
                     "name": "David Monllaó",
+                    "role": "Developer",
                     "email": "david.monllao@gmail.com",
-                    "homepage": "http://moodle.com",
-                    "role": "Developer"
+                    "homepage": "http://moodle.com"
                 }
             ],
             "description": "Moodle behat extension",
                 "Behat",
                 "moodle"
             ],
-            "time": "2019-07-18T08:33:39+00:00"
+            "time": "2019-08-26T22:08:54+00:00"
         },
         {
             "name": "myclabs/deep-copy",
-            "version": "1.9.1",
+            "version": "1.9.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/myclabs/DeepCopy.git",
-                "reference": "e6828efaba2c9b79f4499dae1d66ef8bfa7b2b72"
+                "reference": "007c053ae6f31bba39dfa19a7726f56e9763bbea"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/e6828efaba2c9b79f4499dae1d66ef8bfa7b2b72",
-                "reference": "e6828efaba2c9b79f4499dae1d66ef8bfa7b2b72",
+                "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/007c053ae6f31bba39dfa19a7726f56e9763bbea",
+                "reference": "007c053ae6f31bba39dfa19a7726f56e9763bbea",
                 "shasum": ""
             },
             "require": {
                 "object",
                 "object graph"
             ],
-            "time": "2019-04-07T13:18:21+00:00"
+            "time": "2019-08-09T12:45:53+00:00"
         },
         {
             "name": "phar-io/manifest",
             "authors": [
                 {
                     "name": "Arne Blankerts",
-                    "email": "arne@blankerts.de",
-                    "role": "Developer"
+                    "role": "Developer",
+                    "email": "arne@blankerts.de"
                 },
                 {
                     "name": "Sebastian Heuer",
-                    "email": "sebastian@phpeople.de",
-                    "role": "Developer"
+                    "role": "Developer",
+                    "email": "sebastian@phpeople.de"
                 },
                 {
                     "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de",
-                    "role": "Developer"
+                    "role": "Developer",
+                    "email": "sebastian@phpunit.de"
                 }
             ],
             "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
             "authors": [
                 {
                     "name": "Arne Blankerts",
-                    "email": "arne@blankerts.de",
-                    "role": "Developer"
+                    "role": "Developer",
+                    "email": "arne@blankerts.de"
                 },
                 {
                     "name": "Sebastian Heuer",
-                    "email": "sebastian@phpeople.de",
-                    "role": "Developer"
+                    "role": "Developer",
+                    "email": "sebastian@phpeople.de"
                 },
                 {
                     "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de",
-                    "role": "Developer"
+                    "role": "Developer",
+                    "email": "sebastian@phpunit.de"
                 }
             ],
             "description": "Library for handling version information and constraints",
             "authors": [
                 {
                     "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de",
-                    "role": "lead"
+                    "role": "lead",
+                    "email": "sebastian@phpunit.de"
                 }
             ],
             "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.",
             "authors": [
                 {
                     "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de",
-                    "role": "lead"
+                    "role": "lead",
+                    "email": "sebastian@phpunit.de"
                 }
             ],
             "description": "FilterIterator implementation that filters files based on a list of suffixes.",
             "authors": [
                 {
                     "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de",
-                    "role": "lead"
+                    "role": "lead",
+                    "email": "sebastian@phpunit.de"
                 }
             ],
             "description": "Simple template engine.",
             "authors": [
                 {
                     "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de",
-                    "role": "lead"
+                    "role": "lead",
+                    "email": "sebastian@phpunit.de"
                 }
             ],
             "description": "Utility class for timing",
         },
         {
             "name": "phpunit/php-token-stream",
-            "version": "3.0.2",
+            "version": "3.1.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-token-stream.git",
-                "reference": "c4a66b97f040e3e20b3aa2a243230a1c3a9f7c8c"
+                "reference": "e899757bb3df5ff6e95089132f32cd59aac2220a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/c4a66b97f040e3e20b3aa2a243230a1c3a9f7c8c",
-                "reference": "c4a66b97f040e3e20b3aa2a243230a1c3a9f7c8c",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/e899757bb3df5ff6e95089132f32cd59aac2220a",
+                "reference": "e899757bb3df5ff6e95089132f32cd59aac2220a",
                 "shasum": ""
             },
             "require": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.0-dev"
+                    "dev-master": "3.1-dev"
                 }
             },
             "autoload": {
             "keywords": [
                 "tokenizer"
             ],
-            "time": "2019-07-08T05:24:54+00:00"
+            "time": "2019-07-25T05:29:42+00:00"
         },
         {
             "name": "phpunit/phpunit",
-            "version": "7.5.14",
+            "version": "7.5.15",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
-                "reference": "2834789aeb9ac182ad69bfdf9ae91856a59945ff"
+                "reference": "d79c053d972856b8b941bb233e39dc521a5093f0"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/2834789aeb9ac182ad69bfdf9ae91856a59945ff",
-                "reference": "2834789aeb9ac182ad69bfdf9ae91856a59945ff",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d79c053d972856b8b941bb233e39dc521a5093f0",
+                "reference": "d79c053d972856b8b941bb233e39dc521a5093f0",
                 "shasum": ""
             },
             "require": {
             "authors": [
                 {
                     "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de",
-                    "role": "lead"
+                    "role": "lead",
+                    "email": "sebastian@phpunit.de"
                 }
             ],
             "description": "The PHP Unit Testing framework.",
                 "testing",
                 "xunit"
             ],
-            "time": "2019-07-15T06:24:08+00:00"
+            "time": "2019-08-21T07:05:16+00:00"
         },
         {
             "name": "psr/container",
         },
         {
             "name": "sebastian/exporter",
-            "version": "3.1.0",
+            "version": "3.1.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/exporter.git",
-                "reference": "234199f4528de6d12aaa58b612e98f7d36adb937"
+                "reference": "06a9a5947f47b3029d76118eb5c22802e5869687"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/234199f4528de6d12aaa58b612e98f7d36adb937",
-                "reference": "234199f4528de6d12aaa58b612e98f7d36adb937",
+                "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/06a9a5947f47b3029d76118eb5c22802e5869687",
+                "reference": "06a9a5947f47b3029d76118eb5c22802e5869687",
                 "shasum": ""
             },
             "require": {
                 "BSD-3-Clause"
             ],
             "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de"
+                },
                 {
                     "name": "Jeff Welch",
                     "email": "whatthejeff@gmail.com"
                     "name": "Volker Dusch",
                     "email": "github@wallbash.com"
                 },
-                {
-                    "name": "Bernhard Schussek",
-                    "email": "bschussek@2bepublished.at"
-                },
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de"
-                },
                 {
                     "name": "Adam Harvey",
                     "email": "aharvey@php.net"
+                },
+                {
+                    "name": "Bernhard Schussek",
+                    "email": "bschussek@gmail.com"
                 }
             ],
             "description": "Provides the functionality to export PHP variables for visualization",
                 "export",
                 "exporter"
             ],
-            "time": "2017-04-03T13:19:02+00:00"
+            "time": "2019-08-11T12:43:14+00:00"
         },
         {
             "name": "sebastian/global-state",
             "authors": [
                 {
                     "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de",
-                    "role": "lead"
+                    "role": "lead",
+                    "email": "sebastian@phpunit.de"
                 }
             ],
             "description": "Library that helps with managing the version number of Git-hosted PHP projects",
         },
         {
             "name": "symfony/browser-kit",
-            "version": "v4.3.2",
+            "version": "v4.3.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/browser-kit.git",
-                "reference": "a29dd02a1f3f81b9a15c7730cc3226718ddb55ca"
+                "reference": "9e5dddb637b13db82e35695a8603fe6e118cc119"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/browser-kit/zipball/a29dd02a1f3f81b9a15c7730cc3226718ddb55ca",
-                "reference": "a29dd02a1f3f81b9a15c7730cc3226718ddb55ca",
+                "url": "https://api.github.com/repos/symfony/browser-kit/zipball/9e5dddb637b13db82e35695a8603fe6e118cc119",
+                "reference": "9e5dddb637b13db82e35695a8603fe6e118cc119",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony BrowserKit Component",
             "homepage": "https://symfony.com",
-            "time": "2019-06-11T15:41:59+00:00"
+            "time": "2019-08-26T08:26:39+00:00"
         },
         {
             "name": "symfony/class-loader",
-            "version": "v3.4.29",
+            "version": "v3.4.31",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/class-loader.git",
-                "reference": "4459eef5298dedfb69f771186a580062b8516497"
+                "reference": "e212b06996819a2bce026a63da03b7182d05a690"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/class-loader/zipball/4459eef5298dedfb69f771186a580062b8516497",
-                "reference": "4459eef5298dedfb69f771186a580062b8516497",
+                "url": "https://api.github.com/repos/symfony/class-loader/zipball/e212b06996819a2bce026a63da03b7182d05a690",
+                "reference": "e212b06996819a2bce026a63da03b7182d05a690",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony ClassLoader Component",
             "homepage": "https://symfony.com",
-            "time": "2019-01-16T09:39:14+00:00"
+            "time": "2019-08-20T13:31:17+00:00"
         },
         {
             "name": "symfony/config",
-            "version": "v4.3.2",
+            "version": "v4.3.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/config.git",
-                "reference": "9198eea354be75794a7b1064de00d9ae9ae5090f"
+                "reference": "07d49c0f823e0bc367c6d84e35b61419188a5ece"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/config/zipball/9198eea354be75794a7b1064de00d9ae9ae5090f",
-                "reference": "9198eea354be75794a7b1064de00d9ae9ae5090f",
+                "url": "https://api.github.com/repos/symfony/config/zipball/07d49c0f823e0bc367c6d84e35b61419188a5ece",
+                "reference": "07d49c0f823e0bc367c6d84e35b61419188a5ece",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Config Component",
             "homepage": "https://symfony.com",
-            "time": "2019-06-08T06:33:08+00:00"
+            "time": "2019-08-26T08:26:39+00:00"
         },
         {
             "name": "symfony/console",
         },
         {
             "name": "symfony/css-selector",
-            "version": "v3.4.29",
+            "version": "v3.4.31",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/css-selector.git",
-                "reference": "8ca29297c29b64fb3a1a135e71cb25f67f9fdccf"
+                "reference": "e18c5c4b35e7f17513448a25d02f7af34a4bdb41"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/css-selector/zipball/8ca29297c29b64fb3a1a135e71cb25f67f9fdccf",
-                "reference": "8ca29297c29b64fb3a1a135e71cb25f67f9fdccf",
+                "url": "https://api.github.com/repos/symfony/css-selector/zipball/e18c5c4b35e7f17513448a25d02f7af34a4bdb41",
+                "reference": "e18c5c4b35e7f17513448a25d02f7af34a4bdb41",
                 "shasum": ""
             },
             "require": {
                 "MIT"
             ],
             "authors": [
-                {
-                    "name": "Jean-François Simon",
-                    "email": "jeanfrancois.simon@sensiolabs.com"
-                },
                 {
                     "name": "Fabien Potencier",
                     "email": "fabien@symfony.com"
                 },
+                {
+                    "name": "Jean-François Simon",
+                    "email": "jeanfrancois.simon@sensiolabs.com"
+                },
                 {
                     "name": "Symfony Community",
                     "homepage": "https://symfony.com/contributors"
             ],
             "description": "Symfony CssSelector Component",
             "homepage": "https://symfony.com",
-            "time": "2019-01-16T09:39:14+00:00"
+            "time": "2019-08-20T13:31:17+00:00"
         },
         {
             "name": "symfony/debug",
-            "version": "v3.4.29",
+            "version": "v3.4.31",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/debug.git",
-                "reference": "1172dc1abe44dfadd162239153818b074e6e53bf"
+                "reference": "0b600300918780001e2821db77bc28b677794486"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/debug/zipball/1172dc1abe44dfadd162239153818b074e6e53bf",
-                "reference": "1172dc1abe44dfadd162239153818b074e6e53bf",
+                "url": "https://api.github.com/repos/symfony/debug/zipball/0b600300918780001e2821db77bc28b677794486",
+                "reference": "0b600300918780001e2821db77bc28b677794486",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Debug Component",
             "homepage": "https://symfony.com",
-            "time": "2019-06-18T21:26:03+00:00"
+            "time": "2019-08-20T13:31:17+00:00"
         },
         {
             "name": "symfony/dependency-injection",
         },
         {
             "name": "symfony/dom-crawler",
-            "version": "v4.3.2",
+            "version": "v4.3.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/dom-crawler.git",
-                "reference": "291397232a2eefb3347eaab9170409981eaad0e2"
+                "reference": "cc686552948d627528c0e2e759186dff67c2610e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/291397232a2eefb3347eaab9170409981eaad0e2",
-                "reference": "291397232a2eefb3347eaab9170409981eaad0e2",
+                "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/cc686552948d627528c0e2e759186dff67c2610e",
+                "reference": "cc686552948d627528c0e2e759186dff67c2610e",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony DomCrawler Component",
             "homepage": "https://symfony.com",
-            "time": "2019-06-13T11:03:18+00:00"
+            "time": "2019-08-26T08:26:39+00:00"
         },
         {
             "name": "symfony/event-dispatcher",
-            "version": "v3.4.29",
+            "version": "v3.4.31",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/event-dispatcher.git",
-                "reference": "f18fdd6cc7006441865e698420cee26bac94741f"
+                "reference": "3e922c4c3430b9de624e8a285dada5e61e230959"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/f18fdd6cc7006441865e698420cee26bac94741f",
-                "reference": "f18fdd6cc7006441865e698420cee26bac94741f",
+                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/3e922c4c3430b9de624e8a285dada5e61e230959",
+                "reference": "3e922c4c3430b9de624e8a285dada5e61e230959",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony EventDispatcher Component",
             "homepage": "https://symfony.com",
-            "time": "2019-06-25T07:45:31+00:00"
+            "time": "2019-08-23T08:05:57+00:00"
         },
         {
             "name": "symfony/filesystem",
-            "version": "v4.3.2",
+            "version": "v4.3.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/filesystem.git",
-                "reference": "b9896d034463ad6fd2bf17e2bf9418caecd6313d"
+                "reference": "9abbb7ef96a51f4d7e69627bc6f63307994e4263"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/filesystem/zipball/b9896d034463ad6fd2bf17e2bf9418caecd6313d",
-                "reference": "b9896d034463ad6fd2bf17e2bf9418caecd6313d",
+                "url": "https://api.github.com/repos/symfony/filesystem/zipball/9abbb7ef96a51f4d7e69627bc6f63307994e4263",
+                "reference": "9abbb7ef96a51f4d7e69627bc6f63307994e4263",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Filesystem Component",
             "homepage": "https://symfony.com",
-            "time": "2019-06-23T08:51:25+00:00"
+            "time": "2019-08-20T14:07:54+00:00"
         },
         {
             "name": "symfony/polyfill-ctype",
-            "version": "v1.11.0",
+            "version": "v1.12.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-ctype.git",
-                "reference": "82ebae02209c21113908c229e9883c419720738a"
+                "reference": "550ebaac289296ce228a706d0867afc34687e3f4"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/82ebae02209c21113908c229e9883c419720738a",
-                "reference": "82ebae02209c21113908c229e9883c419720738a",
+                "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/550ebaac289296ce228a706d0867afc34687e3f4",
+                "reference": "550ebaac289296ce228a706d0867afc34687e3f4",
                 "shasum": ""
             },
             "require": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.11-dev"
+                    "dev-master": "1.12-dev"
                 }
             },
             "autoload": {
                 "MIT"
             ],
             "authors": [
-                {
-                    "name": "Symfony Community",
-                    "homepage": "https://symfony.com/contributors"
-                },
                 {
                     "name": "Gert de Pagter",
                     "email": "BackEndTea@gmail.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
                 }
             ],
             "description": "Symfony polyfill for ctype functions",
                 "polyfill",
                 "portable"
             ],
-            "time": "2019-02-06T07:57:58+00:00"
+            "time": "2019-08-06T08:03:45+00:00"
         },
         {
             "name": "symfony/polyfill-mbstring",
-            "version": "v1.11.0",
+            "version": "v1.12.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-mbstring.git",
-                "reference": "fe5e94c604826c35a32fa832f35bd036b6799609"
+                "reference": "b42a2f66e8f1b15ccf25652c3424265923eb4f17"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fe5e94c604826c35a32fa832f35bd036b6799609",
-                "reference": "fe5e94c604826c35a32fa832f35bd036b6799609",
+                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/b42a2f66e8f1b15ccf25652c3424265923eb4f17",
+                "reference": "b42a2f66e8f1b15ccf25652c3424265923eb4f17",
                 "shasum": ""
             },
             "require": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.11-dev"
+                    "dev-master": "1.12-dev"
                 }
             },
             "autoload": {
                 "portable",
                 "shim"
             ],
-            "time": "2019-02-06T07:57:58+00:00"
+            "time": "2019-08-06T08:03:45+00:00"
         },
         {
             "name": "symfony/process",
             "authors": [
                 {
                     "name": "Arne Blankerts",
-                    "email": "arne@blankerts.de",
-                    "role": "Developer"
+                    "role": "Developer",
+                    "email": "arne@blankerts.de"
                 }
             ],
             "description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
         },
         {
             "name": "webmozart/assert",
-            "version": "1.4.0",
+            "version": "1.5.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/webmozart/assert.git",
-                "reference": "83e253c8e0be5b0257b881e1827274667c5c17a9"
+                "reference": "88e6d84706d09a236046d686bbea96f07b3a34f4"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/webmozart/assert/zipball/83e253c8e0be5b0257b881e1827274667c5c17a9",
-                "reference": "83e253c8e0be5b0257b881e1827274667c5c17a9",
+                "url": "https://api.github.com/repos/webmozart/assert/zipball/88e6d84706d09a236046d686bbea96f07b3a34f4",
+                "reference": "88e6d84706d09a236046d686bbea96f07b3a34f4",
                 "shasum": ""
             },
             "require": {
                 "symfony/polyfill-ctype": "^1.8"
             },
             "require-dev": {
-                "phpunit/phpunit": "^4.6",
-                "sebastian/version": "^1.0.1"
+                "phpunit/phpunit": "^4.8.36 || ^7.5.13"
             },
             "type": "library",
             "extra": {
                 "check",
                 "validate"
             ],
-            "time": "2018-12-25T11:19:39+00:00"
+            "time": "2019-08-24T08:43:50+00:00"
         }
     ],
     "aliases": [
index 4257851..b87da32 100644 (file)
@@ -68,8 +68,10 @@ class activities_due extends \core_analytics\local\indicator\binary {
      */
     protected function calculate_sample($sampleid, $sampleorigin, $starttime = false, $endtime = false) {
 
+        $user = $this->retrieve('user', $sampleid);
+
         $actionevents = \core_calendar_external::get_calendar_action_events_by_timesort($starttime, $endtime, 0, 1,
-            true, $sampleid);
+            true, $user->id);
 
         if ($actionevents->events) {
 
index 7f3a170..8877119 100644 (file)
@@ -3741,6 +3741,8 @@ class core_course_external extends external_api {
         $sort = $params['sort'];
 
         switch($classification) {
+            case COURSE_TIMELINE_ALLINCLUDINGHIDDEN:
+                break;
             case COURSE_TIMELINE_ALL:
                 break;
             case COURSE_TIMELINE_PAST:
@@ -3764,10 +3766,16 @@ class core_course_external extends external_api {
         $hiddencourses = get_hidden_courses_on_timeline();
         $courses = [];
 
-        // If the timeline requires the hidden courses then restrict the result to only $hiddencourses else exclude.
-        if ($classification == COURSE_TIMELINE_HIDDEN) {
+        // If the timeline requires really all courses, get really all courses.
+        if ($classification == COURSE_TIMELINE_ALLINCLUDINGHIDDEN) {
+            $courses = course_get_enrolled_courses_for_logged_in_user(0, $offset, $sort, $fields, COURSE_DB_QUERY_LIMIT);
+
+            // Otherwise if the timeline requires the hidden courses then restrict the result to only $hiddencourses.
+        } else if ($classification == COURSE_TIMELINE_HIDDEN) {
             $courses = course_get_enrolled_courses_for_logged_in_user(0, $offset, $sort, $fields,
                 COURSE_DB_QUERY_LIMIT, $hiddencourses);
+
+            // Otherwise get the requested courses and exclude the hidden courses.
         } else {
             $courses = course_get_enrolled_courses_for_logged_in_user(0, $offset, $sort, $fields,
                 COURSE_DB_QUERY_LIMIT, [], $hiddencourses);
index dc8b162..3eec792 100644 (file)
@@ -56,6 +56,7 @@ define('FIRSTUSEDEXCELROW', 3);
 define('MOD_CLASS_ACTIVITY', 0);
 define('MOD_CLASS_RESOURCE', 1);
 
+define('COURSE_TIMELINE_ALLINCLUDINGHIDDEN', 'allincludinghidden');
 define('COURSE_TIMELINE_ALL', 'all');
 define('COURSE_TIMELINE_PAST', 'past');
 define('COURSE_TIMELINE_INPROGRESS', 'inprogress');
@@ -2558,7 +2559,7 @@ function update_course($data, $editoroptions = NULL) {
                 // The summary might be very long, we don't wan't to fill up the log record with the full text.
                 $updatedfields[$field] = '(updated)';
             }
-        } else if ($field == 'tags') {
+        } else if ($field == 'tags' && !empty($CFG->usetags)) {
             // Tags might not have the same array keys, just check the values.
             if (array_values($data->$field) !== array_values($value)) {
                 $updatedfields[$field] = $data->$field;
@@ -4320,9 +4321,9 @@ function course_filter_courses_by_timeline_classification(
 ) : array {
 
     if (!in_array($classification,
-            [COURSE_TIMELINE_ALL, COURSE_TIMELINE_PAST, COURSE_TIMELINE_INPROGRESS,
+            [COURSE_TIMELINE_ALLINCLUDINGHIDDEN, COURSE_TIMELINE_ALL, COURSE_TIMELINE_PAST, COURSE_TIMELINE_INPROGRESS,
                 COURSE_TIMELINE_FUTURE, COURSE_TIMELINE_HIDDEN])) {
-        $message = 'Classification must be one of COURSE_TIMELINE_ALL, COURSE_TIMELINE_PAST, '
+        $message = 'Classification must be one of COURSE_TIMELINE_ALLINCLUDINGHIDDEN, COURSE_TIMELINE_ALL, COURSE_TIMELINE_PAST, '
             . 'COURSE_TIMELINE_INPROGRESS or COURSE_TIMELINE_FUTURE';
         throw new moodle_exception($message);
     }
@@ -4336,7 +4337,7 @@ function course_filter_courses_by_timeline_classification(
         $pref = get_user_preferences('block_myoverview_hidden_course_' . $course->id, 0);
 
         // Added as of MDL-63457 toggle viewability for each user.
-        if (($classification == COURSE_TIMELINE_HIDDEN && $pref) ||
+        if ($classification == COURSE_TIMELINE_ALLINCLUDINGHIDDEN || ($classification == COURSE_TIMELINE_HIDDEN && $pref) ||
             (($classification == COURSE_TIMELINE_ALL || $classification == course_classify_for_timeline($course)) && !$pref)) {
             $filteredcourses[] = $course;
             $filtermatches++;
index 27784d1..492a9f1 100644 (file)
@@ -69,8 +69,11 @@ if ($courseid) {
     $course = null;
     $courseid = null;
     $topchildren = core_course_category::top()->get_children();
+    if (empty($topchildren)) {
+        throw new moodle_exception('cannotviewcategory', 'error');
+    }
     $category = reset($topchildren);
-    $categoryid = $category ? $category->id : 0;
+    $categoryid = $category->id;
     $context = context_coursecat::instance($category->id);
     $url->param('categoryid', $category->id);
 }
index c3583e1..3b2af23 100644 (file)
@@ -64,7 +64,7 @@
         }
     }
 }}
-<div class="mt-2 mb-1 activity-navigation">
+<div class="mt-5 mb-1 activity-navigation">
 {{< core/columns-1to1to1}}
     {{$column1}}
         <div class="float-left">
index 1749a13..3ad47f9 100644 (file)
@@ -28,6 +28,7 @@ defined('MOODLE_INTERNAL') || die();
 global $CFG;
 require_once(__DIR__ . '/../../lib/completionlib.php');
 require_once(__DIR__ . '/../../completion/criteria/completion_criteria_self.php');
+require_once(__DIR__ . '/../../analytics/tests/fixtures/test_target_course_users.php');
 
 /**
  * Unit tests for core_course indicators.
@@ -313,4 +314,30 @@ class core_course_indicators_testcase extends advanced_testcase {
         // Page social is level 1 (the lower level).
         $this->assertEquals($indicator::get_min_value(), $values[$cm2->id][0]);
     }
+
+    /**
+     * test_activities_due
+     *
+     * @return void
+     */
+    public function test_activities_due() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+        $this->setAdminuser();
+
+        $course1 = $this->getDataGenerator()->create_course();
+        $user1 = $this->getDataGenerator()->create_user();
+        $this->getDataGenerator()->enrol_user($user1->id, $course1->id, 'student');
+
+        $target = \core_analytics\manager::get_target('test_target_course_users');
+        $indicators = array('\core_course\analytics\indicator\activities_due');
+        foreach ($indicators as $key => $indicator) {
+            $indicators[$key] = \core_analytics\manager::get_indicator($indicator);
+        }
+
+        $model = \core_analytics\model::create($target, $indicators);
+        $model->enable('\core\analytics\time_splitting\single_range');
+        $model->train();
+    }
 }
index 10407f9..d48e163 100644 (file)
@@ -2,8 +2,12 @@ This files describes API changes in /course/*,
 information provided here is intended especially for developers.
 
 === 3.8 ===
+
 * The following functions have been finally deprecated and can not be used any more:
   - core_course_external::get_activities_overview
+* External function core_course_external::get_enrolled_courses_by_timeline_classification now also supports the classification
+  'allincludinghidden' which delivers all courses including hidden courses. The classification 'all' still returns all courses
+  without hidden courses.
 
 === 3.7 ===
 
index 687f1a4..8cf1d39 100644 (file)
@@ -23,6 +23,7 @@ Feature: Converting rubric score to grades
       | activity   | name              | intro | course | idnumber    | grade   | advancedgradingmethod_submissions |
       | assign     | Test assignment 1 | Test  | C1     | assign1     | <grade> | rubric                            |
     When I log in as "teacher1"
+    And I change window size to "large"
     And I am on "Course 1" course homepage with editing mode on
     And I go to "Test assignment 1" advanced grading definition page
     And I set the following fields to these values:
index 33d0e1e..363bb6f 100644 (file)
@@ -26,6 +26,7 @@ Feature: Rubrics can have levels with negative scores
     And the following "activities" exist:
       | activity   | name              | intro | course | idnumber    | grade   | advancedgradingmethod_submissions |
       | assign     | Test assignment 1 | Test  | C1     | assign1     | 100     | rubric                            |
+    And I change window size to "large"
     When I log in as "teacher1"
     And I am on "Course 1" course homepage with editing mode on
     And I go to "Test assignment 1" advanced grading definition page
index 06a9518..1b1326a 100644 (file)
@@ -77,7 +77,7 @@ Feature: A teacher checks the grade history report in a course
       | Student 2          | The greatest assignment ever  | 50.00          | 70.00         | Teacher 2 |
       | Student 2          | Rewarding assignment          | 60.00          | 80.00         | Teacher 2 |
     # Test filtering by assignment.
-    And I click on "The greatest assignment ever" "option" in the "#id_itemid" "css_element"
+    And I set the field "Grade item" to "The greatest assignment ever"
     And I press "Submit"
     And the following should exist in the "gradereport_history" table:
       | First name/Surname | Grade item                    | Original grade | Revised grade | Grader    |
@@ -87,7 +87,7 @@ Feature: A teacher checks the grade history report in a course
       | Student 1          | Rewarding assignment          |                | 60.00         | Teacher 1 |
       | Student 1          | Rewarding assignment          | 60.00          | 80.00         | Teacher 2 |
     # Test filtering by grader.
-    And I click on "Teacher 1" "option" in the "#id_grader" "css_element"
+    And I set the field "Grader" to "Teacher 1"
     And I press "Submit"
     And the following should exist in the "gradereport_history" table:
       | First name/Surname | Grade item                    | Original grade | Revised grade | Grader    |
index 50f1ce9..06b37e1 100644 (file)
@@ -93,7 +93,8 @@ class dropdown_attribute extends element {
                     'value' => $option,
                     'selected' => $selected == $option
                 ];
-            }, array_keys($options))
+            }, array_keys($options)),
+            'label' => get_string('gradefor', 'gradereport_singleview', $this->label),
         );
 
         return $OUTPUT->render_from_template('gradereport_singleview/dropdown_attribute', $context);
index 436c685..3b0bdb9 100644 (file)
@@ -28,6 +28,7 @@
         "selected": "true"
     }
 }}
+<label for="{{name}}" class="accesshide">{{label}}</label>
 <select id="{{name}}" name="{{name}}" class="custom-select" tabindex="1" {{#disabled}}disabled{{/disabled}}>
     {{#options}}
         <option value="{{value}}" {{#selected}}selected{{/selected}}>{{name}}</option>
index d86cdcf..e18eb5c 100644 (file)
@@ -87,7 +87,7 @@ Feature: We can use Single view
     And the field "Grade for james (Student) 1" matches value "12.05"
     And the field "Exclude for holly (Student) 2" matches value "1"
     And I select "new grade item 1" from the "Select grade item..." singleselect
-    And I click on "Very good" "option"
+    And I set the field "Grade for james (Student) 1" to "Very good"
     And I press "Save"
     Then I should see "Grades were set for 1 items"
     And I press "Continue"
@@ -98,7 +98,7 @@ Feature: We can use Single view
     And I log in as "teacher2"
     And I am on "Course 1" course homepage
     Given I navigate to "View > Single view" in the course gradebook
-    And I click on "Student 4" "option"
+    And I select "Student 4" from the "Select user..." singleselect
     And the "Exclude for Test assignment one" "checkbox" should be disabled
     And the "Override for Test assignment one" "checkbox" should be enabled
 
index ead9bd3..c248e34 100644 (file)
@@ -30,4 +30,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
+$string['moodlelogo'] = 'Logo Moodle';
+$string['next'] = 'Următoarea';
 $string['previous'] = 'Anterior';
+$string['reload'] = 'Reîncarcă';
index 99cc293..15bda41 100644 (file)
@@ -29,6 +29,8 @@ $string['accessstatement'] = 'Accessibility statement';
 $string['activitynext'] = 'Next activity';
 $string['activityprev'] = 'Previous activity';
 $string['breadcrumb'] = 'Navigation bar';
+$string['eventcontextlocked'] = 'Context frozen';
+$string['eventcontextunlocked'] = 'Context unfrozen';
 $string['hideblocka'] = 'Hide {$a} block';
 $string['showblocka'] = 'Show {$a} block';
 $string['sitemap'] = 'Site map';
index 7852893..7e2b8a3 100644 (file)
@@ -319,7 +319,6 @@ $string['configrequestedstudentname'] = 'Word for student used in requested cour
 $string['configrequestedstudentsname'] = 'Word for students used in requested courses';
 $string['configrequestedteachername'] = 'Word for teacher used in requested courses';
 $string['configrequestedteachersname'] = 'Word for teachers used in requested courses';
-$string['configuserquota'] = 'The maximum number of bytes that a user can store in their own private file area. {$a->bytes} bytes == {$a->displaysize}';
 $string['configsectioninterface'] = 'Interface';
 $string['configsectionmail'] = 'Mail';
 $string['configsectionmaintenance'] = 'Maintenance';
@@ -568,6 +567,7 @@ $string['filescleanupperiod'] = 'Clean trash pool files';
 $string['filescleanupperiod_help'] = 'How often trash files are removed. These are files that are associated with a context that no longer exists';
 $string['fileconversioncleanuptask'] = 'Cleanup of temporary records for file conversions.';
 $string['filecreated'] = 'New file created';
+$string['filesizeunits'] = 'file size units';
 $string['filestoredin'] = 'Save file into folder :';
 $string['filestoredinhelp'] = 'Where the file will be stored';
 $string['filterall'] = 'Filter all strings';
@@ -1389,6 +1389,7 @@ $string['userpreference'] = 'User preference';
 $string['userpolicies'] = 'User policies';
 $string['users'] = 'Users';
 $string['userquota'] = 'User quota';
+$string['userquota_desc'] = 'The maximum number of bytes that a user can store in their own private file area.';
 $string['usesitenameforsitepages'] = 'Use site name for site pages';
 $string['usetags'] = 'Enable tags functionality';
 $string['validateemptylineerror'] = 'Empty lines are not valid';
@@ -1416,4 +1417,6 @@ $string['cacheapplicationhelp'] = 'Cached items are shared among all users and e
 
 // Deprecated since Moodle 3.7.
 $string['allowblockstodock'] = 'Allow blocks to use the dock';
-$string['configallowblockstodock'] = 'If enabled and supported by the selected theme users can choose to move blocks to a special dock.';
\ No newline at end of file
+$string['configallowblockstodock'] = 'If enabled and supported by the selected theme users can choose to move blocks to a special dock.';
+// Deprecated since Moodle 3.8.
+$string['configuserquota'] = 'The maximum number of bytes that a user can store in their own private file area. {$a->bytes} bytes == {$a->displaysize}';
index 2a4ee80..e379bb9 100644 (file)
@@ -81,6 +81,7 @@ $string['cachedef_user_group_groupings'] = 'User\'s groupings and groups per cou
 $string['cachedef_yuimodules'] = 'YUI Module definitions';
 $string['cachelock_file_default'] = 'Default file locking';
 $string['cachestores'] = 'Cache stores';
+$string['canuselocalstore'] = 'Can use local store';
 $string['component'] = 'Component';
 $string['confirmlockdeletion'] = 'Confirm lock deletion';
 $string['confirmstoredeletion'] = 'Confirm store deletion';
index f553e1c..32d16f2 100644 (file)
@@ -46,4 +46,5 @@ nobackpackcollections,core_badges
 error:nogroups,core_badges
 purgedefinitionsuccess,core_cache
 purgestoresuccess,core_cache
-eventrolecapabilitiesupdated,core_role
\ No newline at end of file
+eventrolecapabilitiesupdated,core_role
+configuserquota,core_admin
index 2383769..d07475a 100644 (file)
@@ -5276,6 +5276,15 @@ abstract class context extends stdClass implements IteratorAggregate {
         $this->_locked = $locked;
         $DB->set_field('context', 'locked', (int) $locked, ['id' => $this->id]);
         $this->mark_dirty();
+
+        if ($locked) {
+            $eventname = '\\core\\event\\context_locked';
+        } else {
+            $eventname = '\\core\\event\\context_unlocked';
+        }
+        $event = $eventname::create(['context' => $this, 'objectid' => $this->id]);
+        $event->trigger();
+
         self::reset_caches();
 
         return $this;
index e8b3649..ac2142b 100644 (file)
@@ -8611,7 +8611,6 @@ function admin_search_settings_html($query) {
                     $data = $adminroot->errors[$fullname]->data;
                 } else {
                     $data = $setting->get_setting();
-                    $data = $setting->get_setting();
                 // do not use defaults if settings not available - upgradesettings handles the defaults!
                 }
                 $sectionsettings[] = $setting->output_html($data, $query);
index 549a653..1688749 100644 (file)
Binary files a/lib/amd/build/form-autocomplete.min.js and b/lib/amd/build/form-autocomplete.min.js differ
index db0aeaf..11e43c8 100644 (file)
Binary files a/lib/amd/build/form-autocomplete.min.js.map and b/lib/amd/build/form-autocomplete.min.js.map differ
index b91dd0b..43e3227 100644 (file)
@@ -718,6 +718,7 @@ function($, log, str, templates, notification, LoadingIcon) {
             window.setTimeout(function() {
                 // Get the current element with focus.
                 var focusElement = $(document.activeElement);
+                var timeoutPromise = $.Deferred();
 
                 // Only close the menu if the input hasn't regained focus and if the element still exists,
                 // and regain focus if the scrollbar is clicked.
@@ -727,18 +728,22 @@ function($, log, str, templates, notification, LoadingIcon) {
                     inputElement.focus(); // Probably the scrollbar is clicked. Regain focus.
                 } else if (!focusElement.is(inputElement) && $(document.getElementById(state.inputId)).length) {
                     if (options.tags) {
-                        pendingPromise.then(function() {
+                        timeoutPromise.then(function() {
                             return createItem(options, state, originalSelect);
                         })
                         .catch();
                     }
-                    pendingPromise.then(function() {
+                    timeoutPromise.then(function() {
                         return closeSuggestions(state);
                     })
                     .catch();
                 }
 
-                pendingPromise.resolve();
+                timeoutPromise.then(function() {
+                    return pendingPromise.resolve();
+                })
+                .catch();
+                timeoutPromise.resolve();
             }, 500);
         });
         if (options.showSuggestions) {
index 750251b..65c2c6a 100644 (file)
@@ -702,11 +702,11 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
 
         switch ($windowsize) {
             case "small":
-                $width = 640;
-                $height = 480;
+                $width = 1024;
+                $height = 768;
                 break;
             case "medium":
-                $width = 1024;
+                $width = 1366;
                 $height = 768;
                 break;
             case "large":
index 3f8c053..8503da8 100644 (file)
@@ -74,6 +74,7 @@ class behat_partial_named_selector extends \Behat\Mink\Selector\PartialNamedSele
         'form_row' => 'form_row',
         'group_message_header' => 'group_message_header',
         'group_message' => 'group_message',
+        'autocomplete' => 'autocomplete',
     );
 
     /**
@@ -116,6 +117,7 @@ class behat_partial_named_selector extends \Behat\Mink\Selector\PartialNamedSele
         'form_row' => 'form_row',
         'autocomplete_selection' => 'autocomplete_selection',
         'autocomplete_suggestions' => 'autocomplete_suggestions',
+        'autocomplete' => 'autocomplete',
     );
 
     /**
@@ -221,6 +223,9 @@ XPATH
 XPATH
         , 'autocomplete_suggestions' => <<<XPATH
 .//ul[contains(concat(' ', normalize-space(@class), ' '), concat(' ', 'form-autocomplete-suggestions', ' '))]/li[@role='option'][contains(normalize-space(.), %locator%)]
+XPATH
+        , 'autocomplete' => <<<XPATH
+.//descendant::input[@id = //label[contains(normalize-space(string(.)), %locator%)]/@for]/ancestor::*[@data-fieldtype = 'autocomplete']
 XPATH
     );
 
diff --git a/lib/classes/event/context_locked.php b/lib/classes/event/context_locked.php
new file mode 100644 (file)
index 0000000..f4bf990
--- /dev/null
@@ -0,0 +1,85 @@
+<?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/>.
+
+/**
+ * Context locked event.
+ *
+ * @package    core_access
+ * @copyright  2019 University of Nottingham
+ * @author     Neill Magill <neill.magill@nottingham.ac.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Event triggered after a context has been frozen.
+ *
+ * @package    core_access
+ * @since      Moodle 3.8
+ * @copyright  2019 University of Nottingham
+ * @author     Neill Magill <neill.magill@nottingham.ac.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class context_locked extends base {
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' locked the context with id '$this->objectid' ";
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventcontextlocked', 'access');
+    }
+
+    /**
+     * Get URL related to the action
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        // Try to get the url for the context.
+        try {
+            $context = \context::instance_by_id($this->objectid);
+            $url = $context->get_url();
+        } catch (\dml_missing_record_exception $e) {
+            // The context no longer exists, give them the system url instead.
+            $url = \context_system::instance()->get_url();
+        }
+        return $url;
+    }
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['crud'] = 'u';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+        $this->data['objecttable'] = 'context';
+    }
+}
diff --git a/lib/classes/event/context_unlocked.php b/lib/classes/event/context_unlocked.php
new file mode 100644 (file)
index 0000000..92e6ece
--- /dev/null
@@ -0,0 +1,85 @@
+<?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/>.
+
+/**
+ * Context unlocked event.
+ *
+ * @package    core_access
+ * @copyright  2019 University of Nottingham
+ * @author     Neill Magill <neill.magill@nottingham.ac.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Event triggered after a context has been unfrozen.
+ *
+ * @package    core_access
+ * @since      Moodle 3.8
+ * @copyright  2019 University of Nottingham
+ * @author     Neill Magill <neill.magill@nottingham.ac.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class context_unlocked extends base {
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' unlocked the context with id '$this->objectid' ";
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventcontextunlocked', 'access');
+    }
+
+    /**
+     * Get URL related to the action
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        // Try to get the url for the context.
+        try {
+            $context = \context::instance_by_id($this->objectid);
+            $url = $context->get_url();
+        } catch (\dml_missing_record_exception $e) {
+            // The context no longer exists, give them the system url instead.
+            $url = \context_system::instance()->get_url();
+        }
+        return $url;
+    }
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['crud'] = 'u';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+        $this->data['objecttable'] = 'context';
+    }
+}
index e340326..826f814 100644 (file)
@@ -310,11 +310,11 @@ class mustache_template_source_loader {
             if ($name) {
                 switch ($type) {
                     case Mustache_Tokenizer::T_PARTIAL:
-                        list($component, $id) = explode('/', $name);
+                        list($component, $id) = explode('/', $name, 2);
                         $templates = $addtodependencies($templates, $component, $id);
                         break;
                     case Mustache_Tokenizer::T_PARENT:
-                        list($component, $id) = explode('/', $name);
+                        list($component, $id) = explode('/', $name, 2);
                         $templates = $addtodependencies($templates, $component, $id);
                         break;
                     case Mustache_Tokenizer::T_SECTION:
index f6cd4ec..d1dec99 100644 (file)
@@ -862,6 +862,7 @@ $functions = array(
         'classpath' => 'group/externallib.php',
         'description' => 'Returns all groupings in specified course.',
         'type' => 'read',
+        'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
     'core_group_get_course_groups' => array(
         'classname' => 'core_group_external',
@@ -870,7 +871,8 @@ $functions = array(
         'description' => 'Returns all groups in specified course.',
         'type' => 'read',
         'ajax' => true,
-        'capabilities' => 'moodle/course:managegroups'
+        'capabilities' => 'moodle/course:managegroups',
+        'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
     'core_group_get_course_user_groups' => array(
         'classname' => 'core_group_external',
index 9e781cd..021706b 100644 (file)
@@ -834,7 +834,23 @@ abstract class moodle_database {
      * @return string The sql with tablenames being prefixed with $CFG->prefix
      */
     protected function fix_table_names($sql) {
-        return preg_replace('/\{([a-z][a-z0-9_]*)\}/', $this->prefix.'$1', $sql);
+        return preg_replace_callback(
+            '/\{([a-z][a-z0-9_]*)\}/',
+            function($matches) {
+                return $this->fix_table_name($matches[1]);
+            },
+            $sql
+        );
+    }
+
+    /**
+     * Adds the prefix to the table name.
+     *
+     * @param string $tablename The table name
+     * @return string The prefixed table name
+     */
+    protected function fix_table_name($tablename) {
+        return $this->prefix . $tablename;
     }
 
     /**
index b0a6a70..8469c0d 100644 (file)
@@ -675,7 +675,8 @@ class mysqli_native_moodle_database extends moodle_database {
      */
     public function get_indexes($table) {
         $indexes = array();
-        $sql = "SHOW INDEXES FROM {$this->prefix}$table";
+        $fixedtable = $this->fix_table_name($table);
+        $sql = "SHOW INDEXES FROM $fixedtable";
         $this->query_start($sql, null, SQL_QUERY_AUX);
         $result = $this->mysqli->query($sql);
         try {
@@ -746,7 +747,8 @@ class mysqli_native_moodle_database extends moodle_database {
         } else {
             // temporary tables are not in information schema, let's try it the old way
             $result->close();
-            $sql = "SHOW COLUMNS FROM {$this->prefix}$table";
+            $fixedtable = $this->fix_table_name($table);
+            $sql = "SHOW COLUMNS FROM $fixedtable";
             $this->query_start($sql, null, SQL_QUERY_AUX);
             $result = $this->mysqli->query($sql);
             $this->query_end(true);
@@ -1317,8 +1319,8 @@ class mysqli_native_moodle_database extends moodle_database {
         $fields = implode(',', array_keys($params));
         $qms    = array_fill(0, count($params), '?');
         $qms    = implode(',', $qms);
-
-        $sql = "INSERT INTO {$this->prefix}$table ($fields) VALUES($qms)";
+        $fixedtable = $this->fix_table_name($table);
+        $sql = "INSERT INTO $fixedtable ($fields) VALUES($qms)";
 
         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
         $rawsql = $this->emulate_bound_params($sql, $params);
@@ -1483,7 +1485,8 @@ class mysqli_native_moodle_database extends moodle_database {
             }
         }
 
-        $sql = "INSERT INTO {$this->prefix}$table $fieldssql VALUES $valuessql";
+        $fixedtable = $this->fix_table_name($table);
+        $sql = "INSERT INTO $fixedtable $fieldssql VALUES $valuessql";
 
         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
         $rawsql = $this->emulate_bound_params($sql, $params);
@@ -1547,7 +1550,8 @@ class mysqli_native_moodle_database extends moodle_database {
         $params[] = $id; // last ? in WHERE condition
 
         $sets = implode(',', $sets);
-        $sql = "UPDATE {$this->prefix}$table SET $sets WHERE id=?";
+        $fixedtable = $this->fix_table_name($table);
+        $sql = "UPDATE $fixedtable SET $sets WHERE id=?";
 
         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
         $rawsql = $this->emulate_bound_params($sql, $params);
@@ -1621,7 +1625,8 @@ class mysqli_native_moodle_database extends moodle_database {
             $newfield = "$newfield = ?";
             array_unshift($params, $normalised_value);
         }
-        $sql = "UPDATE {$this->prefix}$table SET $newfield $select";
+        $fixedtable = $this->fix_table_name($table);
+        $sql = "UPDATE $fixedtable SET $newfield $select";
         $rawsql = $this->emulate_bound_params($sql, $params);
 
         $this->query_start($sql, $params, SQL_QUERY_UPDATE);
@@ -1644,7 +1649,8 @@ class mysqli_native_moodle_database extends moodle_database {
         if ($select) {
             $select = "WHERE $select";
         }
-        $sql = "DELETE FROM {$this->prefix}$table $select";
+        $fixedtable = $this->fix_table_name($table);
+        $sql = "DELETE FROM $fixedtable $select";
 
         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
         $rawsql = $this->emulate_bound_params($sql, $params);
@@ -2032,4 +2038,17 @@ class mysqli_native_moodle_database extends moodle_database {
         }
         return false;
     }
+
+    /**
+     * Fixes any table names that clash with reserved words.
+     *
+     * @param string $tablename The table name
+     * @return string The fixed table name
+     */
+    protected function fix_table_name($tablename) {
+        $prefixedtablename = parent::fix_table_name($tablename);
+        // This function quotes the table name if it matches one of the MySQL reserved
+        // words, e.g. groups.
+        return $this->get_manager()->generator->getEncQuoted($prefixedtablename);
+    }
 }
index 97d9490..d894b36 100644 (file)
@@ -842,8 +842,7 @@ class pgsql_native_moodle_database extends moodle_database {
      * @return array of objects, or empty array if no records were found
      * @throws dml_exception A DML specific exception is thrown for any errors.
      */
-    public function get_records_sql($sql, array $params=null, $limitfrom=0, $limitnum=0) {
-
+    public function get_records_sql($sql, array $params = null, $limitfrom = 0, $limitnum = 0) {
         list($limitfrom, $limitnum) = $this->normalise_limit_from_num($limitfrom, $limitnum);
 
         if ($limitnum) {
@@ -868,24 +867,19 @@ class pgsql_native_moodle_database extends moodle_database {
             }
         }
 
-        $rows = pg_fetch_all($result);
-        pg_free_result($result);
-
-        $return = array();
-        if ($rows) {
-            foreach ($rows as $row) {
-                $id = reset($row);
-                if ($blobs) {
-                    foreach ($blobs as $blob) {
-                        $row[$blob] = ($row[$blob] !== null ? pg_unescape_bytea($row[$blob]) : null);
-                    }
-                }
-                if (isset($return[$id])) {
-                    $colname = key($row);
-                    debugging("Did you remember to make the first column something unique in your call to get_records? Duplicate value '$id' found in column '$colname'.", DEBUG_DEVELOPER);
+        $return = [];
+        while ($row = pg_fetch_assoc($result)) {
+            $id = reset($row);
+            if ($blobs) {
+                foreach ($blobs as $blob) {
+                    $row[$blob] = ($row[$blob] !== null ? pg_unescape_bytea($row[$blob]) : null);
                 }
-                $return[$id] = (object)$row;
             }
+            if (isset($return[$id])) {
+                $colname = key($row);
+                debugging("Did you remember to make the first column something unique in your call to get_records? Duplicate value '$id' found in column '$colname'.", DEBUG_DEVELOPER);
+            }
+            $return[$id] = (object) $row;
         }
 
         return $return;
index d8cc6da..0847383 100644 (file)
@@ -22,7 +22,7 @@ Feature: Tinymce with enable/disable function.
 
   @javascript
   Scenario: Check disable Tinymce editor.
-    When I click on "option[value=1]" "css_element" in the "select#id_mycontrol" "css_element"
+    When I set the field "My control" to "Disable"
     Then the "class" attribute of "a#id_myeditor_pdw_toggle" "css_element" should contain "mceButtonDisabled"
     And the "class" attribute of "table#id_myeditor_formatselect" "css_element" should contain "mceListBoxDisabled"
     And the "class" attribute of "a#id_myeditor_bold" "css_element" should contain "mceButtonDisabled"
@@ -39,7 +39,7 @@ Feature: Tinymce with enable/disable function.
 
   @javascript
   Scenario: Check enable Tinymce editor.
-    When I click on "option[value=0]" "css_element" in the "select#id_mycontrol" "css_element"
+    When I set the field "My control" to "Enable"
     Then the "class" attribute of "a#id_myeditor_pdw_toggle" "css_element" should contain "mceButtonEnabled"
     And the "class" attribute of "table#id_myeditor_formatselect" "css_element" should contain "mceListBoxEnabled"
     And the "class" attribute of "a#id_myeditor_bold" "css_element" should contain "mceButtonEnabled"
index 6512933..c541731 100644 (file)
@@ -1044,7 +1044,8 @@ function file_save_draft_area_files($draftitemid, $contextid, $component, $filea
 
     // Do not merge files, leave it as it was.
     if ($draftitemid === IGNORE_FILE_MERGE) {
-        return null;
+        // Safely return $text, no need to rewrite pluginfile because this is mostly comming from an external client like the app.
+        return $text;
     }
 
     $usercontext = context_user::instance($USER->id);
index f9112c2..57ec021 100644 (file)
@@ -26,7 +26,7 @@
     <div tabindex="0" class="file-picker fp-generallayout row" role="dialog" aria-live="assertive">
         <div class="fp-repo-area col-md-3 pr-2 nav nav-pills flex-column" role="tablist">
                 <div class="fp-repo nav-item" role="tab" aria-selected="false" tabindex="-1">
-                    <a href="#" class="nav-link" tabindex="-1"><img class="fp-repo-icon" alt=" " src="#" width="16" height="16" />&nbsp;<span class="fp-repo-name"></span></a>
+                    <a href="#" class="nav-link" tabindex="-1"><img class="fp-repo-icon" alt=" " width="16" height="16" />&nbsp;<span class="fp-repo-name"></span></a>
                 </div>
 
         </div>
index 04c241d..5b0e2e9 100644 (file)
@@ -763,53 +763,126 @@ class behat_general extends behat_base {
     /**
      * Checks, that the first specified element appears before the second one.
      *
-     * @Given /^"(?P<preceding_element_string>(?:[^"]|\\")*)" "(?P<selector1_string>(?:[^"]|\\")*)" should appear before "(?P<following_element_string>(?:[^"]|\\")*)" "(?P<selector2_string>(?:[^"]|\\")*)"$/
+     * @Then :preelement :preselectortype should appear before :postelement :postselectortype
+     * @Then :preelement :preselectortype should appear before :postelement :postselectortype in the :containerelement :containerselectortype
      * @throws ExpectationException
      * @param string $preelement The locator of the preceding element
      * @param string $preselectortype The locator of the preceding element
      * @param string $postelement The locator of the latest element
      * @param string $postselectortype The selector type of the latest element
+     * @param string $containerelement
+     * @param string $containerselectortype
      */
-    public function should_appear_before($preelement, $preselectortype, $postelement, $postselectortype) {
-
-        // We allow postselectortype as a non-text based selector.
-        list($preselector, $prelocator) = $this->transform_selector($preselectortype, $preelement);
-        list($postselector, $postlocator) = $this->transform_selector($postselectortype, $postelement);
-
-        $prexpath = $this->find($preselector, $prelocator)->getXpath();
-        $postxpath = $this->find($postselector, $postlocator)->getXpath();
-
-        // Using following xpath axe to find it.
-        $msg = '"'.$preelement.'" "'.$preselectortype.'" does not appear before "'.$postelement.'" "'.$postselectortype.'"';
-        $xpath = $prexpath.'/following::*[contains(., '.$postxpath.')]';
-        if (!$this->getSession()->getDriver()->find($xpath)) {
-            throw new ExpectationException($msg, $this->getSession());
-        }
+    public function should_appear_before(
+        string $preelement,
+        string $preselectortype,
+        string $postelement,
+        string $postselectortype,
+        ?string $containerelement = null,
+        ?string $containerselectortype = null
+    ) {
+        $msg = "'{$preelement}' '{$preselectortype}' does not appear after '{$postelement}' '{$postselectortype}'";
+        $this->check_element_order(
+            $containerelement,
+            $containerselectortype,
+            $preelement,
+            $preselectortype,
+            $postelement,
+            $postselectortype,
+            $msg
+        );
     }
 
     /**
      * Checks, that the first specified element appears after the second one.
      *
-     * @Given /^"(?P<following_element_string>(?:[^"]|\\")*)" "(?P<selector1_string>(?:[^"]|\\")*)" should appear after "(?P<preceding_element_string>(?:[^"]|\\")*)" "(?P<selector2_string>(?:[^"]|\\")*)"$/
+     * @Then :postelement :postselectortype should appear after :preelement :preselectortype
+     * @Then :postelement :postselectortype should appear after :preelement :preselectortype in the :containerelement :containerselectortype
      * @throws ExpectationException
      * @param string $postelement The locator of the latest element
      * @param string $postselectortype The selector type of the latest element
      * @param string $preelement The locator of the preceding element
      * @param string $preselectortype The locator of the preceding element
+     * @param string $containerelement
+     * @param string $containerselectortype
      */
-    public function should_appear_after($postelement, $postselectortype, $preelement, $preselectortype) {
+    public function should_appear_after(
+        string $postelement,
+        string $postselectortype,
+        string $preelement,
+        string $preselectortype,
+        ?string $containerelement = null,
+        ?string $containerselectortype = null
+    ) {
+        $msg = "'{$postelement}' '{$postselectortype}' does not appear after '{$preelement}' '{$preselectortype}'";
+        $this->check_element_order(
+            $containerelement,
+            $containerselectortype,
+            $preelement,
+            $preselectortype,
+            $postelement,
+            $postselectortype,
+            $msg
+        );
+    }
+
+    /**
+     * Shared code to check whether an element is before or after another one.
+     *
+     * @param string $containerelement
+     * @param string $containerselectortype
+     * @param string $preelement The locator of the preceding element
+     * @param string $preselectortype The locator of the preceding element
+     * @param string $postelement The locator of the following element
+     * @param string $postselectortype The selector type of the following element
+     * @param string $msg Message to output if this fails
+     */
+    protected function check_element_order(
+        ?string $containerelement,
+        ?string $containerselectortype,
+        string $preelement,
+        string $preselectortype,
+        string $postelement,
+        string $postselectortype,
+        string $msg
+    ) {
+        $containernode = false;
+        if ($containerselectortype && $containerelement) {
+            // Get the container node.
+            $containernode = $this->get_selected_node($containerselectortype, $containerelement);
+            $msg .= " in the '{$containerelement}' '{$containerselectortype}'";
+        }
 
-        // We allow postselectortype as a non-text based selector.
-        list($postselector, $postlocator) = $this->transform_selector($postselectortype, $postelement);
         list($preselector, $prelocator) = $this->transform_selector($preselectortype, $preelement);
+        list($postselector, $postlocator) = $this->transform_selector($postselectortype, $postelement);
 
-        $postxpath = $this->find($postselector, $postlocator)->getXpath();
-        $prexpath = $this->find($preselector, $prelocator)->getXpath();
+        $newlines = [
+            "\r\n",
+            "\r",
+            "\n",
+        ];
+        $prexpath = str_replace($newlines, ' ', $this->find($preselector, $prelocator, false, $containernode)->getXpath());
+        $postxpath = str_replace($newlines, ' ', $this->find($postselector, $postlocator, false, $containernode)->getXpath());
+
+        if ($this->running_javascript()) {
+            // The xpath to do this was running really slowly on certain Chrome versions so we are using
+            // this DOM method instead.
+            $js = <<<EOF
+(function() {
+    var a = document.evaluate("{$prexpath}", document, null, XPathResult.ANY_TYPE, null).iterateNext();
+    var b = document.evaluate("{$postxpath}", document, null, XPathResult.ANY_TYPE, null).iterateNext();
+    return a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING;
+})()
+EOF;
+            $ok = $this->getSession()->getDriver()->evaluateScript($js);
+        } else {
+
+            // Using following xpath axe to find it.
+            $xpath = "{$prexpath}/following::*[contains(., {$postxpath})]";
+            $ok = $this->getSession()->getDriver()->find($xpath);
+        }
 
-        // Using preceding xpath axe to find it.
-        $msg = '"'.$postelement.'" "'.$postselectortype.'" does not appear after "'.$preelement.'" "'.$preselectortype.'"';
-        $xpath = $postxpath.'/preceding::*[contains(., '.$prexpath.')]';
-        if (!$this->getSession()->getDriver()->find($xpath)) {
+        if (!$ok) {
             throw new ExpectationException($msg, $this->getSession());
         }
     }
@@ -1142,6 +1215,7 @@ class behat_general extends behat_base {
      *
      * @throws ExpectationException
      * @Then /^I change (window|viewport) size to "(small|medium|large|\d+x\d+)"$/
+     * @Then /^I change the (window|viewport) size to "(small|medium|large|\d+x\d+)"$/
      * @param string $windowsize size of the window (small|medium|large|wxh).
      */
     public function i_change_window_size_to($windowviewport, $windowsize) {
diff --git a/lib/tests/event_context_locked_test.php b/lib/tests/event_context_locked_test.php
new file mode 100644 (file)
index 0000000..2a7a435
--- /dev/null
@@ -0,0 +1,109 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Unit tests for the context locking events.
+ *
+ * @package    core
+ * @category   test
+ * @copyright  2019 University of Nottingham
+ * @author     Neill Magill <neill.magill@nottingham.ac.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+use core\event\context_locked;
+use core\event\context_unlocked;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Unit tests for the context_locked  and context_unlocked events.
+ *
+ * @package    core
+ * @category   test
+ * @copyright  2019 University of Nottingham
+ * @author     Neill Magill <neill.magill@nottingham.ac.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_event_context_locked_testcase extends \advanced_testcase {
+    /**
+     * Locks an unlocked context and checks that a core\event\context_locked event is created.
+     *
+     * @param \context $context
+     */
+    protected function lock_context(\context $context) {
+        self::assertFalse($context->is_locked());
+
+        $locksink = $this->redirectEvents();
+        $context->set_locked(true);
+        // This second call should not create an event as the lock status has not changed.
+        $context->set_locked(true);
+        $lockevents = $locksink->get_events();
+        $locksink->close();
+
+        self::assertCount(1, $lockevents);
+        self::assertContainsOnlyInstancesOf('core\event\context_locked', $lockevents);
+        self::assertEquals($context->id, $lockevents[0]->objectid);
+        $this->assertSame('context', $lockevents[0]->objecttable);
+        $this->assertEquals($context, $lockevents[0]->get_context());
+    }
+
+    /**
+     * Tests that events are created when contexts are locked and unlocked.
+     */
+    public function test_creation() {
+        $this->resetAfterTest();
+
+        $category = self::getDataGenerator()->create_category();
+        $catcontext = \context_coursecat::instance($category->id);
+        $course = self::getDataGenerator()->create_course(['category' => $category->id]);
+        $coursecontext = \context_course::instance($course->id);
+        $activitygenerator = self::getDataGenerator()->get_plugin_generator('mod_forum');
+        $activity = $activitygenerator->create_instance(['course' => $course->id]);
+        $activitycontext = \context_module::instance($activity->cmid);
+
+        $this->lock_context($catcontext);
+        $this->unlock_context($catcontext);
+
+        $this->lock_context($coursecontext);
+        $this->unlock_context($coursecontext);
+
+        $this->lock_context($activitycontext);
+        $this->unlock_context($activitycontext);
+    }
+
+    /**
+     * Unlocks a locked context and checks that a core\event\context_unlocked event is created.
+     *
+     * @param \context $context
+     */
+    protected function unlock_context(\context $context) {
+        self::assertTrue($context->is_locked());
+
+        $unlocksink = $this->redirectEvents();
+        $context->set_locked(false);
+        // This second call should not create an event as the lock status has not changed.
+        $context->set_locked(false);
+        $unlockevents = $unlocksink->get_events();
+        $unlocksink->close();
+
+        self::assertCount(1, $unlockevents);
+        self::assertContainsOnlyInstancesOf('core\event\context_unlocked', $unlockevents);
+        self::assertEquals($context->id, $unlockevents[0]->objectid);
+        $this->assertSame('context', $unlockevents[0]->objecttable);
+        $this->assertEquals($context, $unlockevents[0]->get_context());
+    }
+}
index a1b428a..f6e0c1b 100644 (file)
@@ -869,6 +869,11 @@ class core_filelib_testcase extends advanced_testcase {
         // Save without merge.
         file_save_draft_area_files(IGNORE_FILE_MERGE, $usercontext->id, 'user', 'private', 0);
         $this->assertCount(2, $fs->get_area_files($usercontext->id, 'user', 'private'));
+        // Save again, this time including some inline text.
+        $inlinetext = 'Some text <img src="@@PLUGINFILE@@/file.png">';
+        $text = file_save_draft_area_files(IGNORE_FILE_MERGE, $usercontext->id, 'user', 'private', 0, null, $inlinetext);
+        $this->assertCount(2, $fs->get_area_files($usercontext->id, 'user', 'private'));
+        $this->assertEquals($inlinetext, $text);
     }
 
     /**
diff --git a/lib/tests/fixtures/upload_users_enrol_date_period.csv b/lib/tests/fixtures/upload_users_enrol_date_period.csv
new file mode 100644 (file)
index 0000000..a1d0c22
--- /dev/null
@@ -0,0 +1,3 @@
+username,password,firstname,lastname,email,course1,enroltimestart1,enrolperiod1
+student1,Student1#,Student,One,student1@example.com,math102,2019-01-01,
+student2,Student2#,Student,Two,student2@example.com,math102,2020-01-02,10
index 17b27ce..c02d911 100644 (file)
@@ -52,6 +52,7 @@ validation against and defaults to null (so, no user needed) if not provided.
       mod_forum/post/user: mod/forum/templates/local/post/user.mustache
 * Following behat steps have been removed from core:
     - I go to "<gradepath_string>" in the course gradebook
+* A new admin setting widget 'core_admin\local\settings\filesize' is added.
 
 === 3.7 ===
 
@@ -76,6 +77,7 @@ attribute on forms to avoid collisions in forms loaded in AJAX requests.
   and get_reduced_timeout(). These allow for timeouts to be increased by a setting in config.php.
 * The $draftitemid parameter of file_save_draft_area_files() function now supports the constant IGNORE_FILE_MERGE:
   When the parameter is set to that constant, the function won't process file merging, keeping the original state of the file area.
+  Notice also than when $text is set, pluginfile rewrite won't be processed so the text will not be modified.
 * Introduced new callback for plugin developers '<component>_pre_processor_message_send($procname, $proceventdata)':
   This will allow any plugin to manipulate messages or notifications before they are sent by a processor (email, mobile...)
 * New capability 'moodle/category:viewcourselist' in category context that controls whether user is able to browse list of courses
index 851e38d..30f0d87 100644 (file)
@@ -1983,10 +1983,15 @@ class api {
             ],
         ];
 
+        $userpicture = new \user_picture($eventdata->userfrom);
+        $userpicture->size = 1; // Use f1 size.
+        $userpicture = $userpicture->get_url($PAGE)->out(false);
+
         $conv = $DB->get_record('message_conversations', ['id' => $conversationid]);
         if ($conv->type == self::MESSAGE_CONVERSATION_TYPE_GROUP) {
             $convextrafields = self::get_linked_conversation_extra_fields([$conv]);
-            // Conversation image.
+            // Conversation images.
+            $customdata['notificationsendericonurl'] = $userpicture;
             $imageurl = isset($convextrafields[$conv->id]) ? $convextrafields[$conv->id]['imageurl'] : null;
             if ($imageurl) {
                 $customdata['notificationiconurl'] = $imageurl;
@@ -1999,8 +2004,7 @@ class api {
             }
             $customdata['conversationname'] = format_string($conv->name, true, ['context' => $convcontext]);
         } else if ($conv->type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
-            $userpicture = new \user_picture($eventdata->userfrom);
-            $customdata['notificationiconurl'] = $userpicture->get_url($PAGE)->out(false);
+            $customdata['notificationiconurl'] = $userpicture;
         }
         $eventdata->customdata = $customdata;
 
index dcc4e01..63329f4 100644 (file)
@@ -6298,7 +6298,7 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
      * Test verifying that messages can be sent to existing linked group conversations.
      */
     public function test_send_message_to_conversation_linked_group_conversation() {
-        global $CFG;
+        global $CFG, $PAGE;
 
         // Create some users.
         $user1 = self::getDataGenerator()->create_user();
@@ -6349,9 +6349,12 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         // Test customdata.
         $customdata = json_decode($messages[0]->customdata);
         $this->assertObjectHasAttribute('notificationiconurl', $customdata);
+        $this->assertObjectHasAttribute('notificationsendericonurl', $customdata);
         $this->assertEquals($groupimageurl, $customdata->notificationiconurl);
         $this->assertEquals($group->name, $customdata->conversationname);
-
+        $userpicture = new \user_picture($user1);
+        $userpicture->size = 1; // Use f1 size.
+        $this->assertEquals($userpicture->get_url($PAGE)->out(false), $customdata->notificationsendericonurl);
     }
 
     /**
diff --git a/mod/assign/amd/build/override_form.min.js b/mod/assign/amd/build/override_form.min.js
new file mode 100644 (file)
index 0000000..ccd2cf2
Binary files /dev/null and b/mod/assign/amd/build/override_form.min.js differ
diff --git a/mod/assign/amd/build/override_form.min.js.map b/mod/assign/amd/build/override_form.min.js.map
new file mode 100644 (file)
index 0000000..e979bf5
Binary files /dev/null and b/mod/assign/amd/build/override_form.min.js.map differ
diff --git a/mod/assign/amd/src/override_form.js b/mod/assign/amd/src/override_form.js
new file mode 100644 (file)
index 0000000..62a92be
--- /dev/null
@@ -0,0 +1,43 @@
+// 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/>.
+
+/**
+ * A javascript module to enhance the override form.
+ *
+ * @copyright  2019 Ryan Wyllie <ryan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import $ from 'jquery';
+
+export const init = (formId, selectElementName) => {
+    const form = document.getElementById(formId);
+    const selectElement = form.querySelector(`[name="${selectElementName}"]`);
+
+    $(selectElement).on('change', () => {
+        const inputElement = document.createElement('input');
+        inputElement.setAttribute('type', 'hidden');
+        inputElement.setAttribute('name', 'userchange');
+        inputElement.setAttribute('value', true);
+
+        form.appendChild(inputElement);
+
+        if (typeof M.core_formchangechecker !== 'undefined') {
+            M.core_formchangechecker.reset_form_dirty_state();
+        }
+
+        form.submit();
+    });
+};
\ No newline at end of file
index 6140bed..a8e7192 100644 (file)
@@ -184,6 +184,7 @@ class assign_grading_table extends table_sql implements renderable {
         }
 
         $hasoverrides = $this->assignment->has_overrides();
+        $inrelativedatesmode = $this->assignment->get_course()->relativedatesmode;
 
         if ($hasoverrides) {
             $params['assignmentid5'] = (int)$this->assignment->get_instance()->id;
@@ -202,7 +203,17 @@ class assign_grading_table extends table_sql implements renderable {
 
             $fields .= ', priority.priority, ';
             $fields .= 'effective.allowsubmissionsfromdate, ';
-            $fields .= 'effective.duedate, ';
+
+            if ($inrelativedatesmode) {
+                // If the priority is less than the 9999999 constant value it means it's an override
+                // and we should use that value directly. Otherwise we need to apply the uesr's course
+                // start date offset.
+                $fields .= 'CASE WHEN priority.priority < 9999999 THEN effective.duedate ELSE' .
+                           ' effective.duedate + enroloffset.enrolstartoffset END as duedate, ';
+            } else {
+                $fields .= 'effective.duedate, ';
+            }
+
             $fields .= 'effective.cutoffdate ';
 
             $from .= ' LEFT JOIN (
@@ -258,7 +269,7 @@ class assign_grading_table extends table_sql implements renderable {
               )
 
             ) effective ON effective.priority = priority.priority AND effective.userid = priority.userid ';
-        } else if ($this->assignment->get_course()->relativedatesmode) {
+        } else if ($inrelativedatesmode) {
             // In relative dates mode and when we don't have overrides, include the
             // duedate, cutoffdate and allowsubmissionsfrom date anyway as this information is useful and can vary.
             $params['assignmentid5'] = (int)$this->assignment->get_instance()->id;
@@ -401,7 +412,7 @@ class assign_grading_table extends table_sql implements renderable {
         $columns[] = 'status';
         $headers[] = get_string('status', 'assign');
 
-        if ($hasoverrides || $this->assignment->get_course()->relativedatesmode) {
+        if ($hasoverrides || $inrelativedatesmode) {
             // Allowsubmissionsfromdate.
             $columns[] = 'allowsubmissionsfromdate';
             $headers[] = get_string('allowsubmissionsfromdate', 'assign');
index c367b0a..92b6b8b 100644 (file)
@@ -586,6 +586,7 @@ $string['updategrade'] = 'Update grade';
 $string['updatetable'] = 'Save and update table';
 $string['upgradenotimplemented'] = 'Upgrade not implemented in plugin ({$a->type} {$a->subtype})';
 $string['userextensiondate'] = 'Extension granted until: {$a}';
+$string['userassignmentdefaults'] = 'User assignment defaults';
 $string['useridlistnotcached'] = 'The grade changes were NOT saved, as it was not possible to determine which submission they were for.';
 $string['useroverrides'] = 'User overrides';
 $string['useroverridesdeleted'] = 'User overrides deleted';
index 2fe2877..af12729 100644 (file)
@@ -901,24 +901,11 @@ function assign_print_recent_mod_activity($activity, $courseid, $detail, $modnam
 }
 
 /**
- * Checks if a scale is being used by an assignment.
- *
- * This is used by the backup code to decide whether to back up a scale
- * @param int $assignmentid
- * @param int $scaleid
- * @return boolean True if the scale is used by the assignment
+ * @deprecated since Moodle 3.8
  */
-function assign_scale_used($assignmentid, $scaleid) {
-    global $DB;
-
-    $return = false;
-    $rec = $DB->get_record('assign', array('id'=>$assignmentid, 'grade'=>-$scaleid));
-
-    if (!empty($rec) && !empty($scaleid)) {
-        $return = true;
-    }
-
-    return $return;
+function assign_scale_used() {
+    throw new coding_exception('assign_scale_used() can not be used anymore. Plugins can implement ' .
+        '<modname>_scale_used_anywhere, all implementations of <modname>_scale_used are now ignored');
 }
 
 /**
index 0929b1f..7211b0d 100644 (file)
@@ -909,7 +909,7 @@ class assign {
         $keys = array('duedate', 'cutoffdate', 'allowsubmissionsfromdate');
         foreach ($keys as $key) {
             if (isset($override->{$key})) {
-                $this->get_instance()->{$key} = $override->{$key};
+                $this->get_instance($userid)->{$key} = $override->{$key};
             }
         }
 
index f7eb984..a317285 100644 (file)
@@ -58,6 +58,9 @@ class assign_override_form extends moodleform {
     /** @var int sortorder, if provided. */
     protected $sortorder;
 
+    /** @var int selecteduserid, if provided. */
+    protected $selecteduserid;
+
     /**
      * Constructor.
      * @param moodle_url $submiturl the form action URL.
@@ -66,8 +69,9 @@ class assign_override_form extends moodleform {
      * @param object $context the assign context.
      * @param bool $groupmode editing group override (true) or user override (false).
      * @param object $override the override being edited, if it already exists.
+     * @param int $selecteduserid the user selected in the form, if any.
      */
-    public function __construct($submiturl, $cm, $assign, $context, $groupmode, $override) {
+    public function __construct($submiturl, $cm, $assign, $context, $groupmode, $override, $selecteduserid = null) {
 
         $this->cm = $cm;
         $this->assign = $assign;
@@ -76,6 +80,7 @@ class assign_override_form extends moodleform {
         $this->groupid = empty($override->groupid) ? 0 : $override->groupid;
         $this->userid = empty($override->userid) ? 0 : $override->userid;
         $this->sortorder = empty($override->sortorder) ? null : $override->sortorder;
+        $this->selecteduserid = $selecteduserid;
 
         parent::__construct($submiturl, null, 'post');
 
@@ -85,10 +90,13 @@ class assign_override_form extends moodleform {
      * Define this form - called by the parent constructor
      */
     protected function definition() {
-        global $DB;
+        global $DB, $OUTPUT, $PAGE;
 
         $cm = $this->cm;
         $mform = $this->_form;
+        $userid = $this->selecteduserid ?? $this->userid ?: null;
+        $assigninstance = $this->assign->get_instance($userid);
+        $inrelativedatesmode = !empty($this->assign->get_course()->relativedatesmode);
 
         $mform->addElement('header', 'override', get_string('override', 'assign'));
 
@@ -196,6 +204,32 @@ class assign_override_form extends moodleform {
                 $mform->addElement('searchableselector', 'userid',
                         get_string('overrideuser', 'assign'), $userchoices);
                 $mform->addRule('userid', get_string('required'), 'required', null, 'client');
+
+                if ($inrelativedatesmode) {
+                    // If in relative dates mode then add the JS to reload the page when the user
+                    // selection is changed to ensure that the correct dates are displayed.
+                    $PAGE->requires->js_call_amd('mod_assign/override_form', 'init', [
+                        $mform->getAttribute('id'),
+                        'userid'
+                    ]);
+                }
+            }
+
+            if ($inrelativedatesmode) {
+                if ($userid) {
+                    $templatecontext = [
+                        'allowsubmissionsfromdate' => $assigninstance->allowsubmissionsfromdate,
+                        'duedate' => $assigninstance->duedate,
+                        'cutoffdate' => $assigninstance->cutoffdate
+                    ];
+                    $html = $OUTPUT->render_from_template('mod_assign/override_form_user_defaults', $templatecontext);
+                } else {
+                    $html = get_string('noselection', 'form');
+                }
+
+                $groupelements = [];
+                $groupelements[] = $mform->createElement('html', $html);
+                $mform->addGroup($groupelements, null, get_string('userassignmentdefaults', 'mod_assign'), null, false);
             }
         }
 
@@ -203,7 +237,7 @@ class assign_override_form extends moodleform {
         array_push($users, $this->userid);
         $extensionmax = 0;
         foreach ($users as $value) {
-            $extension = $DB->get_record('assign_user_flags', array('assignment' => $this->assign->get_instance()->id,
+            $extension = $DB->get_record('assign_user_flags', array('assignment' => $assigninstance->id,
                 'userid' => $value));
             if ($extension) {
                 if ($extensionmax < $extension->extensionduedate) {
@@ -213,23 +247,23 @@ class assign_override_form extends moodleform {
         }
 
         if ($extensionmax) {
-            $this->assign->get_instance()->extensionduedate = $extensionmax;
+            $assigninstance->extensionduedate = $extensionmax;
         }
 
         // Open and close dates.
         $mform->addElement('date_time_selector', 'allowsubmissionsfromdate',
             get_string('allowsubmissionsfromdate', 'assign'), array('optional' => true));
-        $mform->setDefault('allowsubmissionsfromdate', $this->assign->get_instance()->allowsubmissionsfromdate);
+        $mform->setDefault('allowsubmissionsfromdate', $assigninstance->allowsubmissionsfromdate);
 
         $mform->addElement('date_time_selector', 'duedate', get_string('duedate', 'assign'), array('optional' => true));
-        $mform->setDefault('duedate', $this->assign->get_instance()->duedate);
+        $mform->setDefault('duedate', $assigninstance->duedate);
 
         $mform->addElement('date_time_selector', 'cutoffdate', get_string('cutoffdate', 'assign'), array('optional' => true));
-        $mform->setDefault('cutoffdate', $this->assign->get_instance()->cutoffdate);
+        $mform->setDefault('cutoffdate', $assigninstance->cutoffdate);
 
-        if (isset($this->assign->get_instance()->extensionduedate)) {
+        if (isset($assigninstance->extensionduedate)) {
             $mform->addElement('static', 'extensionduedate', get_string('extensionduedate', 'assign'),
-                userdate($this->assign->get_instance()->extensionduedate));
+                userdate($assigninstance->extensionduedate));
         }
 
         // Submit buttons.
@@ -259,7 +293,8 @@ class assign_override_form extends moodleform {
         $errors = parent::validation($data, $files);
 
         $mform =& $this->_form;
-        $assign = $this->assign;
+        $userid = $this->selecteduserid ?? $this->userid ?: null;
+        $assigninstance = $this->assign->get_instance($userid);
 
         if ($mform->elementExists('userid')) {
             if (empty($data['userid'])) {
@@ -293,13 +328,13 @@ class assign_override_form extends moodleform {
         }
 
         // Ensure that override duedate/allowsubmissionsfromdate are before extension date if exist.
-        if (!empty($assign->get_instance()->extensionduedate) && !empty($data['duedate'])) {
-            if ($assign->get_instance()->extensionduedate < $data['duedate']) {
+        if (!empty($assigninstance->extensionduedate) && !empty($data['duedate'])) {
+            if ($assigninstance->extensionduedate < $data['duedate']) {
                 $errors['duedate'] = get_string('extensionnotafterduedate', 'assign');
             }
         }
-        if (!empty($assign->get_instance()->extensionduedate) && !empty($data['allowsubmissionsfromdate'])) {
-            if ($assign->get_instance()->extensionduedate < $data['allowsubmissionsfromdate']) {
+        if (!empty($assigninstance->extensionduedate) && !empty($data['allowsubmissionsfromdate'])) {
+            if ($assigninstance->extensionduedate < $data['allowsubmissionsfromdate']) {
                 $errors['allowsubmissionsfromdate'] = get_string('extensionnotafterfromdate', 'assign');
             }
         }
@@ -308,7 +343,7 @@ class assign_override_form extends moodleform {
         $changed = false;
         $keys = array('duedate', 'cutoffdate', 'allowsubmissionsfromdate');
         foreach ($keys as $key) {
-            if ($data[$key] != $assign->get_instance()->{$key}) {
+            if ($data[$key] != $assigninstance->{$key}) {
                 $changed = true;
                 break;
             }
index 9928e8b..1a39c09 100644 (file)
@@ -33,6 +33,8 @@ $cmid = optional_param('cmid', 0, PARAM_INT);
 $overrideid = optional_param('id', 0, PARAM_INT);
 $action = optional_param('action', null, PARAM_ALPHA);
 $reset = optional_param('reset', false, PARAM_BOOL);
+$userid = optional_param('userid', null, PARAM_INT);
+$userchange = optional_param('userchange', false, PARAM_BOOL);
 
 $pagetitle = get_string('editoverride', 'assign');
 
@@ -68,7 +70,9 @@ require_login($course, false, $cm);
 
 $context = context_module::instance($cm->id);
 $assign = new assign($context, $cm, $course);
-$assigninstance = $assign->get_instance();
+$assigninstance = $assign->get_instance($userid);
+$shouldadduserid = $userid && !empty($course->relativedatesmode);
+$shouldresetform = optional_param('resetbutton', 0, PARAM_ALPHA) || ($userchange && $action !== 'duplicate');
 
 // Add or edit an override.
 require_capability('mod/assign:manageoverrides', $context);
@@ -111,23 +115,30 @@ if ($action === 'duplicate') {
     $pagetitle = get_string('duplicateoverride', 'assign');
 }
 
+if ($shouldadduserid) {
+    $data->userid = $userid;
+}
+
 $overridelisturl = new moodle_url('/mod/assign/overrides.php', array('cmid' => $cm->id));
 if (!$groupmode) {
     $overridelisturl->param('mode', 'user');
 }
 
 // Setup the form.
-$mform = new assign_override_form($url, $cm, $assign, $context, $groupmode, $override);
+$mform = new assign_override_form($url, $cm, $assign, $context, $groupmode, $override, $userid);
 $mform->set_data($data);
 
 if ($mform->is_cancelled()) {
     redirect($overridelisturl);
 
-} else if (optional_param('resetbutton', 0, PARAM_ALPHA)) {
+} else if ($shouldresetform) {
     $url->param('reset', true);
+    if ($shouldadduserid) {
+        $url->param('userid', $userid);
+    }
     redirect($url);
 
-} else if ($fromform = $mform->get_data()) {
+} else if (!$userchange && $fromform = $mform->get_data()) {
     // Process the data.
     $fromform->assignid = $assigninstance->id;
 
index 4b6aba9..0c5d724 100644 (file)
         width: auto;
         display: inline-block;
         margin: 0 auto;
+        height: auto;
     }
 
     .path-mod-assign [data-region="user-selector"] .alignment {
index a4153dd..7ad838a 100644 (file)
@@ -314,12 +314,18 @@ class assign_submission_file extends assign_submission_plugin {
      * @return boolean
      */
     public function remove(stdClass $submission) {
+        global $DB;
         $fs = get_file_storage();
 
         $fs->delete_area_files($this->assignment->get_context()->id,
                                'assignsubmission_file',
                                ASSIGNSUBMISSION_FILE_FILEAREA,
                                $submission->id);
+
+        $currentsubmission = $this->get_file_submission($submission->id);
+        $currentsubmission->numfiles = 0;
+        $DB->update_record('assignsubmission_file', $currentsubmission);
+
         return true;
     }
 
diff --git a/mod/assign/templates/override_form_user_defaults.mustache b/mod/assign/templates/override_form_user_defaults.mustache
new file mode 100644 (file)
index 0000000..3cff732
--- /dev/null
@@ -0,0 +1,40 @@
+{{!
+    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/>.