Merge branch 'MDL-64587-master' of https://github.com/junpataleta/moodle
authorDavid Monllaó <davidm@moodle.com>
Mon, 18 Feb 2019 09:25:35 +0000 (10:25 +0100)
committerDavid Monllaó <davidm@moodle.com>
Mon, 18 Feb 2019 09:25:35 +0000 (10:25 +0100)
46 files changed:
admin/tool/langimport/classes/locale.php [new file with mode: 0644]
admin/tool/langimport/index.php
admin/tool/langimport/lang/en/tool_langimport.php
admin/tool/langimport/tests/locale_test.php [new file with mode: 0644]
lib/amd/build/checkbox-toggleall.min.js [new file with mode: 0644]
lib/amd/src/checkbox-toggleall.js [new file with mode: 0644]
lib/form/amd/build/showadvanced.min.js [new file with mode: 0644]
lib/form/amd/src/showadvanced.js [new file with mode: 0644]
lib/form/yui/build/moodle-form-showadvanced/moodle-form-showadvanced-debug.js [deleted file]
lib/form/yui/build/moodle-form-showadvanced/moodle-form-showadvanced-min.js [deleted file]
lib/form/yui/build/moodle-form-showadvanced/moodle-form-showadvanced.js [deleted file]
lib/form/yui/src/showadvanced/build.json [deleted file]
lib/form/yui/src/showadvanced/js/showadvanced.js [deleted file]
lib/form/yui/src/showadvanced/meta/showadvanced.json [deleted file]
lib/formslib.php
lib/moodlelib.php
lib/navigationlib.php
lib/upgrade.txt
message/amd/build/notification_processor_settings.min.js
message/amd/build/preferences_notifications_list_controller.min.js
message/amd/build/preferences_processor_form.min.js
message/amd/src/notification_processor_settings.js
message/amd/src/preferences_notifications_list_controller.js
message/amd/src/preferences_processor_form.js
message/templates/preferences_processor.mustache
question/amd/build/qbankmanager.min.js [new file with mode: 0644]
question/amd/src/qbankmanager.js [new file with mode: 0644]
question/classes/bank/checkbox_column.php
question/classes/bank/view.php
question/tests/behat/select_questions.feature [new file with mode: 0644]
question/yui/build/moodle-question-qbankmanager/moodle-question-qbankmanager-debug.js [deleted file]
question/yui/build/moodle-question-qbankmanager/moodle-question-qbankmanager-min.js [deleted file]
question/yui/build/moodle-question-qbankmanager/moodle-question-qbankmanager.js [deleted file]
question/yui/src/qbankmanager/build.json [deleted file]
question/yui/src/qbankmanager/js/qbankmanager.js [deleted file]
question/yui/src/qbankmanager/meta/qbankmanager.json [deleted file]
theme/boost/layout/columns2.php
theme/boost/templates/columns1.mustache
theme/boost/templates/columns2.mustache
theme/boost/templates/core/navbar.mustache
theme/boost/templates/flat_navigation.mustache
theme/boost/templates/footer.mustache
theme/boost/templates/login.mustache
theme/boost/templates/maintenance.mustache
theme/boost/templates/navbar-secure.mustache
theme/boost/templates/secure.mustache

diff --git a/admin/tool/langimport/classes/locale.php b/admin/tool/langimport/classes/locale.php
new file mode 100644 (file)
index 0000000..69c43b6
--- /dev/null
@@ -0,0 +1,84 @@
+<?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/>.
+
+/**
+ * Helper class for the language import tool.
+ *
+ * @package    tool_langimport
+ * @copyright  2018 Université Rennes 2 {@link https://www.univ-rennes2.fr}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_langimport;
+
+use coding_exception;
+
+defined('MOODLE_INTERNAL') || die;
+
+/**
+ * Helper class for the language import tool.
+ *
+ * @copyright  2018 Université Rennes 2 {@link https://www.univ-rennes2.fr}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class locale {
+    /**
+     * Checks availability of locale on current operating system.
+     *
+     * @param string $langpackcode E.g.: en, es, fr, de.
+     * @return bool TRUE if the locale is available on OS.
+     * @throws coding_exception when $langpackcode parameter is a non-empty string.
+     */
+    public function check_locale_availability(string $langpackcode) : bool {
+        global $CFG;
+
+        if (empty($langpackcode)) {
+            throw new coding_exception('Invalid language pack code in \\'.__METHOD__.'() call, only non-empty string is allowed');
+        }
+
+        // Fetch the correct locale based on ostype.
+        if ($CFG->ostype === 'WINDOWS') {
+            $stringtofetch = 'localewin';
+        } else {
+            $stringtofetch = 'locale';
+        }
+
+        // Store current locale.
+        $currentlocale = $this->set_locale(LC_ALL, 0);
+
+        $locale = get_string_manager()->get_string($stringtofetch, 'langconfig', $a = null, $langpackcode);
+
+        // Try to set new locale.
+        $return = $this->set_locale(LC_ALL, $locale);
+
+        // Restore current locale.
+        $this->set_locale(LC_ALL, $currentlocale);
+
+        // If $return is not equal to false, it means that setlocale() succeed to change locale.
+        return $return !== false;
+    }
+
+    /**
+     * Wrap for the native PHP function setlocale().
+     *
+     * @param int $category Specifying the category of the functions affected by the locale setting.
+     * @param string $locale E.g.: en_AU.utf8, en_GB.utf8, es_ES.utf8, fr_FR.utf8, de_DE.utf8.
+     * @return string|false Returns the new current locale, or FALSE on error.
+     */
+    protected function set_locale(int $category = LC_ALL, string $locale = '0') {
+        return setlocale($category, $locale);
+    }
+}
index 92a09da..1180abc 100644 (file)
@@ -109,9 +109,16 @@ echo $OUTPUT->header();
 echo $OUTPUT->heading(get_string('langimport', 'tool_langimport'));
 
 $installedlangs = get_string_manager()->get_list_of_translations(true);
+$locale = new \tool_langimport\locale();
 
+$missinglocales = '';
 $missingparents = array();
