Merge branch 'MDL-66336_master' of https://github.com/marxjohnson/moodle
authorJun Pataleta <jun@moodle.com>
Wed, 21 Aug 2019 14:41:41 +0000 (22:41 +0800)
committerJun Pataleta <jun@moodle.com>
Wed, 21 Aug 2019 14:41:41 +0000 (22:41 +0800)
99 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
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
blog/rsslib.php
calendar/renderer.php
composer.lock
course/classes/analytics/indicator/activities_due.php
course/templates/activity_navigation.mustache
course/tests/indicators_test.php
grade/grading/form/rubric/tests/behat/grade_calculation.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
lang/en/access.php
lang/en/admin.php
lang/en/deprecated.txt
lib/accesslib.php
lib/adminlib.php
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/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/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/fixtures/upload_users_enrol_date_period.csv [new file with mode: 0644]
lib/upgrade.txt
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/upgrade.txt
mod/data/locallib.php
mod/feedback/lib.php
mod/feedback/upgrade.txt
mod/forum/deprecatedlib.php
mod/forum/lib.php
mod/forum/upgrade.txt
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

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 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 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 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 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 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 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 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 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 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 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..f1a9590 100644 (file)
@@ -2032,4 +2032,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 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());
+    }
+}
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 ===
 
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 8850123..aad0975 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/>.
+}}
+{{!
+    @template mod_assign/override_form_user_defaults
+
+    HTML for showing user default dates in override form.
+}}
+<dl>
+    <dt>{{#str}} allowsubmissionsfromdate, mod_assign {{/str}}</dt>
+    <dl>
+        {{#allowsubmissionsfromdate}} {{#userdate}} {{.}}, {{#str}} strftimedatetime, core_langconfig {{/str}} {{/userdate}} {{/allowsubmissionsfromdate}}
+        {{^allowsubmissionsfromdate}} {{#str}} disabled, mod_assign {{/str}} {{/allowsubmissionsfromdate}}
+    </dl>
+
+    <dt>{{#str}} duedate, mod_assign {{/str}}</dt>
+    <dl>
+        {{#duedate}} {{#userdate}} {{.}}, {{#str}} strftimedatetime, core_langconfig {{/str}} {{/userdate}} {{/duedate}}
+        {{^duedate}} {{#str}} disabled, mod_assign {{/str}} {{/duedate}}
+    </dl>
+
+    <dt>{{#str}} cutoffdate, mod_assign {{/str}}</dt>
+    <dl>
+        {{#cutoffdate}} {{#userdate}} {{cutoffdate}}, {{#str}} strftimedatetime, core_langconfig {{/str}} {{/userdate}} {{/cutoffdate}}
+        {{^cutoffdate}} {{#str}} disabled, mod_assign {{/str}} {{/cutoffdate}}
+    </dl>
+</dl>
index f0c9e8c..5268533 100644 (file)
@@ -1,6 +1,8 @@
 This files describes API changes in the assign code.
 === 3.8 ===
 * Webservice function mod_assign_get_submission_status, return value 'warnofungroupedusers', changed from PARAM_BOOL to PARAM_ALPHA. See the description for possible values.
+* The following functions have been finally deprecated and can not be used anymore:
+    * assign_scale_used()
 
 === 3.7 ===
 * Submissions plugins should implement the "remove" function to remove data when "Remove submission" is used.
index 0b4887f..e28942d 100644 (file)
@@ -215,17 +215,11 @@ function book_grades($bookid) {
 }
 
 /**
- * This function returns if a scale is being used by one book
- * it it has support for grading and scales. Commented code should be
- * modified if necessary. See book, glossary or journal modules
- * as reference.
- *
- * @param int $bookid
- * @param int $scaleid
- * @return boolean True if the scale is used by any journal
+ * @deprecated since Moodle 3.8
  */
-function book_scale_used($bookid, $scaleid) {
-    return false;
+function book_scale_used() {
+    throw new coding_exception('book_scale_used() can not be used anymore. Plugins can implement ' .
+        '<modname>_scale_used_anywhere, all implementations of <modname>_scale_used are now ignored');
 }
 
 /**
index 1fb9028..f58e5dc 100644 (file)
@@ -1,5 +1,10 @@
 This files describes API changes in the book code.
 
+=== 3.8 ===
+
+* The following functions have been finally deprecated and can not be used anymore:
+    * book_scale_used()
+
 === 3.7 ===
 
 * book_export_contents() callback now returns tags information for every chapter.
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 8595969..124bc3c 100644 (file)
@@ -586,10 +586,11 @@ function feedback_cron () {
 }
 
 /**
- * @return bool false
+ * @deprecated since Moodle 3.8
  */
-function feedback_scale_used ($feedbackid, $scaleid) {
-    return false;
+function feedback_scale_used() {
+    throw new coding_exception('feedback_scale_used() can not be used anymore. Plugins can implement ' .
+        '<modname>_scale_used_anywhere, all implementations of <modname>_scale_used are now ignored');
 }
 
 /**
index e084e60..c9a0ef6 100644 (file)
@@ -1,3 +1,8 @@
+=== 3.8 ===
+
+* The following functions have been finally deprecated and can not be used anymore:
+    * feedback_scale_used()
+
 === 3.6 ===
 
 * The following functions have been finally deprecated and can not be used anymore:
index f884344..8e71f22 100644 (file)
@@ -1651,3 +1651,11 @@ function forum_count_replies($post, $children = true) {
             true
         );
 }
+
+/**
+ * @deprecated since Moodle 3.8
+ */
+function forum_scale_used() {
+    throw new coding_exception('forum_scale_used() can not be used anymore. Plugins can implement ' .
+        '<modname>_scale_used_anywhere, all implementations of <modname>_scale_used are now ignored');
+}
index 8d1b4e1..62f2423 100644 (file)
@@ -804,28 +804,6 @@ function forum_grade_item_delete($forum) {
     return grade_update('mod/forum', $forum->course, 'mod', 'forum', $forum->id, 0, NULL, array('deleted'=>1));
 }
 
-
-/**
- * This function returns if a scale is being used by one forum
- *
- * @global object
- * @param int $forumid
- * @param int $scaleid negative number
- * @return bool
- */
-function forum_scale_used ($forumid,$scaleid) {
-    global $DB;
-    $return = false;
-
-    $rec = $DB->get_record("forum",array("id" => "$forumid","scale" => "-$scaleid"));
-
-    if (!empty($rec) && !empty($scaleid)) {
-        $return = true;
-    }
-
-    return $return;
-}
-
 /**
  * Checks if scale is being used by any instance of forum
  *
index 6f5c9b9..e64b5fb 100644 (file)
@@ -1,6 +1,11 @@
 This files describes API changes in /mod/forum/*,
 information provided here is intended especially for developers.
 
+=== 3.8 ===
+
+* The following functions have been finally deprecated and can not be used anymore:
+    * forum_scale_used()
+
 === 3.7 ===
   * Changed the forum discussion rendering to use templates rather than print functions.
   * Added new forum entities, factories, exporters, renderers, and vaults in the local namespace to better encapsulate the forum data.
index c0b44d1..6de89ca 100644 (file)
@@ -865,24 +865,11 @@ function glossary_grade_item_delete($glossary) {
 }
 
 /**
- * @global object
- * @param int $gloassryid
- * @param int $scaleid
- * @return bool
+ * @deprecated since Moodle 3.8
  */
-function glossary_scale_used ($glossaryid,$scaleid) {
-//This function returns if a scale is being used by one glossary
-    global $DB;
-
-    $return = false;
-
-    $rec = $DB->get_record("glossary", array("id"=>$glossaryid, "scale"=>-$scaleid));
-
-    if (!empty($rec)  && !empty($scaleid)) {
-        $return = true;
-    }
-
-    return $return;
+function glossary_scale_used() {
+    throw new coding_exception('glossary_scale_used() can not be used anymore. Plugins can implement ' .
+        '<modname>_scale_used_anywhere, all implementations of <modname>_scale_used are now ignored');
 }
 
 /**
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 2ce1356..7738fe5 100644 (file)
@@ -1,6 +1,10 @@
 This files describes API changes in /mod/glossary/*,
 information provided here is intended especially for developers.
 
+=== 3.8 ===
+* The following functions have been finally deprecated and can not be used anymore:
+    * glossary_scale_used()
+
 === 3.7 ===
 * External functions get_entries_by_* and get_entry now return an additional field "tags" containing the entry tags.
 
index 9553ae4..0d98e57 100644 (file)
@@ -363,26 +363,11 @@ function lti_grades($basicltiid) {
 }
 
 /**
- * This function returns if a scale is being used by one basiclti
- * it it has support for grading and scales. Commented code should be
- * modified if necessary. See forum, glossary or journal modules
- * as reference.
- *
- * @param int $basicltiid ID of an instance of this module
- * @return mixed
- *
- * @TODO: implement this moodle function (if needed)
- **/
-function lti_scale_used ($basicltiid, $scaleid) {
-    $return = false;
-
-    // $rec = get_record("basiclti","id","$basicltiid","scale","-$scaleid");
-    //
-    // if (!empty($rec)  && !empty($scaleid)) {
-    //     $return = true;
-    // }
-
-    return $return;
+ * @deprecated since Moodle 3.8
+ */
+function lti_scale_used() {
+    throw new coding_exception('lti_scale_used() can not be used anymore. Plugins can implement ' .
+        '<modname>_scale_used_anywhere, all implementations of <modname>_scale_used are now ignored');
 }
 
 /**
index 707cbc9..4e8e5b9 100644 (file)
@@ -1,5 +1,10 @@
 This files describes API changes in the lti code.
 
+=== 3.8 ===
+
+* The following functions have been finally deprecated and can not be used anymore:
+    * lti_scale_used()
+
 === 3.7 ===
 
 * Deprecated functions to add support for LTI 1 tools to access services:
index 0d0738a..3a07fa5 100644 (file)
@@ -366,25 +366,11 @@ function wiki_grades($wikiid) {
 }
 
 /**
- * This function returns if a scale is being used by one wiki
- * it it has support for grading and scales. Commented code should be
- * modified if necessary. See forum, glossary or journal modules
- * as reference.
- *
- * @param int $wikiid ID of an instance of this module
- * @return mixed
- * @todo Finish documenting this function
- **/
-function wiki_scale_used($wikiid, $scaleid) {
-    $return = false;
-
-    //$rec = get_record("wiki","id","$wikiid","scale","-$scaleid");
-    //
-    //if (!empty($rec)  && !empty($scaleid)) {
-    //    $return = true;
-    //}
-
-    return $return;
+ * @deprecated since Moodle 3.8
+ */
+function wiki_scale_used() {
+    throw new coding_exception('wiki_scale_used() can not be used anymore. Plugins can implement ' .
+        '<modname>_scale_used_anywhere, all implementations of <modname>_scale_used are now ignored');
 }
 
 /**
index bc1bfd9..6f643bf 100644 (file)
@@ -1,6 +1,10 @@
 This files describes API changes in /mod/wiki/*,
 information provided here is intended especially for developers.
 
+=== 3.8 ===
+* The following functions have been finally deprecated and can not be used anymore:
+    * wiki_scale_used()
+
 === 3.7 ===
 * External functions get_subwiki_pages and get_page_contents now return an additional field "tags" returning the entry tags.
 
index 866a3d1..cfe4922 100644 (file)
@@ -1057,37 +1057,11 @@ function workshop_print_recent_mod_activity($activity, $courseid, $detail, $modn
 }
 
 /**
- * Is a given scale used by the instance of workshop?
- *
- * The function asks all installed grading strategy subplugins. The workshop
- * core itself does not use scales. Both grade for submission and grade for
- * assessments do not use scales.
- *
- * @param int $workshopid id of workshop instance
- * @param int $scaleid id of the scale to check
- * @return bool
+ * @deprecated since Moodle 3.8
  */
-function workshop_scale_used($workshopid, $scaleid) {
-    global $CFG; // other files included from here
-
-    $strategies = core_component::get_plugin_list('workshopform');
-    foreach ($strategies as $strategy => $strategypath) {
-        $strategylib = $strategypath . '/lib.php';
-        if (is_readable($strategylib)) {
-            require_once($strategylib);
-        } else {
-            throw new coding_exception('the grading forms subplugin must contain library ' . $strategylib);
-        }
-        $classname = 'workshop_' . $strategy . '_strategy';
-        if (method_exists($classname, 'scale_used')) {
-            if (call_user_func_array(array($classname, 'scale_used'), array($scaleid, $workshopid))) {
-                // no need to include any other files - scale is used
-                return true;
-            }
-        }
-    }
-
-    return false;
+function workshop_scale_used() {
+    throw new coding_exception('workshop_scale_used() can not be used anymore. Plugins can implement ' .
+        '<modname>_scale_used_anywhere, all implementations of <modname>_scale_used are now ignored');
 }
 
 /**
index 06bd0e4..a2d7342 100644 (file)
@@ -1,6 +1,11 @@
 This files describes API changes in /mod/workshop - activity modules,
 information provided here is intended especially for developers.
 
+=== 3.8 ===
+
+* The following functions have been finally deprecated and can not be used anymore:
+    * workshop_scale_used()
+
 === 3.7 ===
 
 * workshop_cron() has been removed. Sub-plugins should now implement scheduled tasks.
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)
+        ]);
     }
 
     /**