Merge branch 'MDL-64437-master' of git://github.com/mihailges/moodle
authorJun Pataleta <jun@moodle.com>
Tue, 20 Aug 2019 06:19:34 +0000 (14:19 +0800)
committerJun Pataleta <jun@moodle.com>
Tue, 20 Aug 2019 06:19:34 +0000 (14:19 +0800)
55 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/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/uploaduser/index.php
admin/tool/uploaduser/locallib.php
admin/tool/uploaduser/tests/behat/upload_users.feature
blog/rsslib.php
calendar/renderer.php
composer.lock
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
lang/en/admin.php
lang/en/deprecated.txt
lib/adminlib.php
lib/behat/behat_base.php
lib/behat/classes/partial_named_selector.php
lib/dml/pgsql_native_moodle_database.php
lib/editor/tinymce/tests/behat/disablecontrol.feature
lib/templates/filemanager_modal_generallayout.mustache
lib/tests/behat/behat_general.php
lib/tests/fixtures/upload_users_enrol_date_period.csv [new file with mode: 0644]
lib/upgrade.txt
mod/assign/submission/file/locallib.php
mod/data/locallib.php
mod/glossary/tests/behat/categories.feature
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

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 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 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 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 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 fef1a71..9f51b08 100644 (file)
             "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",
         },
         {
             "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",
         },
         {
             "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",
         },
         {
             "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",
         },
         {
             "name": "symfony/browser-kit",
-            "version": "v4.3.2",
+            "version": "v4.3.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/browser-kit.git",
         },
         {
             "name": "symfony/class-loader",
-            "version": "v3.4.29",
+            "version": "v3.4.30",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/class-loader.git",
         },
         {
             "name": "symfony/config",
-            "version": "v4.3.2",
+            "version": "v4.3.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/config.git",
-                "reference": "9198eea354be75794a7b1064de00d9ae9ae5090f"
+                "reference": "a17a2aea43950ce83a0603ed301bac362eb86870"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/config/zipball/9198eea354be75794a7b1064de00d9ae9ae5090f",
-                "reference": "9198eea354be75794a7b1064de00d9ae9ae5090f",
+                "url": "https://api.github.com/repos/symfony/config/zipball/a17a2aea43950ce83a0603ed301bac362eb86870",
+                "reference": "a17a2aea43950ce83a0603ed301bac362eb86870",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Config Component",
             "homepage": "https://symfony.com",
-            "time": "2019-06-08T06:33:08+00:00"
+            "time": "2019-07-18T10:34:59+00:00"
         },
         {
             "name": "symfony/console",
         },
         {
             "name": "symfony/css-selector",
-            "version": "v3.4.29",
+            "version": "v3.4.30",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/css-selector.git",
                 "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"
         },
         {
             "name": "symfony/debug",
-            "version": "v3.4.29",
+            "version": "v3.4.30",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/debug.git",
-                "reference": "1172dc1abe44dfadd162239153818b074e6e53bf"
+                "reference": "bc977cb2681d75988ab2d53d14c4245c6c04f82f"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/debug/zipball/1172dc1abe44dfadd162239153818b074e6e53bf",
-                "reference": "1172dc1abe44dfadd162239153818b074e6e53bf",
+                "url": "https://api.github.com/repos/symfony/debug/zipball/bc977cb2681d75988ab2d53d14c4245c6c04f82f",
+                "reference": "bc977cb2681d75988ab2d53d14c4245c6c04f82f",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Debug Component",
             "homepage": "https://symfony.com",
-            "time": "2019-06-18T21:26:03+00:00"
+            "time": "2019-07-23T08:39:19+00:00"
         },
         {
             "name": "symfony/dependency-injection",
         },
         {
             "name": "symfony/dom-crawler",
-            "version": "v4.3.2",
+            "version": "v4.3.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/dom-crawler.git",
         },
         {
             "name": "symfony/event-dispatcher",
-            "version": "v3.4.29",
+            "version": "v3.4.30",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/event-dispatcher.git",
         },
         {
             "name": "symfony/filesystem",
-            "version": "v4.3.2",
+            "version": "v4.3.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/filesystem.git",
         },
         {
             "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",
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 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 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 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 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
     );
 
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 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..1c37206 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).iterateNext();
+    var b = document.evaluate("{$postxpath}", document).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());
         }
     }
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..19185a5 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 ===
 
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;
     }
 