-foreach ($installedlangs as $installedlang => $unused) {
+foreach ($installedlangs as $installedlang => $langpackname) {
+    // Check locale availability.
+    if (!$locale->check_locale_availability($installedlang)) {
+        $missinglocales .= '<li>'.$langpackname.'</li>';
+    }
+
     $parent = get_parent_language($installedlang);
     if (empty($parent)) {
         continue;
@@ -121,6 +128,14 @@ foreach ($installedlangs as $installedlang => $unused) {
     }
 }
 
+if (!empty($missinglocales)) {
+    // There is at least one missing locale.
+    $a = new stdClass();
+    $a->globallocale = moodle_getlocale();
+    $a->missinglocales = $missinglocales;
+    $controller->errors[] = get_string('langunsupported', 'tool_langimport', $a);
+}
+
 if ($availablelangs = $controller->availablelangs) {
     $remote = true;
 } else {
index faeb02c..27739ed 100644 (file)
@@ -37,6 +37,7 @@ $string['langpackupdateskipped'] = 'Update of \'{$a}\' language pack skipped';
 $string['langpackuptodate'] = 'Language pack \'{$a}\' is up-to-date';
 $string['langpackupdated'] = 'Language pack \'{$a}\' was successfully updated';
 $string['langpackupdatedevent'] = 'Language pack updated';
+$string['langunsupported'] = '<p>Your server does not seem to fully support the following languages:</p><ul>{$a->missinglocales}</ul><p>Instead, the global locale ({$a->globallocale}) will be used to format certain strings such as dates or numbers.</p>';
 $string['langupdatecomplete'] = 'Language pack update completed';
 $string['missingcfglangotherroot'] = 'Missing configuration value $CFG->langotherroot';
 $string['missinglangparent'] = 'Missing parent language <em>{$a->parent}</em> of <em>{$a->lang}</em>.';
diff --git a/admin/tool/langimport/tests/locale_test.php b/admin/tool/langimport/tests/locale_test.php
new file mode 100644 (file)
index 0000000..4d1ffef
--- /dev/null
@@ -0,0 +1,71 @@
+<?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/>.
+
+/**
+ * Tests for \tool_langimport\locale class.
+ *
+ * @package    tool_langimport
+ * @copyright  2018 Université Rennes 2 {@link https://www.univ-rennes2.fr}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Tests for \tool_langimport\locale class.
+ *
+ * @copyright  2018 Université Rennes 2 {@link https://www.univ-rennes2.fr}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class locale_testcase extends \advanced_testcase {
+    /**
+     * Test that \tool_langimport\locale::check_locale_availability() works as expected.
+     *
+     * @return void
+     */
+    public function test_check_locale_availability() {
+        // Create a mock of set_locale() method to simulate :
+        // - first setlocale() call which backup current locale
+        // - second setlocale() call which try to set new 'es' locale
+        // - third setlocale() call which restore locale.
+        $mock = $this->getMockBuilder(\tool_langimport\locale::class)
+            ->setMethods(['set_locale'])
+            ->getMock();
+        $mock->method('set_locale')->will($this->onConsecutiveCalls('en', 'es', 'en'));
+
+        // Test what happen when locale is available on system.
+        $result = $mock->check_locale_availability('en');
+        $this->assertTrue($result);
+
+        // Create a mock of set_locale() method to simulate :
+        // - first setlocale() call which backup current locale
+        // - second setlocale() call which fail to set new locale
+        // - third setlocale() call which restore locale.
+        $mock = $this->getMockBuilder(\tool_langimport\locale::class)
+            ->setMethods(['set_locale'])
+            ->getMock();
+        $mock->method('set_locale')->will($this->onConsecutiveCalls('en', false, 'en'));
+
+        // Test what happen when locale is not available on system.
+        $result = $mock->check_locale_availability('en');
+        $this->assertFalse($result);
+
+        // Test an invalid parameter.
+        $locale = new \tool_langimport\locale();
+        $this->expectException(coding_exception::class);
+        $locale->check_locale_availability('');
+    }
+}
diff --git a/lib/amd/build/checkbox-toggleall.min.js b/lib/amd/build/checkbox-toggleall.min.js
new file mode 100644 (file)
index 0000000..90e5ebc
Binary files /dev/null and b/lib/amd/build/checkbox-toggleall.min.js differ
diff --git a/lib/amd/src/checkbox-toggleall.js b/lib/amd/src/checkbox-toggleall.js
new file mode 100644 (file)
index 0000000..c6d4103
--- /dev/null
@@ -0,0 +1,126 @@
+// 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 module to help with toggle select/deselect all.
+ *
+ * @module     core/checkbox-toggleall
+ * @copyright  2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(['jquery', 'core/pubsub'], function($, PubSub) {
+
+    var registered = false;
+
+    var events = {
+        checkboxToggled: 'core/checkbox-toggleall:checkboxToggled',
+    };
+
+    var getAllCheckboxes = function(root, toggleGroup) {
+        return root.find('[data-action="toggle"][data-togglegroup="' + toggleGroup + '"]');
+    };
+
+    var getAllSlaveCheckboxes = function(root, toggleGroup) {
+        return getAllCheckboxes(root, toggleGroup).filter('[data-toggle="slave"]');
+    };
+
+    var getControlCheckboxes = function(root, toggleGroup) {
+        return getAllCheckboxes(root, toggleGroup).filter('[data-toggle="master"]');
+    };
+
+    var toggleSlavesFromMasters = function(e) {
+        var root = e.data.root;
+        var target = $(e.target);
+
+        var toggleGroupName = target.data('togglegroup');
+        var targetState = target.is(':checked');
+
+        var slaves = getAllSlaveCheckboxes(root, toggleGroupName);
+        var checkedSlaves = slaves.filter(':checked');
+
+        setMasterStates(root, toggleGroupName, targetState);
+
+        // Set the slave checkboxes from the masters.
+        slaves.prop('checked', targetState);
+
+        PubSub.publish(events.checkboxToggled, {
+            root: root,
+            toggleGroupName: toggleGroupName,
+            slaves: slaves,
+            checkedSlaves: checkedSlaves,
+            anyChecked: targetState,
+        });
+    };
+
+    var toggleMastersFromSlaves = function(e) {
+        var root = e.data.root;
+        var target = $(e.target);
+
+        var toggleGroupName = target.data('togglegroup');
+
+        var slaves = getAllSlaveCheckboxes(root, toggleGroupName);
+        var checkedSlaves = slaves.filter(':checked');
+        var targetState = (slaves.length === checkedSlaves.length);
+
+        setMasterStates(root, toggleGroupName, targetState);
+
+        PubSub.publish(events.checkboxToggled, {
+            root: root,
+            toggleGroupName: toggleGroupName,
+            slaves: slaves,
+            checkedSlaves: checkedSlaves,
+            anyChecked: !!checkedSlaves.length,
+        });
+    };
+
+    var setMasterStates = function(root, toggleGroupName, targetState) {
+        // Set the master checkboxes value and ARIA labels..
+        var masters = getControlCheckboxes(root, toggleGroupName);
+        masters.prop('checked', targetState);
+        masters.each(function(i, masterCheckbox) {
+            masterCheckbox = $(masterCheckbox);
+            var masterLabel = root.find('[for="' + masterCheckbox.attr('id') + '"]');
+            var targetString;
+            if (masterLabel.length) {
+                if (targetState) {
+                    targetString = masterCheckbox.data('toggle-deselectall');
+                } else {
+                    targetString = masterCheckbox.data('toggle-selectall');
+                }
+
+                if (masterLabel.html() !== targetString) {
+                    masterLabel.html(targetString);
+                }
+            }
+        });
+    };
+
+    var registerListeners = function() {
+        if (!registered) {
+            registered = true;
+
+            var root = $(document.body);
+            root.on('change', '[data-action="toggle"][data-toggle="master"]', {root: root}, toggleSlavesFromMasters);
+            root.on('change', '[data-action="toggle"][data-toggle="slave"]', {root: root}, toggleMastersFromSlaves);
+        }
+    };
+
+    return {
+        init: function() {
+            registerListeners();
+        },
+        events: events,
+    };
+});
diff --git a/lib/form/amd/build/showadvanced.min.js b/lib/form/amd/build/showadvanced.min.js
new file mode 100644 (file)
index 0000000..fb4ccde
Binary files /dev/null and b/lib/form/amd/build/showadvanced.min.js differ
diff --git a/lib/form/amd/src/showadvanced.js b/lib/form/amd/src/showadvanced.js
new file mode 100644 (file)
index 0000000..a9f9645
--- /dev/null
@@ -0,0 +1,219 @@
+// 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 class to help show and hide advanced form content.
+ *
+ * @module     core_form/showadvanced
+ * @class      showadvanced
+ * @package    core_form
+ * @copyright  2016 Damyon Wiese <damyon@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(['jquery', 'core/log', 'core/str', 'core/notification'], function($, Log, Strings, Notification) {
+
+    var SELECTORS = {
+            FIELDSETCONTAINSADVANCED: 'fieldset.containsadvancedelements',
+            DIVFITEMADVANCED: 'div.fitem.advanced',
+            DIVFCONTAINER: 'div.fcontainer',
+            MORELESSLINK: 'fieldset.containsadvancedelements .moreless-toggler'
+        },
+        CSS = {
+            SHOW: 'show',
+            MORELESSACTIONS: 'moreless-actions',
+            MORELESSTOGGLER: 'moreless-toggler',
+            SHOWLESS: 'moreless-less'
+        },
+        WRAPPERS = {
+            FITEM: '<div class="fitem"></div>',
+            FELEMENT: '<div class="felement"></div>'
+        },
+        IDPREFIX = 'showadvancedid-';
+
+    /** @type {Integer} uniqIdSeed Auto incrementing number used to generate ids. */
+    var uniqIdSeed = 0;
+
+    /**
+     * ShowAdvanced behaviour class.
+     * @param {String} id The id of the form.
+     */
+    var ShowAdvanced = function(id) {
+        this.id = id;
+
+        var form = $(document.getElementById(id));
+        this.enhanceForm(form);
+    };
+
+    /** @type {String} id The form id to enhance. */
+    ShowAdvanced.prototype.id = '';
+
+    /**
+     * @method enhanceForm
+     * @param {JQuery} form JQuery selector representing the form
+     * @return {ShowAdvanced}
+     */
+    ShowAdvanced.prototype.enhanceForm = function(form) {
+        var fieldsets = form.find(SELECTORS.FIELDSETCONTAINSADVANCED);
+
+        // Enhance each fieldset in the form matching the selector.
+        fieldsets.each(function(index, item) {
+            this.enhanceFieldset($(item));
+        }.bind(this));
+
+        // Attach some event listeners.
+        // Subscribe more/less links to click event.
+        form.on('click', SELECTORS.MORELESSLINK, this.switchState);
+
+        // Subscribe to key events but filter for space or enter.
+        form.on('keydown', SELECTORS.MORELESSLINK, function(e) {
+            // Enter or space.
+            if (e.which == 13 || e.which == 32) {
+                return this.switchState(e);
+            }
+            return true;
+        }.bind(this));
+        return this;
+    };
+
+
+    /**
+     * Generates a uniq id for the dom element it's called on unless the element already has an id.
+     * The id is set on the dom node before being returned.
+     *
+     * @method generateId
+     * @param {JQuery} node JQuery selector representing a single DOM Node.
+     * @return {String}
+     */
+    ShowAdvanced.prototype.generateId = function(node) {
+        var id = node.prop('id');
+        if (typeof id === 'undefined') {
+            id = IDPREFIX + (uniqIdSeed++);
+            node.prop('id', id);
+        }
+        return id;
+    };
+
+    /**
+     * @method enhanceFieldset
+     * @param {JQuery} fieldset JQuery selector representing a fieldset
+     * @return {ShowAdvanced}
+     */
+    ShowAdvanced.prototype.enhanceFieldset = function(fieldset) {
+        var statuselement = $('input[name=mform_showmore_' + fieldset.prop('id') + ']');
+        if (!statuselement.length) {
+            Log.debug("M.form.showadvanced::processFieldset was called on an fieldset without a status field: '" +
+                fieldset.prop('id') + "'");
+            return this;
+        }
+
+        // Fetch some strings.
+        Strings.get_strings([{
+            key: 'showmore',
+            component: 'core_form'
+        }, {
+            key: 'showless',
+            component: 'core_form'
+        }]).then(function(results) {
+            var showmore = results[0],
+                showless = results[1];
+
+            // Generate more/less links.
+            var morelesslink = $('<a href="#"></a>');
+            morelesslink.addClass(CSS.MORELESSTOGGLER);
+            if (statuselement.val() === '0') {
+                morelesslink.html(showmore);
+            } else {
+                morelesslink.html(showless);
+                morelesslink.addClass(CSS.SHOWLESS);
+                fieldset.find(SELECTORS.DIVFITEMADVANCED).addClass(CSS.SHOW);
+            }
+            // Build a list of advanced fieldsets.
+            var idlist = [];
+            fieldset.find(SELECTORS.DIVFITEMADVANCED).each(function(index, node) {
+                idlist[idlist.length] = this.generateId($(node));
+            }.bind(this));
+
+            // Set aria attributes.
+            morelesslink.attr('role', 'button');
+            morelesslink.attr('aria-controls', idlist.join(' '));
+
+            // Add elements to the DOM.
+            var fitem = $(WRAPPERS.FITEM);
+            fitem.addClass(CSS.MORELESSACTIONS);
+            var felement = $(WRAPPERS.FELEMENT);
+            felement.append(morelesslink);
+            fitem.append(felement);
+
+            fieldset.find(SELECTORS.DIVFCONTAINER).append(fitem);
+            return true;
+        }.bind(this)).fail(Notification.exception);
+
+        return this;
+    };
+
+    /**
+     * @method switchState
+     * @param {Event} e Event that triggered this action.
+     * @return {Boolean}
+     */
+    ShowAdvanced.prototype.switchState = function(e) {
+        e.preventDefault();
+
+        // Fetch some strings.
+        Strings.get_strings([{
+            key: 'showmore',
+            component: 'core_form'
+        }, {
+            key: 'showless',
+            component: 'core_form'
+        }]).then(function(results) {
+            var showmore = results[0],
+                showless = results[1],
+                fieldset = $(e.target).closest(SELECTORS.FIELDSETCONTAINSADVANCED);
+
+            // Toggle collapsed class.
+            fieldset.find(SELECTORS.DIVFITEMADVANCED).toggleClass(CSS.SHOW);
+
+            // Get corresponding hidden variable.
+            var statuselement = $('input[name=mform_showmore_' + fieldset.prop('id') + ']');
+
+            // Invert it and change the link text.
+            if (statuselement.val() === '0') {
+                statuselement.val(1);
+                $(e.target).addClass(CSS.SHOWLESS);
+                $(e.target).html(showless);
+            } else {
+                statuselement.val(0);
+                $(e.target).removeClass(CSS.SHOWLESS);
+                $(e.target).html(showmore);
+            }
+            return true;
+        }).fail(Notification.exception);
+
+        return this;
+    };
+
+    return {
+        /**
+         * Initialise this module.
+         * @method init
+         * @param {String} formid
+         * @return {ShowAdvanced}
+         */
+        init: function(formid) {
+            return new ShowAdvanced(formid);
+        }
+    };
+});
diff --git a/lib/form/yui/build/moodle-form-showadvanced/moodle-form-showadvanced-debug.js b/lib/form/yui/build/moodle-form-showadvanced/moodle-form-showadvanced-debug.js
deleted file mode 100644 (file)
index 4a032e0..0000000
Binary files a/lib/form/yui/build/moodle-form-showadvanced/moodle-form-showadvanced-debug.js and /dev/null differ
diff --git a/lib/form/yui/build/moodle-form-showadvanced/moodle-form-showadvanced-min.js b/lib/form/yui/build/moodle-form-showadvanced/moodle-form-showadvanced-min.js
deleted file mode 100644 (file)
index 8cfbaa1..0000000
Binary files a/lib/form/yui/build/moodle-form-showadvanced/moodle-form-showadvanced-min.js and /dev/null differ
diff --git a/lib/form/yui/build/moodle-form-showadvanced/moodle-form-showadvanced.js b/lib/form/yui/build/moodle-form-showadvanced/moodle-form-showadvanced.js
deleted file mode 100644 (file)
index 6c2ee05..0000000
Binary files a/lib/form/yui/build/moodle-form-showadvanced/moodle-form-showadvanced.js and /dev/null differ
diff --git a/lib/form/yui/src/showadvanced/build.json b/lib/form/yui/src/showadvanced/build.json
deleted file mode 100644 (file)
index 2f81737..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-{
-    "name": "moodle-form-showadvanced",
-    "builds": {
-        "moodle-form-showadvanced": {
-            "jsfiles": [
-                "showadvanced.js"
-            ]
-        }
-    }
-}
diff --git a/lib/form/yui/src/showadvanced/js/showadvanced.js b/lib/form/yui/src/showadvanced/js/showadvanced.js
deleted file mode 100644 (file)
index 564f86f..0000000
+++ /dev/null
@@ -1,146 +0,0 @@
-/**
- * Provides the form showadvanced class.
- *
- * @module moodle-form-showadvanced
- */
-
-/**
- * A class to help show and hide advanced form content.
- *
- * @class M.form.showadvanced
- * @constructor
- * @extends Base
- */
-function SHOWADVANCED() {
-    SHOWADVANCED.superclass.constructor.apply(this, arguments);
-}
-
-var SELECTORS = {
-        FIELDSETCONTAINSADVANCED: 'fieldset.containsadvancedelements',
-        DIVFITEMADVANCED: 'div.fitem.advanced',
-        DIVFCONTAINER: 'div.fcontainer',
-        MORELESSLINK: 'fieldset.containsadvancedelements .moreless-toggler'
-    },
-    CSS = {
-        SHOW: 'show',
-        MORELESSACTIONS: 'moreless-actions',
-        MORELESSTOGGLER: 'moreless-toggler',
-        SHOWLESS: 'moreless-less'
-    },
-    WRAPPERS = {
-        FITEM: '<div class="fitem"></div>',
-        FELEMENT: '<div class="felement"></div>'
-    },
-    ATTRS = {};
-
-/**
- * The form ID attribute definition.
- *
- * @attribute formid
- * @type String
- * @default null
- * @writeOnce
- */
-ATTRS.formid = {
-    value: null
-};
-
-Y.extend(SHOWADVANCED, Y.Base, {
-    /**
-     * The initializer for the showadvanced instance.
-     *
-     * @method initializer
-     * @protected
-     */
-    initializer: function() {
-        var form = Y.one('#' + this.get('formid')),
-            fieldlist = form.all(SELECTORS.FIELDSETCONTAINSADVANCED);
-
-        // Look through fieldset divs that contain advanced elements.
-        fieldlist.each(this.processFieldset, this);
-
-        // Subscribe more/less links to click event.
-        form.delegate('click', this.switchState, SELECTORS.MORELESSLINK);
-        form.delegate('key', this.switchState, 'down:enter,32', SELECTORS.MORELESSLINK);
-    },
-
-    /**
-     * Process the supplied fieldset to add appropriate links, and ARIA roles.
-     *
-     * @method processFieldset
-     * @param {Node} fieldset The Node relating to the fieldset to add collapsing to.
-     * @chainable
-     */
-    processFieldset: function(fieldset) {
-        var statuselement = Y.one('input[name=mform_showmore_' + fieldset.get('id') + ']');
-        if (!statuselement) {
-            Y.log("M.form.showadvanced::processFieldset was called on an fieldset without a status field: '" +
-                fieldset.get('id') + "'", 'debug', 'moodle-form-showadvanced');
-            return this;
-        }
-
-        var morelesslink = Y.Node.create('<a href="#"></a>');
-        morelesslink.addClass(CSS.MORELESSTOGGLER);
-        if (statuselement.get('value') === '0') {
-            morelesslink.setHTML(M.util.get_string('showmore', 'form'));
-        } else {
-            morelesslink.setHTML(M.util.get_string('showless', 'form'));
-            morelesslink.addClass(CSS.SHOWLESS);
-            fieldset.all(SELECTORS.DIVFITEMADVANCED).addClass(CSS.SHOW);
-        }
-
-        // Get list of IDs controlled by this button to set the aria-controls attribute.
-        var idlist = [];
-        fieldset.all(SELECTORS.DIVFITEMADVANCED).each(function(node) {
-            idlist[idlist.length] = node.generateID();
-        });
-        morelesslink.setAttribute('role', 'button');
-        morelesslink.setAttribute('aria-controls', idlist.join(' '));
-
-        var fitem = Y.Node.create(WRAPPERS.FITEM);
-        fitem.addClass(CSS.MORELESSACTIONS);
-        var felement = Y.Node.create(WRAPPERS.FELEMENT);
-        felement.append(morelesslink);
-        fitem.append(felement);
-
-        fieldset.one(SELECTORS.DIVFCONTAINER).append(fitem);
-
-        return this;
-    },
-
-    /**
-     * Toggle the state for the fieldset that was clicked.
-     *
-     * @method switchState
-     * @param {EventFacade} e
-     */
-    switchState: function(e) {
-        e.preventDefault();
-        var fieldset = this.ancestor(SELECTORS.FIELDSETCONTAINSADVANCED);
-
-        // Toggle collapsed class.
-        fieldset.all(SELECTORS.DIVFITEMADVANCED).toggleClass(CSS.SHOW);
-
-        // Get corresponding hidden variable.
-        var statuselement = Y.one('input[name=mform_showmore_' + fieldset.get('id') + ']');
-
-        // Invert it and change the link text.
-        if (statuselement.get('value') === '0') {
-            statuselement.set('value', 1);
-            this.addClass(CSS.SHOWLESS);
-            this.setHTML(M.util.get_string('showless', 'form'));
-        } else {
-            statuselement.set('value', 0);
-            this.removeClass(CSS.SHOWLESS);
-            this.setHTML(M.util.get_string('showmore', 'form'));
-        }
-    }
-}, {
-    NAME: 'moodle-form-showadvanced',
-    ATTRS: ATTRS
-});
-
-M.form = M.form || {};
-M.form.showadvanced = M.form.showadvanced || function(params) {
-    return new SHOWADVANCED(params);
-};
diff --git a/lib/form/yui/src/showadvanced/meta/showadvanced.json b/lib/form/yui/src/showadvanced/meta/showadvanced.json
deleted file mode 100644 (file)
index befdc20..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-{
-    "moodle-form-showadvanced": {
-        "requires": [
-            "node",
-            "base",
-            "selector-css3"
-        ]
-    }
-}
index caa39ef..acd0b21 100644 (file)
@@ -2919,8 +2919,7 @@ class MoodleQuickForm_Renderer extends HTML_QuickForm_Renderer_Tableless{
             $PAGE->requires->yui_module('moodle-form-shortforms', 'M.form.shortforms', array(array('formid' => $formid)));
         }
         if (!empty($this->_advancedElements)){
-            $PAGE->requires->strings_for_js(array('showmore', 'showless'), 'form');
-            $PAGE->requires->yui_module('moodle-form-showadvanced', 'M.form.showadvanced', array(array('formid' => $formid)));
+            $PAGE->requires->js_call_amd('core_form/showadvanced', 'init', [$formid]);
         }
     }
 
index 5ebe7af..3c44836 100644 (file)
@@ -8010,6 +8010,28 @@ function moodle_major_version($fromdisk = false) {
 
 // MISCELLANEOUS.
 
+/**
+ * Gets the system locale
+ *
+ * @return string Retuns the current locale.
+ */
+function moodle_getlocale() {
+    global $CFG;
+
+    // Fetch the correct locale based on ostype.
+    if ($CFG->ostype == 'WINDOWS') {
+        $stringtofetch = 'localewin';
+    } else {
+        $stringtofetch = 'locale';
+    }
+
+    if (!empty($CFG->locale)) { // Override locale for all language packs.
+        return $CFG->locale;
+    }
+
+    return get_string($stringtofetch, 'langconfig');
+}
+
 /**
  * Sets the system locale
  *
@@ -8023,20 +8045,11 @@ function moodle_setlocale($locale='') {
 
     $oldlocale = $currentlocale;
 
-    // Fetch the correct locale based on ostype.
-    if ($CFG->ostype == 'WINDOWS') {
-        $stringtofetch = 'localewin';
-    } else {
-        $stringtofetch = 'locale';
-    }
-
     // The priority is the same as in get_string() - parameter, config, course, session, user, global language.
     if (!empty($locale)) {
         $currentlocale = $locale;
-    } else if (!empty($CFG->locale)) { // Override locale for all language packs.
-        $currentlocale = $CFG->locale;
     } else {
-        $currentlocale = get_string($stringtofetch, 'langconfig');
+        $currentlocale = moodle_getlocale();
     }
 
     // Do nothing if locale already set up.
index 93d32b5..1090652 100644 (file)
@@ -430,15 +430,16 @@ class navigation_node implements renderable {
      *
      * @param flat_navigation $nodes List of the found flat navigation nodes.
      * @param boolean $showdivider Show a divider before the first node.
+     * @param string $label A label for the collection of navigation links.
      */
-    public function build_flat_navigation_list(flat_navigation $nodes, $showdivider = false) {
+    public function build_flat_navigation_list(flat_navigation $nodes, $showdivider = false, $label = '') {
         if ($this->showinflatnavigation) {
             $indent = 0;
             if ($this->type == self::TYPE_COURSE || $this->key === self::COURSE_INDEX_PAGE) {
                 $indent = 1;
             }
             $flat = new flat_navigation_node($this, $indent);
-            $flat->set_showdivider($showdivider);
+            $flat->set_showdivider($showdivider, $label);
             $nodes->add($flat);
         }
         foreach ($this->children as $child) {
@@ -913,6 +914,12 @@ class navigation_node_collection implements IteratorAggregate, Countable {
      */
     protected $count = 0;
 
+    /**
+     * Label for collection of nodes.
+     * @var string
+     */
+    protected $collectionlabel = '';
+
     /**
      * Adds a navigation node to the collection
      *
@@ -988,6 +995,24 @@ class navigation_node_collection implements IteratorAggregate, Countable {
         return $keys;
     }
 
+    /**
+     * Set a label for this collection.
+     *
+     * @param string $label
+     */
+    public function set_collectionlabel($label) {
+        $this->collectionlabel = $label;
+    }
+
+    /**
+     * Return a label for this collection.
+     *
+     * @return string
+     */
+    public function get_collectionlabel() {
+        return $this->collectionlabel;
+    }
+
     /**
      * Fetches a node from this collection.
      *
@@ -3770,6 +3795,9 @@ class flat_navigation_node extends navigation_node {
     /** @var $showdivider bool Show a divider before this element */
     private $showdivider = false;
 
+    /** @var $collectionlabel string Label for a group of nodes */
+    private $collectionlabel = '';
+
     /**
      * A proxy constructor
      *
@@ -3791,6 +3819,31 @@ class flat_navigation_node extends navigation_node {
         $this->indent = $indent;
     }
 
+    /**
+     * Setter, a label is required for a flat navigation node that shows a divider.
+     *
+     * @param string $label
+     */
+    public function set_collectionlabel($label) {
+        $this->collectionlabel = $label;
+    }
+
+    /**
+     * Getter, get the label for this flat_navigation node, or it's parent if it doesn't have one.
+     *
+     * @return string
+     */
+    public function get_collectionlabel() {
+        if (!empty($this->collectionlabel)) {
+            return $this->collectionlabel;
+        }
+        if ($this->parent && ($this->parent instanceof flat_navigation_node || $this->parent instanceof flat_navigation)) {
+            return $this->parent->get_collectionlabel();
+        }
+        debugging('Navigation region requires a label', DEBUG_DEVELOPER);
+        return '';
+    }
+
     /**
      * Does this node represent a course section link.
      * @return boolean
@@ -3828,9 +3881,15 @@ class flat_navigation_node extends navigation_node {
     /**
      * Setter for "showdivider"
      * @param $val boolean
+     * @param $label string Label for the group of nodes
      */
-    public function set_showdivider($val) {
+    public function set_showdivider($val, $label = '') {
         $this->showdivider = $val;
+        if ($this->showdivider && empty($label)) {
+            debugging('Navigation region requires a label', DEBUG_DEVELOPER);
+        } else {
+            $this->set_collectionlabel($label);
+        }
     }
 
     /**
@@ -3848,7 +3907,6 @@ class flat_navigation_node extends navigation_node {
     public function set_indent($val) {
         $this->indent = $val;
     }
-
 }
 
 /**
@@ -3903,6 +3961,7 @@ class flat_navigation extends navigation_node_collection {
                 format_string($course->fullname, true, array('context' => $coursecontext));
 
             $flat = new flat_navigation_node(navigation_node::create($coursename, $url), 0);
+            $flat->set_collectionlabel($coursename);
             $flat->key = 'coursehome';
             $flat->icon = new pix_icon('i/course', '');
 
@@ -3930,9 +3989,9 @@ class flat_navigation extends navigation_node_collection {
                 }
             }
 
-            $this->page->navigation->build_flat_navigation_list($this, true);
+            $this->page->navigation->build_flat_navigation_list($this, true, get_string('site'));
         } else {
-            $this->page->navigation->build_flat_navigation_list($this, false);
+            $this->page->navigation->build_flat_navigation_list($this, false, get_string('site'));
         }
 
         $admin = $PAGE->settingsnav->find('siteadministration', navigation_node::TYPE_SITE_ADMIN);
@@ -3942,7 +4001,7 @@ class flat_navigation extends navigation_node_collection {
         }
         if ($admin) {
             $flat = new flat_navigation_node($admin, 0);
-            $flat->set_showdivider(true);
+            $flat->set_showdivider(true, get_string('sitesettings'));
             $flat->key = 'sitesettings';
             $flat->icon = new pix_icon('t/preferences', '');
             $this->add($flat);
@@ -3956,7 +4015,7 @@ class flat_navigation extends navigation_node_collection {
             $url = new moodle_url($PAGE->url, ['bui_addblock' => '', 'sesskey' => sesskey()]);
             $addablock = navigation_node::create(get_string('addblock'), $url);
             $flat = new flat_navigation_node($addablock, 0);
-            $flat->set_showdivider(true);
+            $flat->set_showdivider(true, get_string('blocksaddedit'));
             $flat->key = 'addblock';
             $flat->icon = new pix_icon('i/addblock', '');
             $this->add($flat);
@@ -3969,6 +4028,26 @@ class flat_navigation extends navigation_node_collection {
         }
     }
 
+
+    /**
+     * Override the parent so we can set a label for this collection if it has not been set yet.
+     *
+     * @param navigation_node $node Node to add
+     * @param string $beforekey If specified, adds before a node with this key,
+     *   otherwise adds at end
+     * @return navigation_node Added node
+     */
+    public function add(navigation_node $node, $beforekey=null) {
+        $result = parent::add($node, $beforekey);
+        // Extend the parent to get a name for the collection of nodes if required.
+        if (empty($this->collectionlabel)) {
+            if ($node instanceof flat_navigation_node) {
+                $this->set_collectionlabel($node->get_collectionlabel());
+            }
+        }
+
+        return $result;
+    }
 }
 
 /**
index 530bce0..9f6e4af 100644 (file)
@@ -3,6 +3,7 @@ information provided here is intended especially for developers.
 
 === 3.7 ===
 
+* Nodes in the navigation api can have labels for each group. See set/get_collectionlabel().
 * The method core_user::is_real_user() now returns false for userid = 0 parameter
 * 'mform1' dependencies (in themes, js...) will stop working because a randomly generated string has been added to the id
 attribute on forms to avoid collisions in forms loaded in AJAX requests.
index 0bcc236..ff35374 100644 (file)
Binary files a/message/amd/build/notification_processor_settings.min.js and b/message/amd/build/notification_processor_settings.min.js differ
index d4347b0..3d1c9f6 100644 (file)
Binary files a/message/amd/build/preferences_notifications_list_controller.min.js and b/message/amd/build/preferences_notifications_list_controller.min.js differ
index 6964dfb..8ce9f05 100644 (file)
Binary files a/message/amd/build/preferences_processor_form.min.js and b/message/amd/build/preferences_processor_form.min.js differ
index bb3763b..1fb2132 100644 (file)
  * @copyright  2016 Ryan Wyllie <ryan@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-define(['jquery', 'core/ajax', 'core/notification', 'core/fragment', 'core/templates', 'core/str', 'tool_lp/dialogue'],
-        function($, Ajax, Notification, Fragment, Templates, Str, Dialogue) {
+define([
+        'jquery',
+        'core/ajax',
+        'core/str',
+        'core/notification',
+        'core/custom_interaction_events',
+        'core/modal',
+        'core/modal_registry',
+        'core/fragment',
+        ],
+        function(
+            $,
+            Ajax,
+            Str,
+            Notification,
+            CustomEvents,
+            Modal,
+            ModalRegistry,
+            Fragment
+        ) {
 
+    var registered = false;
     var SELECTORS = {
+        SAVE_BUTTON: '[data-action="save"]',
+        CANCEL_BUTTON: '[data-action="cancel"]',
         PROCESSOR: '[data-processor-name]',
         PREFERENCE_ROW: '[data-region="preference-row"]',
     };
 
     /**
-     * Constructor for the notification processor settings.
+     * Constructor for the Modal.
      *
-     * @param {object} element jQuery object root element of the processor
+     * @param {object} root The root jQuery element for the modal.
      */
-    var NotificationProcessorSettings = function(element) {
-        this.root = $(element);
-        this.name = this.root.attr('data-name');
-        this.userId = this.root.attr('data-user-id');
-        this.contextId = this.root.attr('data-context-id');
+    var NotificationProcessorSettings = function(root) {
+        Modal.call(this, root);
+        this.name = null;
+        this.userId = null;
+        this.contextId = null;
+        this.element = null;
+        this.saveButton = this.getFooter().find(SELECTORS.SAVE_BUTTON);
+        this.cancelButton = this.getFooter().find(SELECTORS.CANCEL_BUTTON);
+    };
+
+    NotificationProcessorSettings.TYPE = 'core_message-notification_processor_settings';
+    NotificationProcessorSettings.prototype = Object.create(Modal.prototype);
+    NotificationProcessorSettings.prototype.constructor = NotificationProcessorSettings;
+
+    /**
+     * Set the userid to the given value.
+     *
+     * @method setUserId
+     * @param {int} id The notification userid
+     */
+    NotificationProcessorSettings.prototype.setUserId = function(id) {
+        this.userId = id;
+    };
+
+    /**
+     * Retrieve the current userid, if any.
+     *
+     * @method getUserId
+     * @return {int|null} The notification userid
+     */
+    NotificationProcessorSettings.prototype.getUserId = function() {
+        return this.userId;
+    };
+
+    /**
+     * Set the object to the given value.
+     *
+     * @method setElement
+     * @param {object} element The notification node element.
+     */
+    NotificationProcessorSettings.prototype.setElement = function(element) {
+        this.element = element;
+    };
+
+    /**
+     * Retrieve the current element, if any.
+     *
+     * @method getElement
+     * @return {object|null} The notification node element.
+     */
+    NotificationProcessorSettings.prototype.getElement = function() {
+        return this.element;
+    };
+
+    /**
+     * Set the name to the given value.
+     *
+     * @method setName
+     * @param {string} name The notification name.
+     */
+    NotificationProcessorSettings.prototype.setName = function(name) {
+        this.name = name;
+    };
+
+    /**
+     * Retrieve the current name, if any.
+     *
+     * @method getName
+     * @return {string|null} The notification name.
+     */
+    NotificationProcessorSettings.prototype.getName = function() {
+        return this.name;
+    };
+    /**
+     * Set the context id to the given value.
+     *
+     * @method setContextId
+     * @param {Number} id The notification context id
+     */
+    NotificationProcessorSettings.prototype.setContextId = function(id) {
+        this.contextId = id;
+    };
+
+    /**
+     * Retrieve the current context id, if any.
+     *
+     * @method getContextId
+     * @return {Number|null} The notification context id
+     */
+    NotificationProcessorSettings.prototype.getContextId = function() {
+        return this.contextId;
+    };
+
+    /**
+     * Get the form element from the modal.
+     *
+     * @method getForm
+     * @return {object}
+     */
+    NotificationProcessorSettings.prototype.getForm = function() {
+        return this.getBody().find('form');
+    };
+
+    /**
+     * Disable the buttons in the footer.
+     *
+     * @method disableButtons
+     */
+    NotificationProcessorSettings.prototype.disableButtons = function() {
+        this.saveButton.prop('disabled', true);
+        this.cancelButton.prop('disabled', true);
+    };
+
+    /**
+     * Enable the buttons in the footer.
+     *
+     * @method enableButtons
+     */
+    NotificationProcessorSettings.prototype.enableButtons = function() {
+        this.saveButton.prop('disabled', false);
+        this.cancelButton.prop('disabled', false);
+    };
+
+    /**
+     * Load the title for the modal to the appropriate value
+     * depending on message outputs.
+     *
+     * @method loadTitleContent
+     * @return {object} A promise resolved with the new title text.
+     */
+    NotificationProcessorSettings.prototype.loadTitleContent = function() {
+        this.titlePromise = Str.get_string('processorsettings', 'message');
+        this.setTitle(this.titlePromise);
+
+        return this.titlePromise;
+    };
+
+    /**
+     * Load the body for the modal to the appropriate value
+     * depending on message outputs.
+     *
+     * @method loadBodyContent
+     * @return {object} A promise resolved with the fragment html and js from
+     */
+    NotificationProcessorSettings.prototype.loadBodyContent = function() {
+        this.disableButtons();
+
+        var args = {
+            userid: this.getUserId(),
+            type: this.getName(),
+        };
+
+        this.bodyPromise = Fragment.loadFragment('message', 'processor_settings', this.getContextId(), args);
+        this.setBody(this.bodyPromise);
+
+        this.bodyPromise.then(function() {
+            this.enableButtons();
+            return;
+        }.bind(this))
+        .fail(Notification.exception);
+
+        return this.bodyPromise;
     };
 
     /**
-     * Show the notification processor settings dialogue.
+     * Load both the title and body content.
+     *
+     * @method loadAllContent
+     * @return {object} promise
+     */
+    NotificationProcessorSettings.prototype.loadAllContent = function() {
+        return $.when(this.loadTitleContent(), this.loadBodyContent());
+    };
+
+    /**
+     * Load the modal content before showing it. This
+     * is to allow us to re-use the same modal for creating and
+     * editing different message outputs within the page.
      *
      * @method show
      */
     NotificationProcessorSettings.prototype.show = function() {
-        Fragment.loadFragment('message', 'processor_settings', this.contextId, {
-            userid: this.userId,
-            type: this.name,
-        })
-        .done(function(html, js) {
-            Str.get_string('processorsettings', 'message').done(function(string) {
-                var dialogue = new Dialogue(
-                    string,
-                    html,
-                    function() {
-                        Templates.runTemplateJS(js);
-                    },
-                    function() {
-                        // Removed dialogue from the DOM after close.
-                        dialogue.close();
-                    }
-                );
-
-                $(document).on('mpp:formsubmitted', function() {
-                    dialogue.close();
-                    this.updateConfiguredStatus();
-                }.bind(this));
-
-                $(document).on('mpp:formcancelled', function() {
-                    dialogue.close();
-                });
-            }.bind(this));
-        }.bind(this));
+        this.loadAllContent();
+        Modal.prototype.show.call(this);
+    };
+
+    /**
+     * Clear the notification from the modal when it's closed so
+     * that it is loaded fresh next time it's displayed.
+     *
+     * @method hide
+     */
+    NotificationProcessorSettings.prototype.hide = function() {
+        Modal.prototype.hide.call(this);
+        this.setContextId(null);
+        this.setName(null);
+        this.setUserId(null);
     };
 
     /**
@@ -86,7 +263,7 @@ define(['jquery', 'core/ajax', 'core/notification', 'core/fragment', 'core/templ
      * @return {Promise|boolean}
      */
     NotificationProcessorSettings.prototype.updateConfiguredStatus = function() {
-        var processorHeader = this.root.closest(SELECTORS.PROCESSOR);
+        var processorHeader = $(this.getElement()).closest(SELECTORS.PROCESSOR);
 
         if (!processorHeader.hasClass('unconfigured')) {
             return false;
@@ -114,5 +291,44 @@ define(['jquery', 'core/ajax', 'core/notification', 'core/fragment', 'core/templ
             });
     };
 
+    /**
+     * Set up all of the event handling for the modal.
+     *
+     * @method registerEventListeners
+     */
+    NotificationProcessorSettings.prototype.registerEventListeners = function() {
+        // Apply parent event listeners.
+        Modal.prototype.registerEventListeners.call(this);
+
+        // When the user clicks the save button we trigger the form submission.
+        this.getModal().on(CustomEvents.events.activate, SELECTORS.SAVE_BUTTON, function(e, data) {
+            this.getForm().submit();
+            data.originalEvent.preventDefault();
+        }.bind(this));
+
+        this.getModal().on('mpp:formsubmitted', function(e) {
+            this.hide();
+            this.updateConfiguredStatus();
+            e.stopPropagation();
+        }.bind(this));
+
+        this.getModal().on(CustomEvents.events.activate, SELECTORS.CANCEL_BUTTON, function(e, data) {
+            this.hide();
+            data.originalEvent.preventDefault();
+            e.stopPropagation();
+        }.bind(this));
+    };
+
+    // Automatically register with the modal registry the first time this module is imported
+    // so that you can create modals
+    // of this type using the modal factory.
+    if (!registered) {
+        ModalRegistry.register(
+                                NotificationProcessorSettings.TYPE,
+                                NotificationProcessorSettings,
+                                'core/modal_save_cancel');
+        registered = true;
+    }
+
     return NotificationProcessorSettings;
 });
index 14ab2e6..8b73113 100644 (file)
  * @copyright  2016 Ryan Wyllie <ryan@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-define(['jquery', 'core/ajax', 'core/notification', 'core/custom_interaction_events', 'core_message/notification_preference',
-        'core_message/notification_processor_settings'],
-        function($, Ajax, Notification, CustomEvents, NotificationPreference, NotificationProcessorSettings) {
+define(['jquery',
+        'core/ajax',
+        'core/notification',
+        'core/custom_interaction_events',
+        'core_message/notification_preference',
+        'core_message/notification_processor_settings',
+        'core/modal_factory',
+        ],
+        function(
+          $,
+          Ajax,
+          Notification,
+          CustomEvents,
+          NotificationPreference,
+          NotificationProcessorSettings,
+          ModalFactory
+        ) {
 
     var SELECTORS = {
         DISABLE_NOTIFICATIONS: '[data-region="disable-notification-container"] [data-disable-notifications]',
@@ -143,11 +157,25 @@ define(['jquery', 'core/ajax', 'core/notification', 'core/custom_interaction_eve
             }
         }.bind(this));
 
-        this.root.on(CustomEvents.events.activate, SELECTORS.PROCESSOR_SETTING, function(e, data) {
+        var eventFormPromise = ModalFactory.create({
+            type: NotificationProcessorSettings.TYPE,
+        });
+
+        this.root.on(CustomEvents.events.activate, SELECTORS.PROCESSOR_SETTING, function(e) {
             var element = $(e.target).closest(SELECTORS.PROCESSOR_SETTING);
-            var processorSettings = new NotificationProcessorSettings(element);
-            processorSettings.show();
-            data.originalEvent.preventDefault();
+
+            e.preventDefault();
+            eventFormPromise.then(function(modal) {
+                // Configure modal with element settings.
+                modal.setUserId($(element).attr('data-user-id'));
+                modal.setName($(element).attr('data-name'));
+                modal.setContextId($(element).attr('data-context-id'));
+                modal.setElement(element);
+                modal.show();
+
+                e.stopImmediatePropagation();
+                return;
+            }).fail(Notification.exception);
         });
 
         CustomEvents.define(disabledNotificationsElement, [
index 53b3766..e458d8b 100644 (file)
@@ -22,8 +22,8 @@
  * @copyright  2016 Ryan Wyllie <ryan@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-define(['jquery', 'core/ajax', 'core/notification', 'core/custom_interaction_events'],
-        function($, Ajax, Notification, CustomEvents) {
+define(['jquery', 'core/ajax', 'core/notification'],
+        function($, Ajax, Notification) {
     /**
      * Constructor for the ProcessorForm.
      *
@@ -37,18 +37,9 @@ define(['jquery', 'core/ajax', 'core/notification', 'core/custom_interaction_eve
         this.root.find('form').on('submit', function(e) {
             e.preventDefault();
             this.save().done(function() {
-                $(document).trigger('mpp:formsubmitted');
+                $(element).trigger('mpp:formsubmitted');
             });
         }.bind(this));
-
-        var cancelButton = this.root.find('[data-cancel-button]');
-        CustomEvents.define(cancelButton, [
-            CustomEvents.events.activate
-        ]);
-
-        cancelButton.on(CustomEvents.events.activate, function() {
-            $(document).trigger('mpp:formcancelled');
-        });
     };
 
     /**
index 42be717..dd77931 100644 (file)
     </div>
     <form>
         {{{formhtml}}}
-        <div class="form-actions m-t-1">
-            <button type="submit" class="btn btn-primary">{{#str}} savechanges {{/str}}</button>
-            <button type="button" class="btn" data-cancel-button>{{#str}} cancel {{/str}}</button>
-        </div>
     </form>
 </div>
 {{#js}}
diff --git a/question/amd/build/qbankmanager.min.js b/question/amd/build/qbankmanager.min.js
new file mode 100644 (file)
index 0000000..50dd343
Binary files /dev/null and b/question/amd/build/qbankmanager.min.js differ
diff --git a/question/amd/src/qbankmanager.js b/question/amd/src/qbankmanager.js
new file mode 100644 (file)
index 0000000..78ff864
--- /dev/null
@@ -0,0 +1,55 @@
+// 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 handle question ajax actions.
+ *
+ * @module     core_question/qbankmanager
+ * @class      qbankmanager
+ * @package    core_question
+ * @copyright 2018 The Open University
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(['jquery', 'core/pubsub', 'core/checkbox-toggleall'], function($, PubSub, ToggleAll) {
+
+    var registerListeners = function() {
+        PubSub.subscribe(ToggleAll.events.checkboxToggled, toggleButtonStates);
+    };
+
+    var toggleButtonStates = function(data) {
+        if ('qbank' !== data.toggleGroupName) {
+            return;
+        }
+
+        setButtonState(data.anyChecked);
+    };
+
+    var setButtonState = function(state) {
+        var buttons = $('.modulespecificbuttonscontainer').find('input, select, link');
+        buttons.attr('disabled', !state);
+    };
+
+    return {
+        /**
+         * Set up the Question Bank Manager.
+         *
+         * @method init
+         */
+        init: function() {
+            setButtonState(false);
+            registerListeners();
+        },
+    };
+});
index 289aaba..cddbaf7 100644 (file)
@@ -26,7 +26,9 @@ class checkbox_column extends column_base {
     protected $strselect;
 
     public function init() {
-        $this->strselect = get_string('select');
+        global $PAGE;
+
+        $PAGE->requires->js_call_amd('core/checkbox-toggleall', 'init');
     }
 
     public function get_name() {
@@ -34,21 +36,41 @@ class checkbox_column extends column_base {
     }
 
     protected function get_title() {
-        return '<input type="checkbox" disabled="disabled" id="qbheadercheckbox" />';
+        $input = \html_writer::empty_tag('input', [
+            'id' => 'qbheadercheckbox',
+            'name' => 'qbheadercheckbox',
+            'type' => 'checkbox',
+            'value' => '1',
+            'data-action' => 'toggle',
+            'data-toggle' => 'master',
+            'data-togglegroup' => 'qbank',
+            'data-toggle-selectall' => get_string('selectall', 'moodle'),
+            'data-toggle-deselectall' => get_string('deselectall', 'moodle'),
+        ]);
+
+        $label = \html_writer::tag('label', get_string('selectall', 'moodle'), [
+            'class' => 'accesshide',
+            'for' => 'qbheadercheckbox',
+        ]);
+
+        return $input . $label;
     }
 
     protected function get_title_tip() {
-        global $PAGE;
-        $PAGE->requires->strings_for_js(array('selectall', 'deselectall'), 'moodle');
-        $PAGE->requires->yui_module('moodle-question-qbankmanager', 'M.question.qbankmanager.init');
         return get_string('selectquestionsforbulk', 'question');
-
     }
 
     protected function display_content($question, $rowclasses) {
-        global $PAGE;
-        echo '<input title="' . $this->strselect . '" type="checkbox" name="q' .
-                $question->id . '" id="checkq' . $question->id . '" value="1"/>';
+        echo \html_writer::empty_tag('input', [
+            'title' => get_string('select'),
+            'type' => 'checkbox',
+            'name' => "q{$question->id}",
+            'id' => "checkq{$question->id}",
+            'value' => '1',
+            'data-action' => 'toggle',
+            'data-toggle' => 'slave',
+            'data-togglegroup' => 'qbank',
+        ]);
     }
 
     public function get_required_fields() {
index ec5b81f..9225af8 100644 (file)
@@ -688,7 +688,7 @@ class view {
     protected function display_question_list($contexts, $pageurl, $categoryandcontext,
             $cm = null, $recurse=1, $page=0, $perpage=100, $showhidden=false,
             $showquestiontext = false, $addcontexts = array()) {
-        global $CFG, $DB, $OUTPUT;
+        global $CFG, $DB, $OUTPUT, $PAGE;
 
         // This function can be moderately slow with large question counts and may time out.
         // We probably do not want to raise it to unlimited, so randomly picking 5 minutes.
@@ -758,6 +758,8 @@ class view {
         }
         echo '</div>';
 
+        $PAGE->requires->js_call_amd('core_question/qbankmanager', 'init');
+
         $this->display_bottom_controls($totalnumber, $recurse, $category, $catcontext, $addcontexts);
 
         echo '</fieldset>';
diff --git a/question/tests/behat/select_questions.feature b/question/tests/behat/select_questions.feature
new file mode 100644 (file)
index 0000000..b4b766e
--- /dev/null
@@ -0,0 +1,56 @@
+@core @core_question
+Feature: The questions in the question bank can be selected in various ways
+  In selected to do something for questions
+  As a teacher
+  I want to choose them to move, delete it.
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email                |
+      | teacher1 | Teacher   | 1        | teacher1@example.com |
+    And the following "courses" exist:
+      | fullname | shortname | format |
+      | Course 1 | C1        | weeks  |
+    And the following "course enrolments" exist:
+      | user     | course | role           |
+      | teacher1 | C1     | editingteacher |
+    And the following "question categories" exist:
+      | contextlevel | reference | name           |
+      | Course       | C1        | Test questions |
+    And the following "questions" exist:
+      | questioncategory | qtype     | name              | user     | questiontext    |
+      | Test questions   | essay     | A question 1 name | admin    | Question 1 text |
+      | Test questions   | essay     | B question 2 name | teacher1 | Question 2 text |
+      | Test questions   | numerical | C question 3 name | teacher1 | Question 3 text |
+    And I log in as "teacher1"
+    And I am on "Course 1" course homepage
+    And I navigate to "Question bank > Questions" in current page administration
+
+  @javascript
+  Scenario: The question text can be chosen all in the list of questions
+    Given the field "Select all" matches value ""
+    When I click on "Select all" "checkbox"
+    And the field "A question 1 name" matches value "1"
+    And the field "B question 2 name" matches value "1"
+    And the field "C question 3 name" matches value "1"
+    Then I click on "Deselect all" "checkbox"
+    And the field "A question 1 name" matches value ""
+    And the field "B question 2 name" matches value ""
+    And the field "C question 3 name" matches value ""
+
+  @javascript
+  Scenario: The question text can be chosen in the list of questions
+    Given the field "Select all" matches value ""
+    When I click on "A question 1 name" "checkbox"
+    Then the field "Select all" matches value ""
+    And I click on "B question 2 name" "checkbox"
+    And I click on "C question 3 name" "checkbox"
+    And the field "Deselect all" matches value "1"
+
+  @javascript
+  Scenario: The action button can be disabled when the question not be chosen in the list of questions
+    Given the "Delete" "button" should be disabled
+    And the "Move to >>" "button" should be disabled
+    When I click on "Select all" "checkbox"
+    Then the "Delete" "button" should be enabled
+    And the "Move to >>" "button" should be enabled
diff --git a/question/yui/build/moodle-question-qbankmanager/moodle-question-qbankmanager-debug.js b/question/yui/build/moodle-question-qbankmanager/moodle-question-qbankmanager-debug.js
deleted file mode 100644 (file)
index fbe45fe..0000000
Binary files a/question/yui/build/moodle-question-qbankmanager/moodle-question-qbankmanager-debug.js and /dev/null differ
diff --git a/question/yui/build/moodle-question-qbankmanager/moodle-question-qbankmanager-min.js b/question/yui/build/moodle-question-qbankmanager/moodle-question-qbankmanager-min.js
deleted file mode 100644 (file)
index 22b2432..0000000
Binary files a/question/yui/build/moodle-question-qbankmanager/moodle-question-qbankmanager-min.js and /dev/null differ
diff --git a/question/yui/build/moodle-question-qbankmanager/moodle-question-qbankmanager.js b/question/yui/build/moodle-question-qbankmanager/moodle-question-qbankmanager.js
deleted file mode 100644 (file)
index fbe45fe..0000000
Binary files a/question/yui/build/moodle-question-qbankmanager/moodle-question-qbankmanager.js and /dev/null differ
diff --git a/question/yui/src/qbankmanager/build.json b/question/yui/src/qbankmanager/build.json
deleted file mode 100644 (file)
index 2c8d36e..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-{
-    "name": "moodle-question-qbankmanager",
-    "builds": {
-        "moodle-question-qbankmanager": {
-            "jsfiles": [
-                "qbankmanager.js"
-            ]
-        }
-    }
-}
diff --git a/question/yui/src/qbankmanager/js/qbankmanager.js b/question/yui/src/qbankmanager/js/qbankmanager.js
deleted file mode 100644 (file)
index cb6ea38..0000000
+++ /dev/null
@@ -1,137 +0,0 @@
-// 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/>.
-
-/*
- * Question Bank Management.
- *
- * @package    question
- * @copyright  2014 Andrew Nicols
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-/**
- * Questionbank Management.
- *
- * @module moodle-question-qbankmanager
- */
-
-/**
- * Question Bank Management.
- *
- * @class M.question.qbankmanager
- */
-
-var manager = {
-    /**
-     * A reference to the header checkbox.
-     *
-     * @property _header
-     * @type Node
-     * @private
-     */
-    _header: null,
-
-    /**
-     * A reference to the add to quiz button.
-     *
-     * @property _addbutton
-     * @type Node
-     * @private
-     */
-    _addbutton: null,
-
-    /**
-     * The ID of the first checkbox on the page.
-     *
-     * @property _firstCheckbox
-     * @type Node
-     * @private
-     */
-    _firstCheckbox: null,
-
-    /**
-     * Set up the Question Bank Manager.
-     *
-     * @method init
-     */
-    init: function() {
-        // Find the header checkbox, and set the initial values.
-        this._header = Y.one('#qbheadercheckbox');
-        if (!this._header) {
-            return;
-        }
-        this._header.setAttrs({
-            disabled: false,
-            title: M.util.get_string('selectall', 'moodle')
-        });
-
-        this._header.on('click', this._headerClick, this);
-
-        this._addbutton = Y.one('.modulespecificbuttonscontainer input[name="add"]');
-        // input[name="add"] is not always available.
-        if (this._addbutton) {
-            this._addbutton.setAttrs({
-                disabled: true
-            });
-
-            this._header.on('click', this._questionClick, this);
-            Y.one('.categoryquestionscontainer').delegate('change', this._questionClick,
-                'td.checkbox input[type="checkbox"]', this);
-        }
-
-        // Store the first checkbox details.
-        var table = this._header.ancestor('table');
-        this._firstCheckbox = table.one('tbody tr td.checkbox input');
-    },
-
-    /**
-     * Handle toggling of the header checkbox.
-     *
-     * @method _headerClick
-     * @private
-     */
-    _headerClick: function() {
-        // Get the list of questions we affect.
-        var categoryQuestions = Y.one('#categoryquestions')
-                .all('[type=checkbox],[type=radio]');
-
-        // We base the state of all of the questions on the state of the first.
-        if (this._firstCheckbox.get('checked')) {
-            categoryQuestions.set('checked', false);
-            this._header.setAttribute('title', M.util.get_string('selectall', 'moodle'));
-        } else {
-            categoryQuestions.set('checked', true);
-            this._header.setAttribute('title', M.util.get_string('deselectall', 'moodle'));
-        }
-
-        this._header.set('checked', false);
-    },
-
-    /**
-     * Handle toggling of a question checkbox.
-     *
-     * @method _questionClick
-     * @private
-     */
-    _questionClick: function() {
-        var areChecked = Y.all('td.checkbox input[type="checkbox"]:checked').size();
-        this._addbutton.setAttrs({
-            disabled: (areChecked === 0)
-        });
-    }
-};
-
-M.question = M.question || {};
-M.question.qbankmanager = M.question.qbankmanager || manager;
diff --git a/question/yui/src/qbankmanager/meta/qbankmanager.json b/question/yui/src/qbankmanager/meta/qbankmanager.json
deleted file mode 100644 (file)
index 6410cbc..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-{
-    "moodle-question-qbankmanager": {
-        "requires": [
-            "node",
-            "selector-css3"
-        ]
-    }
-}
index d942fcb..b44726f 100644 (file)
@@ -51,6 +51,8 @@ $templatecontext = [
     'hasregionmainsettingsmenu' => !empty($regionmainsettingsmenu)
 ];
 
-$templatecontext['flatnavigation'] = $PAGE->flatnav;
+$nav = $PAGE->flatnav;
+$templatecontext['flatnavigation'] = $nav;
+$templatecontext['firstcollectionlabel'] = $nav->get_collectionlabel();
 echo $OUTPUT->render_from_template('theme_boost/columns2', $templatecontext);
 
index 022df60..49ed3ba 100644 (file)
@@ -47,7 +47,7 @@
     <div id="page" class="container-fluid">
         <div id="page-content" class="row pb-3">
             <div id="region-main-box" class="col-12">
-                <section id="region-main">
+                <section id="region-main" aria-label="{{#str}}content{{/str}}">
                     {{{ output.course_content_header }}}
                     {{{ output.main_content }}}
                     {{{ output.activity_navigation }}}
index 7b329cb..d0425d3 100644 (file)
@@ -68,7 +68,7 @@
                     <div> {{{ output.region_main_settings_menu }}} </div>
                 </div>
                 {{/hasregionmainsettingsmenu}}
-                <section id="region-main" {{#hasblocks}}class="has-blocks mb-3"{{/hasblocks}}>
+                <section id="region-main" {{#hasblocks}}class="has-blocks mb-3"{{/hasblocks}} aria-label="{{#str}}content{{/str}}">
 
                     {{#hasregionmainsettingsmenu}}
                         <div class="region_main_settings_menu_proxy"></div>
@@ -80,7 +80,7 @@
 
                 </section>
                 {{#hasblocks}}
-                <section data-region="blocks-column" class="d-print-none">
+                <section data-region="blocks-column" class="d-print-none" aria-label="{{#str}}blocks{{/str}}">
                     {{{ sidepreblocks }}}
                 </section>
                 {{/hasblocks}}
index 07d0c04..207081f 100644 (file)
@@ -63,7 +63,7 @@
         ]
     }
 }}
-<nav role="navigation">
+<nav role="navigation" aria-label="{{#str}}breadcrumb, access{{/str}}">
     <ol class="breadcrumb">
         {{#get_items}}
             {{#has_action}}
index f19e907..3c9d44a 100644 (file)
         ]
     }
 }}
-<nav class="list-group">
+<nav class="list-group" aria-label="{{firstcollectionlabel}}">
 {{# flatnavigation }}
     {{#showdivider}}
 </nav>
-<nav class="list-group m-t-1">
+<nav class="list-group m-t-1" aria-label="{{get_collectionlabel}}">
     {{/showdivider}}
     {{#action}}
     <a class="list-group-item list-group-item-action {{#isactive}}active{{/isactive}}" href="{{{action}}}" data-key="{{key}}" data-isexpandable="{{isexpandable}}" data-indent="{{get_indent}}" data-showdivider="{{showdivider}}" data-type="{{type}}" data-nodetype="{{nodetype}}" data-collapse="{{collapse}}" data-forceopen="{{forceopen}}" data-isactive="{{isactive}}" data-hidden="{{hidden}}" data-preceedwithhr="{{preceedwithhr}}" {{#parent.key}}data-parent-key="{{.}}"{{/parent.key}}>
index 4cb4c34..9827bbd 100644 (file)
@@ -28,7 +28,7 @@
         {{{ output.login_info }}}
         <div class="tool_usertours-resettourcontainer"></div>
         {{{ output.home_link }}}
-        <nav class="nav navbar-nav d-md-none">
+        <nav class="nav navbar-nav d-md-none" aria-label="{{#str}}custommenu, admin{{/str}}">
             {{# output.custom_menu_flat }}
                 <ul class="list-unstyled pt-3">
                     {{> theme_boost/custom_menu_footer }}
@@ -38,4 +38,4 @@
         {{{ output.standard_footer_html }}}
         {{{ output.standard_end_of_body_html }}}
     </div>
-</footer>
\ No newline at end of file
+</footer>
index ebca109..f44017d 100644 (file)
@@ -40,7 +40,7 @@
     <div id="page" class="container-fluid mt-0">
         <div id="page-content" class="row">
             <div id="region-main-box" class="col-12">
-                <section id="region-main" class="col-12">
+                <section id="region-main" class="col-12" aria-label="{{#str}}content{{/str}}">
                     {{{ output.course_content_header }}}
                     {{{ output.main_content }}}
                     {{{ output.course_content_footer }}}
index f62c6c8..0330892 100644 (file)
@@ -56,7 +56,7 @@
         </div>
 
         <div id="page-content" class="row">
-            <section id="region-main" class="col-12">
+            <section id="region-main" class="col-12" aria-label="{{#str}}content{{/str}}">
                 {{{ output.main_content }}}
             </section>
         </div>
index dbbca33..8dadab0 100644 (file)
@@ -17,7 +17,7 @@
 {{!
     secure navbar.
 }}
-<nav class="fixed-top navbar navbar-light bg-white navbar-expand moodle-has-zindex">
+<nav class="fixed-top navbar navbar-light bg-white navbar-expand moodle-has-zindex" aria-label="{{#str}}navigation{{/str}}">
 
         <a href="{{{ config.wwwroot }}}" class="navbar-brand {{# output.should_display_navbar_logo }}has-logo{{/ output.should_display_navbar_logo }}
             {{^ output.should_display_navbar_logo }}
@@ -37,4 +37,4 @@
             {{{ output.secure_login_info }}}
             </li>
         </ul>
-</nav>
\ No newline at end of file
+</nav>
index 385e3ab..a6ceaba 100644 (file)
@@ -59,7 +59,7 @@
 
         <div id="page-content" class="row">
             <div id="region-main-box" class="col-12">
-                <section id="region-main" {{#hasblocks}}class="has-blocks"{{/hasblocks}}>
+                <section id="region-main" {{#hasblocks}}class="has-blocks"{{/hasblocks}} aria-label="{{#str}}content{{/str}}">
 
                     {{{ output.course_content_header }}}
                     {{{ output.main_content }}}
@@ -67,7 +67,7 @@
 
                 </section>
                 {{#hasblocks}}
-                <section data-region="blocks-column">
+                <section data-region="blocks-column" aria-label="{{#str}}blocks{{/str}}">
                     {{{ sidepreblocks }}}
                 </section>
                 {{/hasblocks}}