index db8a779..b8bad67 100644 (file)
@@ -1421,6 +1421,7 @@ function data_approve_entry($entryid, $approve) {
 
 /**
  * Populate the field contents of a new record with the submitted data.
+ * An event has been previously triggered upon the creation of the new record in data_add_record().
  *
  * @param  stdClass $data           database object
  * @param  stdClass $context        context object
@@ -1449,18 +1450,6 @@ function data_add_fields_contents_to_new_record($data, $context, $recordid, $fie
     foreach ($processeddata->fields as $fieldname => $field) {
         $field->update_content($recordid, $datarecord->$fieldname, $fieldname);
     }
-
-    // Trigger an event for updating this record.
-    $event = \mod_data\event\record_created::create(array(
-        'objectid' => $recordid,
-        'context' => $context,
-        'courseid' => $data->course,
-        'other' => array(
-            'dataid' => $data->id
-        )
-    ));
-    $event->add_record_snapshot('data', $data);
-    $event->trigger();
 }
 
 /**
index 193488d..2154e7b 100644 (file)
@@ -80,12 +80,12 @@ Feature: Glossary entries can be organised in categories
     And I follow "Browse by category"
     And "//h3[contains(.,'CATEGORYAUTOLINKS')]" "xpath_element" should appear before "//h3[contains(.,'CATEGORYNOLINKS')]" "xpath_element"
     And "//h4[contains(.,'EntryCategoryAL')]" "xpath_element" should appear before "//h3[contains(.,'CATEGORYNOLINKS')]" "xpath_element"
-    And "//h4[contains(.,'EntryCategoryBoth')]" "xpath_element" should appear before "//h3[contains(.,'CATEGORYNOLINKS')]" "xpath_element"
-    And "//h3[contains(.,'CATEGORYNOLINKS')]" "xpath_element" should appear before "//h4[contains(.,'EntryCategoryBoth')]" "xpath_element"
+    And "(//h4[contains(.,'EntryCategoryBoth')])[1]" "xpath_element" should appear before "//h3[contains(.,'CATEGORYNOLINKS')]" "xpath_element"
+    And "//h3[contains(.,'CATEGORYNOLINKS')]" "xpath_element" should appear before "(//h4[contains(.,'EntryCategoryBoth')])[2]" "xpath_element"
     And "//h4[contains(.,'EntryCategoryNL')]" "xpath_element" should appear after "//h3[contains(.,'CATEGORYNOLINKS')]" "xpath_element"
     And I should not see "EntryNoCategory"
     And I set the field "hook" to "Not categorised"
-    And I click on "Not categorised" "option" in the "#catmenu select" "css_element"
+    And I set the field "Categories" to "Not categorised"
     And I should see "EntryNoCategory"
     And I should not see "EntryCategoryNL"
     And I should not see "EntryCategoryAL"
@@ -112,8 +112,7 @@ Feature: Glossary entries can be organised in categories
     And I should not see "EntryNoCategory"
     And I should not see "EntryCategoryAL"
     And I should see "EntryCategoryBoth"
-    And I click on "Not categorised" "option" in the "#catmenu select" "css_element"
+    And I set the field "Categories" to "Not categorised"
     And I should see "EntryNoCategory"
     And I should see "EntryCategoryAL"
     And I should not see "EntryCategoryBoth"
-    And I log out
index 4d09630..5d10eb8 100644 (file)
@@ -102,7 +102,7 @@ class core_question_generator extends component_generator_base {
      * @param array|stdClass $overrides any fields that should be different from the base example.
      */
     public function update_question($question, $which = null, $overrides = null) {
-        global $CFG;
+        global $CFG, $DB;
         require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
 
         $qtype = $question->qtype;
@@ -113,7 +113,16 @@ class core_question_generator extends component_generator_base {
         $fromform = (object) $this->datagenerator->combine_defaults_and_record(
                 (array) $fromform, $overrides);
 
-        return question_bank::get_qtype($qtype)->save_question($question, $fromform);
+        $question = question_bank::get_qtype($qtype)->save_question($question, $fromform);
+
+        if ($overrides && array_key_exists('createdby', $overrides)) {
+            // Manually update the createdby because questiontypebase forces current user and some tests require a
+            // specific user.
+            $question->createdby = $overrides['createdby'];
+            $DB->update_record('question', $question);
+        }
+
+        return $question;
     }
 
     /**
index 2688e3b..4101947 100644 (file)
@@ -367,7 +367,7 @@ if (!empty($instanceid) && !empty($roleid)) {
         }
         echo '</div>';
         echo '<div class="p-y-1">';
-        echo html_writer::label(get_string('withselectedusers'), 'formactionselect');
+        echo html_writer::label(get_string('withselectedusers'), 'formactionid');
         $displaylist['#messageselect'] = get_string('messageselectadd');
         $withselectedparams = array(
             'id' => 'formactionid',
index a14beda..e5c1093 100644 (file)
@@ -34,10 +34,10 @@ Feature: Select users when searching for user-created content
       | activity | PAGE1    |
     And I search for "frogs" using the header global search box
     And I expand all fieldsets
-    And I set the field with xpath "//select[@id='id_userids']/..//input[@type='text']" to "Anne"
+    When I expand the "Users" autocomplete
     # Alphabetical surname order.
-    Then "Anne Additional" "text" should appear before "Anne Ditin" "text"
-    And "Anne Ditin" "text" should appear before "Anne Other" "text"
+    Then "Anne Additional" "text" should appear before "Anne Ditin" "text" in the "Users" "autocomplete"
+    And "Anne Ditin" "text" should appear before "Anne Other" "text" in the "Users" "autocomplete"
 
   @javascript
   Scenario: As administrator, search for users within course
@@ -49,9 +49,9 @@ Feature: Select users when searching for user-created content
     And I search for "frogs" using the header global search box
     And I expand all fieldsets
     And I select "Course: Frogs" from the "Search within" singleselect
-    And I set the field with xpath "//select[@id='id_userids']/..//input[@type='text']" to "Anne"
+    When I expand the "Users" autocomplete
     # Users in selected course appear first.
-    And "Anne Additional" "text" should appear after "Anne Other" "text"
+    Then "Anne Additional" "text" should appear after "Anne Other" "text" in the "Users" "autocomplete"
 
   @javascript
   Scenario: As student, cannot see users on other courses
@@ -62,6 +62,6 @@ Feature: Select users when searching for user-created content
       | activity | PAGE1    |
     And I search for "frogs" using the header global search box
     And I expand all fieldsets
-    And I set the field with xpath "//select[@id='id_userids']/..//input[@type='text']" to "A"
-    Then "Anne Ditin" "text" should appear before "Anne Other" "text"
+    When I expand the "Users" autocomplete
+    Then "Anne Ditin" "text" should appear before "Anne Other" "text" in the "Users" "autocomplete"
     And "Anne Additional" "text" should not exist
index e0e162e..2474622 100644 (file)
@@ -547,10 +547,6 @@ $popout-header-height: 4rem;
     margin: 0;
 }
 
-.path-mod-assign [data-region="grade-panel"] .fitem > .col-md-3 > .pull-xs-right {
-    float: none !important; /* stylelint-disable-line declaration-no-important */
-}
-
 .path-mod-assign [data-region="grade-panel"] .mform .fitem.has-popout .felement {
     width: 100%;
 }
index fd1c6ba..4e8c4d3 100644 (file)
@@ -16339,10 +16339,6 @@ div#dock {
 .path-mod-assign [data-region="grade-panel"] .fitem.row {
   margin: 0; }
 
-.path-mod-assign [data-region="grade-panel"] .fitem > .col-md-3 > .pull-xs-right {
-  float: none !important;
-  /* stylelint-disable-line declaration-no-important */ }
-
 .path-mod-assign [data-region="grade-panel"] .mform .fitem.has-popout .felement {
   width: 100%; }
 
index b061746..f3c92ab 100644 (file)
@@ -16611,10 +16611,6 @@ div#dock {
 .path-mod-assign [data-region="grade-panel"] .fitem.row {
   margin: 0; }
 
-.path-mod-assign [data-region="grade-panel"] .fitem > .col-md-3 > .pull-xs-right {
-  float: none !important;
-  /* stylelint-disable-line declaration-no-important */ }
-
 .path-mod-assign [data-region="grade-panel"] .mform .fitem.has-popout .felement {
   width: 100%; }
 
index 8587ab9..81ef76e 100644 (file)
@@ -66,11 +66,18 @@ class user_filter_date extends user_filter_type {
     public function setupForm(&$mform) {
         $objs = array();
 
-        $objs[] = $mform->createElement('static', $this->_name.'_sck', null, get_string('isafter', 'filters'));
+        $objs[] = $mform->createElement('static', $this->_name.'_s1', null,
+            html_writer::start_tag('div', array('class' => 'w-100 d-flex align-items-center')));
+        $objs[] = $mform->createElement('static', $this->_name.'_s2', null,
+            html_writer::tag('div', get_string('isafter', 'filters'), array('class' => 'mr-2')));
         $objs[] = $mform->createElement('date_selector', $this->_name.'_sdt', null, array('optional' => true));
-        $objs[] = $mform->createElement('static', $this->_name.'_break', null, '<br/>');
-        $objs[] = $mform->createElement('static', $this->_name.'_edk', null, get_string('isbefore', 'filters'));
+        $objs[] = $mform->createElement('static', $this->_name.'_s3', null, html_writer::end_tag('div'));
+        $objs[] = $mform->createElement('static', $this->_name.'_s4', null,
+            html_writer::start_tag('div', array('class' => 'w-100 d-flex align-items-center')));
+        $objs[] = $mform->createElement('static', $this->_name.'_s5', null,
+            html_writer::tag('div', get_string('isbefore', 'filters'), array('class' => 'mr-2')));
         $objs[] = $mform->createElement('date_selector', $this->_name.'_edt', null, array('optional' => true));
+        $objs[] = $mform->createElement('static', $this->_name.'_s6', null, html_writer::end_tag('div'));
 
         $grp =& $mform->addElement('group', $this->_name.'_grp', $this->_label, $objs, '', false);
 
index 304d13a..6821dd4 100644 (file)
@@ -367,9 +367,8 @@ echo '</div>';
 
 if ($newcourse == 1) {
     $str = get_string('proceedtocourse', 'enrol');
-    // Floated left so it goes under the enrol users button on mobile.
     // The margin is to make it line up with the enrol users button when they are both on the same line.
-    $classes = 'my-1 pull-xs-left';
+    $classes = 'my-1';
     $url = course_get_url($course);
     echo $OUTPUT->single_button($url, $str, 'GET', array('class' => $classes));
 }
index a3f4bba..eff2a76 100644 (file)
@@ -46,14 +46,10 @@ class behat_user extends behat_base {
      * @param string $nodetext The menu item to select.
      */
     public function i_choose_from_the_participants_page_bulk_action_menu($nodetext) {
-        $nodetext = behat_context_helper::escape($nodetext);
-
-        // Open the select.
-        $this->execute("behat_general::i_click_on", array("//select[@id='formactionid']", "xpath_element"));
-
-        // Click on the option.
-        $this->execute("behat_general::i_click_on", array("//select[@id='formactionid']" .
-                                                          "/option[contains(., " . $nodetext . ")]", "xpath_element"));
+        $this->execute("behat_forms::i_set_the_field_to", [
+            "With selected users...",
+            $this->escape($nodetext)
+        ]);
     }
 
     /**