Merge branch 'MDL-70409-310' of git://github.com/merrill-oakland/moodle into MOODLE_3...
authorSara Arjona <sara@moodle.com>
Tue, 15 Dec 2020 15:15:50 +0000 (16:15 +0100)
committerSara Arjona <sara@moodle.com>
Tue, 15 Dec 2020 15:15:50 +0000 (16:15 +0100)
55 files changed:
admin/tool/behat/tests/behat/edit_permissions.feature
admin/tool/behat/tests/behat/keyboard.feature
blocks/myoverview/amd/build/view.min.js
blocks/myoverview/amd/build/view.min.js.map
blocks/myoverview/amd/src/view.js
blocks/myoverview/classes/output/main.php
blocks/myoverview/tests/behat/block_myoverview_pagelimit_persistence.feature
blocks/myoverview/tests/behat/block_myoverview_pagination.feature [new file with mode: 0644]
blocks/timeline/templates/event-list-item.mustache
course/amd/build/local/activitychooser/dialogue.min.js
course/amd/build/local/activitychooser/dialogue.min.js.map
course/amd/build/local/activitychooser/selectors.min.js
course/amd/build/local/activitychooser/selectors.min.js.map
course/amd/src/local/activitychooser/dialogue.js
course/amd/src/local/activitychooser/selectors.js
enrol/cohort/settings.php
enrol/database/settings.php
enrol/fee/settings.php
enrol/manual/settings.php
enrol/mnet/settings.php
enrol/paypal/settings.php
enrol/self/settings.php
install/lang/ky/admin.php [new file with mode: 0644]
install/lang/mk/error.php
install/lang/mk/moodle.php
install/lang/sv/admin.php
lib/amd/build/paged_content_factory.min.js
lib/amd/build/paged_content_factory.min.js.map
lib/amd/build/permissionmanager.min.js
lib/amd/build/permissionmanager.min.js.map
lib/amd/src/paged_content_factory.js
lib/amd/src/permissionmanager.js
lib/cronlib.php
lib/db/upgrade.php
lib/templates/paged_content_paging_bar.mustache
lib/templates/permissionmanager_panelcontent.mustache
lib/templates/permissionmanager_role.mustache
lib/templates/settings_link_page.mustache
lib/tests/behat/behat_hooks.php
mod/assign/module.js
mod/lti/amd/build/contentitem.min.js
mod/lti/amd/build/contentitem.min.js.map
mod/lti/amd/src/contentitem.js
mod/lti/locallib.php
question/engine/lib.php
search/classes/manager.php
search/tests/manager_test.php
theme/boost/amd/build/aria.min.js
theme/boost/amd/build/aria.min.js.map
theme/boost/amd/build/loader.min.js
theme/boost/amd/build/loader.min.js.map
theme/boost/amd/src/aria.js
theme/boost/amd/src/loader.js
theme/boost/templates/admin_setting_tabs.mustache
version.php

index e3ffc92..7394512 100644 (file)
@@ -6,14 +6,18 @@ Feature: Edit capabilities
 
   Background:
     Given the following "users" exist:
-      | username | firstname | lastname | email |
-      | teacher1 | Teacher | 1 | teacher1@example.com |
+      | username | firstname | lastname  |
+      | teacher1 | Teacher   | 1         |
+      | tutor    | Teaching  | Assistant |
+      | student  | Student   | One       |
     And the following "courses" exist:
-      | fullname | shortname | category |
-      | Course 1 | C1 | 0 |
+      | fullname | shortname |
+      | Course 1 | C1        |
     And the following "course enrolments" exist:
-      | user | course | role |
-      | teacher1 | C1 | editingteacher |
+      | user     | course | role           |
+      | teacher1 | C1     | editingteacher |
+      | tutor    | C1     | teacher        |
+      | student  | C1     | student        |
 
   Scenario: Default system capabilities modification
     Given I log in as "admin"
@@ -60,3 +64,25 @@ Feature: Edit capabilities
     Then "mod/forum:deleteanypost" capability has "Prohibit" permission
     And "mod/forum:editanypost" capability has "Prevent" permission
     And "mod/forum:addquestion" capability has "Allow" permission
+
+  @javascript
+  Scenario: Edit permissions escapes role names correctly
+    When I am on the "C1" "Course" page logged in as "admin"
+    And I navigate to "Edit settings" in current page administration
+    And I set the following fields to these values:
+      | Your word for 'Teacher'             | Teacher >= editing  |
+      | Your word for 'Non-editing teacher' | Teacher < "editing" |
+      | Your word for 'Student'             | Studier & 'learner' |
+    And I press "Save and display"
+    And I navigate to course participants
+    Then I should see "Teacher >= editing (Teacher)" in the "Teacher 1" "table_row"
+    And I should see "Teacher < \"editing\" (Non-editing teacher)" in the "Teaching Assistant" "table_row"
+    And I should see "Studier & 'learner' (Student)" in the "Student One" "table_row"
+    And I navigate to "Users > Permissions" in current page administration
+    And I should see "Teacher >= editing" in the "mod/forum:replypost" "table_row"
+    And I should see "Teacher < \"editing\"" in the "mod/forum:replypost" "table_row"
+    And I should see "Studier & 'learner'" in the "mod/forum:replypost" "table_row"
+    And I follow "Prohibit"
+    And "Teacher >= editing" "button" in the "Prohibit role" "dialogue" should be visible
+    And "Teacher < \"editing\"" "button" in the "Prohibit role" "dialogue" should be visible
+    And "Studier & 'learner'" "button" in the "Prohibit role" "dialogue" should be visible
index ca9a74f..cdcb081 100644 (file)
@@ -28,15 +28,16 @@ Feature: Verify that keyboard steps work as expected
     And I press the shift tab key
     And the focused element is "Username" "field"
 
-  @javascript
-  Scenario: Using the arrow keys allows me to navigate through menus
-    Given the following "users" exist:
-      | username | email                        | firstname | lastname |
-      | saffronr | saffron.rutledge@example.com | Saffron   | Rutledge |
-    And I log in as "saffronr"
-    And I click on "Saffron Rutledge" "link" in the ".usermenu" "css_element"
-    When I press the up key
-    Then the focused element is "Log out" "link"
+#  TODO: Uncomment the following when MDL-66979 is integrated.
+#  @javascript
+#  Scenario: Using the arrow keys allows me to navigate through menus
+#    Given the following "users" exist:
+#      | username | email                        | firstname | lastname |
+#      | saffronr | saffron.rutledge@example.com | Saffron   | Rutledge |
+#    And I log in as "saffronr"
+#    And I click on "Saffron Rutledge" "link" in the ".usermenu" "css_element"
+#    When I press the up key
+#    Then the focused element is "Log out" "link"
 
   @javascript
   Scenario: The escape key can be used to close a dialogue
index 8e18c99..17ed845 100644 (file)
Binary files a/blocks/myoverview/amd/build/view.min.js and b/blocks/myoverview/amd/build/view.min.js differ
index 723993f..e238948 100644 (file)
Binary files a/blocks/myoverview/amd/build/view.min.js.map and b/blocks/myoverview/amd/build/view.min.js.map differ
index 524c4ba..1c92361 100644 (file)
@@ -543,11 +543,9 @@ function(
 
         // Filter out all pagination options which are too large for the amount of courses user is enrolled in.
         var totalCourseCount = parseInt(root.find(Selectors.courseView.region).attr('data-totalcoursecount'), 10);
-        if (totalCourseCount) {
-            itemsPerPage = itemsPerPage.filter(function(pagingOption) {
-                return pagingOption.value < totalCourseCount;
-            });
-        }
+        itemsPerPage = itemsPerPage.filter(function(pagingOption) {
+            return pagingOption.value < totalCourseCount || pagingOption.value === 0;
+        });
 
         var filters = getFilterValues(root);
         var config = $.extend({}, DEFAULT_PAGED_CONTENT_CONFIG);
@@ -602,7 +600,8 @@ function(
                                 pageCourses = $.merge(loadedPages[currentPage].courses, courses.slice(0, nextPageStart));
                             }
                         } else {
-                            nextPageStart = pageData.limit;
+                            // When the page limit is zero, there is only one page of courses, no start for next page.
+                            nextPageStart = pageData.limit || false;
                             pageCourses = (pageData.limit > 0) ? courses.slice(0, pageData.limit) : courses;
                         }
 
@@ -611,8 +610,8 @@ function(
                             courses: pageCourses
                         };
 
-                        // Set up the next page
-                        var remainingCourses = nextPageStart ? courses.slice(nextPageStart, courses.length) : [];
+                        // Set up the next page (if there is more than one page).
+                        var remainingCourses = nextPageStart !== false ? courses.slice(nextPageStart, courses.length) : [];
                         if (remainingCourses.length) {
                             loadedPages[currentPage + 1] = {
                                 courses: remainingCourses
index c290c98..b0236fd 100644 (file)
@@ -202,8 +202,9 @@ class main implements renderable, templatable {
         // Check and remember the given view.
         $this->view = $view ? $view : BLOCK_MYOVERVIEW_VIEW_CARD;
 
-        // Check and remember the given page size.
-        if ($paging == BLOCK_MYOVERVIEW_PAGING_ALL) {
+        // Check and remember the given page size, `null` indicates no page size set
+        // while a `0` indicates a paging size of `All`.
+        if (!is_null($paging) && $paging == BLOCK_MYOVERVIEW_PAGING_ALL) {
             $this->paging = BLOCK_MYOVERVIEW_PAGING_ALL;
         } else {
             $this->paging = $paging ? $paging : BLOCK_MYOVERVIEW_PAGING_12;
index a192c3d..fef9a8b 100644 (file)
@@ -7,30 +7,30 @@ Feature: The my overview block allows users to persistence of their page limits
       | student1 | Student   | X        | student1@example.com | S1       |
     And the following "courses" exist:
       | fullname | shortname | category |
-      | Course 1 | C1        | 0        |
-      | Course 2 | C2        | 0        |
-      | Course 3 | C3        | 0        |
-      | Course 4 | C4        | 0        |
-      | Course 5 | C5        | 0        |
-      | Course 6 | C6        | 0        |
-      | Course 7 | C7        | 0        |
-      | Course 8 | C8        | 0        |
-      | Course 9 | C9        | 0        |
+      | Course 1 | C01        | 0        |
+      | Course 2 | C02        | 0        |
+      | Course 3 | C03        | 0        |
+      | Course 4 | C04        | 0        |
+      | Course 5 | C05        | 0        |
+      | Course 6 | C06        | 0        |
+      | Course 7 | C07        | 0        |
+      | Course 8 | C08        | 0        |
+      | Course 9 | C09        | 0        |
       | Course 10 | C10        | 0        |
       | Course 11 | C11        | 0        |
       | Course 12 | C12        | 0        |
       | Course 13 | C13        | 0        |
     And the following "course enrolments" exist:
       | user | course | role |
-      | student1 | C1 | student |
-      | student1 | C2 | student |
-      | student1 | C3 | student |
-      | student1 | C4 | student |
-      | student1 | C5 | student |
-      | student1 | C6 | student |
-      | student1 | C7 | student |
-      | student1 | C8 | student |
-      | student1 | C9 | student |
+      | student1 | C01 | student |
+      | student1 | C02 | student |
+      | student1 | C03 | student |
+      | student1 | C04 | student |
+      | student1 | C05 | student |
+      | student1 | C06 | student |
+      | student1 | C07 | student |
+      | student1 | C08 | student |
+      | student1 | C09 | student |
       | student1 | C10 | student |
       | student1 | C11 | student |
       | student1 | C12 | student |
@@ -38,8 +38,8 @@ Feature: The my overview block allows users to persistence of their page limits
 
   Scenario: Toggle the page limit between page reloads
     Given I log in as "student1"
-    When I click on "[data-toggle='dropdown']" "css_element" in the "Course overview" "block"
-    And I click on "All" "link"
+    When I click on "[data-action='limit-toggle']" "css_element" in the "Course overview" "block"
+    And I click on "All" "link" in the ".dropdown-menu.show" "css_element"
     Then I should see "Course 13"
     And I reload the page
     Then I should see "Course 13"
@@ -47,8 +47,8 @@ Feature: The my overview block allows users to persistence of their page limits
 
   Scenario: Toggle the page limit between grouping changes
     Given I log in as "student1"
-    When I click on "[data-toggle='dropdown']" "css_element" in the "Course overview" "block"
-    And I click on "All" "link"
+    When I click on "[data-action='limit-toggle']" "css_element" in the "Course overview" "block"
+    And I click on "All" "link" in the ".dropdown-menu.show" "css_element"
     And I click on "All (except removed from view)" "button" in the "Course overview" "block"
     And I click on "In progress" "link" in the "Course overview" "block"
     Then I should see "Course 13"
diff --git a/blocks/myoverview/tests/behat/block_myoverview_pagination.feature b/blocks/myoverview/tests/behat/block_myoverview_pagination.feature
new file mode 100644 (file)
index 0000000..9792252
--- /dev/null
@@ -0,0 +1,191 @@
+@block @block_myoverview @javascript
+Feature: My overview block pagination
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email                | idnumber |
+      | student1 | Student   | X        | student1@example.com | S1       |
+    And the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 01 | C1       | 0        |
+      | Course 02 | C2       | 0        |
+      | Course 03 | C3       | 0        |
+      | Course 04 | C4       | 0        |
+      | Course 05 | C5       | 0        |
+      | Course 06 | C6       | 0        |
+      | Course 07 | C7       | 0        |
+      | Course 08 | C8       | 0        |
+      | Course 09 | C9       | 0        |
+      | Course 10 | C10      | 0        |
+      | Course 11 | C11      | 0        |
+      | Course 12 | C12      | 0        |
+      | Course 13 | C13      | 0        |
+      | Course 14 | C14      | 0        |
+      | Course 15 | C15      | 0        |
+      | Course 16 | C16      | 0        |
+      | Course 17 | C17      | 0        |
+      | Course 18 | C18      | 0        |
+      | Course 19 | C19      | 0        |
+      | Course 20 | C20      | 0        |
+      | Course 21 | C21      | 0        |
+      | Course 22 | C22      | 0        |
+      | Course 23 | C23      | 0        |
+      | Course 24 | C24      | 0        |
+      | Course 25 | C25      | 0        |
+
+  Scenario: The pagination controls should be hidden if I am not enrolled in any courses
+    When I log in as "student1"
+    Then I should see "No courses" in the "Course overview" "block"
+    And I should not see "Show" in the "Course overview" "block"
+    And ".block_myoverview .dropdown-menu.show" "css_element" should not be visible
+    And ".block_myoverview [data-control='next']" "css_element" should not be visible
+    And ".block_myoverview [data-control='previous']" "css_element" should not be visible
+    And I log out
+
+  Scenario: The pagination controls should be hidden if I am enrolled in 12 courses or less
+    Given the following "course enrolments" exist:
+      | user | course | role |
+      | student1 | C1 | student |
+      | student1 | C2 | student |
+      | student1 | C3 | student |
+      | student1 | C4 | student |
+      | student1 | C5 | student |
+      | student1 | C6 | student |
+      | student1 | C7 | student |
+      | student1 | C8 | student |
+      | student1 | C9 | student |
+      | student1 | C10 | student |
+      | student1 | C11 | student |
+      | student1 | C12 | student |
+    When I log in as "student1"
+    Then I should not see "Show" in the "Course overview" "block"
+    And ".block_myoverview .dropdown-menu.show" "css_element" should not be visible
+    And ".block_myoverview [data-control='next']" "css_element" should not be visible
+    And ".block_myoverview [data-control='previous']" "css_element" should not be visible
+    And I log out
+
+  Scenario: The default pagination should be 12 courses
+    Given the following "course enrolments" exist:
+      | user | course | role |
+      | student1 | C1 | student |
+      | student1 | C2 | student |
+      | student1 | C3 | student |
+      | student1 | C4 | student |
+      | student1 | C5 | student |
+      | student1 | C6 | student |
+      | student1 | C7 | student |
+      | student1 | C8 | student |
+      | student1 | C9 | student |
+      | student1 | C10 | student |
+      | student1 | C11 | student |
+      | student1 | C12 | student |
+      | student1 | C13 | student |
+    When I log in as "student1"
+    Then I should see "12" in the "[data-action='limit-toggle']" "css_element"
+    And I log out
+
+  Scenario: I should only see pagination limit options less than total number of enrolled courses
+    Given the following "course enrolments" exist:
+      | user | course | role |
+      | student1 | C1 | student |
+      | student1 | C2 | student |
+      | student1 | C3 | student |
+      | student1 | C4 | student |
+      | student1 | C5 | student |
+      | student1 | C6 | student |
+      | student1 | C7 | student |
+      | student1 | C8 | student |
+      | student1 | C9 | student |
+      | student1 | C10 | student |
+      | student1 | C11 | student |
+      | student1 | C12 | student |
+      | student1 | C13 | student |
+    And I log in as "student1"
+    When I click on "[data-action='limit-toggle']" "css_element" in the "Course overview" "block"
+    Then I should see "All" in the ".dropdown-menu.show" "css_element"
+    And I should see "12" in the ".dropdown-menu.show" "css_element"
+    And ".block_myoverview [data-control='next']" "css_element" should be visible
+    And ".block_myoverview [data-control='previous']" "css_element" should be visible
+    But I should not see "24" in the ".block_myoverview .dropdown-menu.show" "css_element"
+    And I log out
+
+  Scenario: Previous page button should be disabled when on the first page of courses
+    Given the following "course enrolments" exist:
+      | user | course | role |
+      | student1 | C1 | student |
+      | student1 | C2 | student |
+      | student1 | C3 | student |
+      | student1 | C4 | student |
+      | student1 | C5 | student |
+      | student1 | C6 | student |
+      | student1 | C7 | student |
+      | student1 | C8 | student |
+      | student1 | C9 | student |
+      | student1 | C10 | student |
+      | student1 | C11 | student |
+      | student1 | C12 | student |
+      | student1 | C13 | student |
+    When I log in as "student1"
+    Then the "class" attribute of ".block_myoverview [data-control='previous']" "css_element" should contain "disabled"
+    And I log out
+
+  Scenario: Next page button should be disabled when on the last page of courses
+    Given the following "course enrolments" exist:
+      | user | course | role |
+      | student1 | C1 | student |
+      | student1 | C2 | student |
+      | student1 | C3 | student |
+      | student1 | C4 | student |
+      | student1 | C5 | student |
+      | student1 | C6 | student |
+      | student1 | C7 | student |
+      | student1 | C8 | student |
+      | student1 | C9 | student |
+      | student1 | C10 | student |
+      | student1 | C11 | student |
+      | student1 | C12 | student |
+      | student1 | C13 | student |
+    When I log in as "student1"
+    And I click on "[data-control='next']" "css_element" in the "Course overview" "block"
+    And I wait until ".block_myoverview [data-control='next']" "css_element" exists
+    Then the "class" attribute of ".block_myoverview [data-control='next']" "css_element" should contain "disabled"
+    And I log out
+
+  Scenario: Next and previous page buttons should both be enabled when not on last or first page of courses
+    Given the following "course enrolments" exist:
+      | user | course | role |
+      | student1 | C1 | student |
+      | student1 | C2 | student |
+      | student1 | C3 | student |
+      | student1 | C4 | student |
+      | student1 | C5 | student |
+      | student1 | C6 | student |
+      | student1 | C7 | student |
+      | student1 | C8 | student |
+      | student1 | C9 | student |
+      | student1 | C10 | student |
+      | student1 | C11 | student |
+      | student1 | C12 | student |
+      | student1 | C13 | student |
+      | student1 | C14 | student |
+      | student1 | C15 | student |
+      | student1 | C16 | student |
+      | student1 | C17 | student |
+      | student1 | C18 | student |
+      | student1 | C19 | student |
+      | student1 | C20 | student |
+      | student1 | C21 | student |
+      | student1 | C22 | student |
+      | student1 | C23 | student |
+      | student1 | C24 | student |
+      | student1 | C25 | student |
+    When I log in as "student1"
+    And I click on "[data-control='next']" "css_element" in the "Course overview" "block"
+    And I wait until ".block_myoverview [data-control='next']" "css_element" exists
+    Then the "class" attribute of ".block_myoverview [data-control='next']" "css_element" should not contain "disabled"
+    And the "class" attribute of ".block_myoverview [data-control='previous']" "css_element" should not contain "disabled"
+    And I should see "Course 13" in the "Course overview" "block"
+    And I should see "Course 24" in the "Course overview" "block"
+    But I should not see "Course 12" in the "Course overview" "block"
+    And I should not see "Course 25" in the "Course overview" "block"
+    And I log out
index c483c28..a33a10e 100644 (file)
@@ -52,9 +52,9 @@
             <a href="{{url}}"
                title={{#quote}}{{{name}}}{{/quote}}
                aria-label='{{#str}} ariaeventlistitem, block_timeline, { "name": {{#quote}}{{{name}}}{{/quote}}, "course": {{#quote}}{{{course.fullnamedisplay}}}{{/quote}}, "date": "{{#userdate}} {{timesort}}, {{#str}} strftimedatetime, core_langconfig {{/str}} {{/userdate}}" } {{/str}}'
-            ><h6 class="event-name text-truncate mb-0">{{#quote}}{{{name}}}{{/quote}}</h6></a>
+            ><h6 class="event-name text-truncate mb-0">{{{name}}}</h6></a>
             {{#course.fullnamedisplay}}
-            <small class="text-muted text-truncate mb-0">{{#quote}}{{{course.fullnamedisplay}}}{{/quote}}</small>
+            <small class="text-muted text-truncate mb-0">{{{course.fullnamedisplay}}}</small>
             {{/course.fullnamedisplay}}
             {{#action.actionable}}
             <h6 class="mb-0 pt-2">
index d517d5b..8028476 100644 (file)
Binary files a/course/amd/build/local/activitychooser/dialogue.min.js and b/course/amd/build/local/activitychooser/dialogue.min.js differ
index 740fe3f..c782a84 100644 (file)
Binary files a/course/amd/build/local/activitychooser/dialogue.min.js.map and b/course/amd/build/local/activitychooser/dialogue.min.js.map differ
index 45f8f4f..ea395d6 100644 (file)
Binary files a/course/amd/build/local/activitychooser/selectors.min.js and b/course/amd/build/local/activitychooser/selectors.min.js differ
index 13f9e9f..25b6d79 100644 (file)
Binary files a/course/amd/build/local/activitychooser/selectors.min.js.map and b/course/amd/build/local/activitychooser/selectors.min.js.map differ
index a3bde76..a7c7018 100644 (file)
@@ -216,7 +216,6 @@ const registerListenerEvents = (modal, mappedModules, partialFavourite, footerDa
         const firstChooserOption = sectionChooserOptions.querySelector(selectors.regions.chooserOption.container);
 
         toggleFocusableChooserOption(firstChooserOption, true);
-        initTabsKeyboardNavigation(body);
         initChooserOptionsKeyboardNavigation(body, mappedModules, sectionChooserOptions, modal);
 
         return body;
@@ -235,77 +234,6 @@ const registerListenerEvents = (modal, mappedModules, partialFavourite, footerDa
     .catch();
 };
 
-/**
- * Initialise the keyboard navigation controls for the tab list items.
- *
- * @method initTabsKeyboardNavigation
- * @param {HTMLElement} body Our modal that we are working with
- */
-const initTabsKeyboardNavigation = (body) => {
-    // Set up the tab handlers.
-    const favTabNav = body.querySelector(selectors.regions.favouriteTabNav);
-    const recommendedTabNav = body.querySelector(selectors.regions.recommendedTabNav);
-    const defaultTabNav = body.querySelector(selectors.regions.defaultTabNav);
-    const activityTabNav = body.querySelector(selectors.regions.activityTabNav);
-    const resourceTabNav = body.querySelector(selectors.regions.resourceTabNav);
-    const tabNavArray = [favTabNav, recommendedTabNav, defaultTabNav, activityTabNav, resourceTabNav];
-    tabNavArray.forEach((element) => {
-        return element.addEventListener('keydown', (e) => {
-            // The first visible navigation tab link.
-            const firstLink = e.target.parentElement.querySelector(selectors.elements.visibletabs);
-            // The last navigation tab link. It would always be the default activities tab link.
-            const lastLink = e.target.parentElement.lastElementChild;
-
-            if (e.keyCode === arrowRight) {
-                const nextLink = e.target.nextElementSibling;
-                if (nextLink === null) {
-                    e.target.tabIndex = -1;
-                    firstLink.tabIndex = 0;
-                    firstLink.focus();
-                } else if (nextLink.classList.contains('d-none')) {
-                    e.target.tabIndex = -1;
-                    lastLink.tabIndex = 0;
-                    lastLink.focus();
-                } else {
-                    e.target.tabIndex = -1;
-                    nextLink.tabIndex = 0;
-                    nextLink.focus();
-                }
-            }
-            if (e.keyCode === arrowLeft) {
-                const previousLink = e.target.previousElementSibling;
-                if (previousLink === null) {
-                    e.target.tabIndex = -1;
-                    lastLink.tabIndex = 0;
-                    lastLink.focus();
-                } else if (previousLink.classList.contains('d-none')) {
-                    e.target.tabIndex = -1;
-                    firstLink.tabIndex = 0;
-                    firstLink.focus();
-                } else {
-                    e.target.tabIndex = -1;
-                    previousLink.tabIndex = 0;
-                    previousLink.focus();
-                }
-            }
-            if (e.keyCode === home) {
-                e.target.tabIndex = -1;
-                firstLink.tabIndex = 0;
-                firstLink.focus();
-            }
-            if (e.keyCode === end) {
-                e.target.tabIndex = -1;
-                lastLink.tabIndex = 0;
-                lastLink.focus();
-            }
-            if (e.keyCode === space) {
-                e.preventDefault();
-                e.target.click();
-            }
-        });
-    });
-};
-
 /**
  * Initialise the keyboard navigation controls for the chooser options.
  *
index dd20e75..34e9adb 100644 (file)
@@ -52,10 +52,8 @@ export default {
         help: getDataSelector('region', 'help'),
         modules: getDataSelector('region', 'modules'),
         favouriteTabNav: getDataSelector('region', 'favourite-tab-nav'),
-        recommendedTabNav: getDataSelector('region', 'recommended-tab-nav'),
         defaultTabNav: getDataSelector('region', 'default-tab-nav'),
         activityTabNav: getDataSelector('region', 'activity-tab-nav'),
-        resourceTabNav: getDataSelector('region', 'resources-tab-nav'),
         favouriteTab: getDataSelector('region', 'favourites'),
         recommendedTab: getDataSelector('region', 'recommended'),
         defaultTab: getDataSelector('region', 'default'),
index 8c08a74..50cd5dc 100644 (file)
@@ -36,7 +36,7 @@ if ($ADMIN->fulltree) {
         $student = get_archetype_roles('student');
         $student = reset($student);
         $settings->add(new admin_setting_configselect('enrol_cohort/roleid',
-            get_string('defaultrole', 'role'), '', $student->id, $options));
+            get_string('defaultrole', 'role'), '', $student->id ?? null, $options));
 
         $options = array(
             ENROL_EXT_REMOVED_UNENROL        => get_string('extremovedunenrol', 'enrol'),
index 28c7504..92d0833 100644 (file)
@@ -86,7 +86,11 @@ if ($ADMIN->fulltree) {
         $options = get_default_enrol_roles(context_system::instance());
         $student = get_archetype_roles('student');
         $student = reset($student);
-        $settings->add(new admin_setting_configselect('enrol_database/defaultrole', get_string('defaultrole', 'enrol_database'), get_string('defaultrole_desc', 'enrol_database'), $student->id, $options));
+        $settings->add(new admin_setting_configselect('enrol_database/defaultrole',
+            get_string('defaultrole', 'enrol_database'),
+            get_string('defaultrole_desc', 'enrol_database'),
+            $student->id ?? null,
+            $options));
     }
 
     $settings->add(new admin_setting_configcheckbox('enrol_database/ignorehiddencourses', get_string('ignorehiddencourses', 'enrol_database'), get_string('ignorehiddencourses_desc', 'enrol_database'), 0));
index 2f383b8..50d919e 100644 (file)
@@ -71,7 +71,7 @@ if ($ADMIN->fulltree) {
         $student = get_archetype_roles('student');
         $student = reset($student);
         $settings->add(new admin_setting_configselect('enrol_fee/roleid',
-            get_string('defaultrole', 'enrol_fee'), get_string('defaultrole_desc', 'enrol_fee'), $student->id, $options));
+            get_string('defaultrole', 'enrol_fee'), get_string('defaultrole_desc', 'enrol_fee'), $student->id ?? null, $options));
     }
 
     $settings->add(new admin_setting_configduration('enrol_fee/enrolperiod',
index 2056c19..8dcabcc 100644 (file)
@@ -63,7 +63,7 @@ if ($ADMIN->fulltree) {
         $student = get_archetype_roles('student');
         $student = reset($student);
         $settings->add(new admin_setting_configselect('enrol_manual/roleid',
-            get_string('defaultrole', 'role'), '', $student->id, $options));
+            get_string('defaultrole', 'role'), '', $student->id ?? null, $options));
     }
 
     $options = array(2 => get_string('coursestart'), 3 => get_string('today'), 4 => get_string('now', 'enrol_manual'));
index fc7517f..c74c8e1 100644 (file)
@@ -36,6 +36,6 @@ if ($ADMIN->fulltree) {
         $student = reset($student);
         $settings->add(new admin_setting_configselect_with_advanced('enrol_mnet/roleid',
             get_string('defaultrole', 'role'), '',
-            array('value'=>$student->id, 'adv'=>true), $options));
+            array('value' => $student->id ?? null, 'adv' => true), $options));
     }
 }
index 66e0ae4..d9a3a7f 100644 (file)
@@ -66,7 +66,10 @@ if ($ADMIN->fulltree) {
         $student = get_archetype_roles('student');
         $student = reset($student);
         $settings->add(new admin_setting_configselect('enrol_paypal/roleid',
-            get_string('defaultrole', 'enrol_paypal'), get_string('defaultrole_desc', 'enrol_paypal'), $student->id, $options));
+            get_string('defaultrole', 'enrol_paypal'),
+            get_string('defaultrole_desc', 'enrol_paypal'),
+            $student->id ?? null,
+            $options));
     }
 
     $settings->add(new admin_setting_configduration('enrol_paypal/enrolperiod',
index 6375ea2..cee5d7f 100644 (file)
@@ -79,7 +79,10 @@ if ($ADMIN->fulltree) {
         $student = get_archetype_roles('student');
         $student = reset($student);
         $settings->add(new admin_setting_configselect('enrol_self/roleid',
-            get_string('defaultrole', 'enrol_self'), get_string('defaultrole_desc', 'enrol_self'), $student->id, $options));
+            get_string('defaultrole', 'enrol_self'),
+            get_string('defaultrole_desc', 'enrol_self'),
+            $student->id ?? null,
+            $options));
     }
 
     $settings->add(new admin_setting_configduration('enrol_self/enrolperiod',
diff --git a/install/lang/ky/admin.php b/install/lang/ky/admin.php
new file mode 100644 (file)
index 0000000..8b6b711
--- /dev/null
@@ -0,0 +1,43 @@
+<?php
+// This file is part of Moodle - https://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 <https://www.gnu.org/licenses/>.
+
+/**
+ * Automatically generated strings for Moodle installer
+ *
+ * Do not edit this file manually! It contains just a subset of strings
+ * needed during the very first steps of installation. This file was
+ * generated automatically by export-installer.php (which is part of AMOS
+ * {@link http://docs.moodle.org/dev/Languages/AMOS}) using the
+ * list of strings defined in /install/stringnames.txt.
+ *
+ * @package   installer
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['clianswerno'] = 'Жок';
+$string['cliansweryes'] = 'Ооба';
+$string['cliincorrectvalueerror'] = 'Ката, «{$a->option}» үчүн  «{$a->value}» туура эмес маани';
+$string['cliincorrectvalueretry'] = 'Туура эмес маани, кайрадан аракет кылып көрүңүз';
+$string['clitypevalue'] = 'Маанисин киргизгиле';
+$string['clitypevaluedefault'] = 'Маанисин киргизгиле, алгач белгиленген ({$a}) маанисин  колдонуу үчүн Enter баскычын баскыла';
+$string['cliunknowoption'] = 'Таанылбаган параметрлер {$a}
+
+Сураныч, --help параметрин колдонуңуз';
+$string['cliyesnoprompt'] = 'У киргизиңиз (Ооба дегенди билдирет) же n киргизиңиз (Жок дегенди билдирет)';
+$string['environmentrequireinstall'] = 'Орнотуу жана күйгүзүү зарыл';
+$string['environmentrequireversion'] = '{$a->needed} версиясы талап кылынат, сиз колдонгон версия {$a->current}';
index d427fa4..8a9b9ab 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
-$string['cannotcreatelangdir'] = 'Не може да се креира lang директориумот.';
-$string['cannotcreatetempdir'] = 'Не може да се креира temp директориумот.';
-$string['cannotdownloadcomponents'] = 'Не може да се преземат компонентите.';
-$string['cannotdownloadzipfile'] = 'Не може да се преземи ZIP датотеката.';
-$string['cannotfindcomponent'] = 'Не може да се најде компонентата.';
-$string['cannotsavemd5file'] = 'Не може да се сними md5 датотеката.';
-$string['cannotsavezipfile'] = 'Не може да се сними ZIP датотеката.';
-$string['cannotunzipfile'] = 'Не може да се отпакува датотеката.';
-$string['componentisuptodate'] = 'Компонентата е од последната верзија';
-$string['downloadedfilecheckfailed'] = 'Проверката на преземената датотека не заврши успешно.';
-$string['invalidmd5'] = 'невалидна md5';
-$string['missingrequiredfield'] = 'Некои задолжителни полиња недостасуваат';
-$string['wrongdestpath'] = 'Погрешна дестинација.';
-$string['wrongsourcebase'] = 'Погрешна изворна адреса.';
-$string['wrongzipfilename'] = 'Погрешна ZIP датотека.';
+$string['cannotcreatedboninstall'] = '<p>Не може да се креира базата со податоци.</p>
+<p>Посочената база со податоци не постои и корисникот нема овластувања да ја креира базата со податоци.</p>
+<p>Администраторот на сајтот треба да ја верификува конфигурацијата на базата со податоци.</p>';
+$string['cannotcreatelangdir'] = 'Lang именик не може да се креира';
+$string['cannotcreatetempdir'] = 'Не може да се креира привремена папка';
+$string['cannotdownloadcomponents'] = 'Не можете да ги симнете компонентите';
+$string['cannotdownloadzipfile'] = 'Не можете да спуштите ZIP фајл';
+$string['cannotfindcomponent'] = 'Компонентата не е пронајдена';
+$string['cannotsavemd5file'] = 'Не може да се сними md5 фајлот';
+$string['cannotsavezipfile'] = 'Не може да се сними ZIP фајл';
+$string['cannotunzipfile'] = 'Не можеше да се одзипува фајлот';
+$string['componentisuptodate'] = 'Компонентата е ажурирана.';
+$string['dmlexceptiononinstall'] = '<p>Настана грешка во базата на податоци [{$a->errorcode}].<br />{$a->debuginfo}</p>';
+$string['downloadedfilecheckfailed'] = 'Проверката на преземениот фајл не успеа';
+$string['invalidmd5'] = 'Променливата за проверка беше погрешна - обидете се повторно';
+$string['missingrequiredfield'] = 'Некое задолжително поле недостасува';
+$string['remotedownloaderror'] = '<p>Преземањето на компонентата на вашиот сервер не успеа. Проверете ги поставките за прокси; се препорачува екстензијата PHP cURL.</p>
+<p>Мора да го рачно да го симнете фајлот <a href="{$a->url}">{$a->url}</a>, копирајте го "{$a->dest}" во вашиот сервер и таму отпакувајте го.</p>';
+$string['wrongdestpath'] = 'Погрешна дестинациска патека';
+$string['wrongsourcebase'] = 'Погрешна URL на извор';
+$string['wrongzipfilename'] = 'Погрешно име на ZIP фајл';
index 7f30f96..5f95e7a 100644 (file)
@@ -33,4 +33,4 @@ $string['language'] = 'Јазик';
 $string['moodlelogo'] = 'Moodle лого';
 $string['next'] = 'Следно';
 $string['previous'] = 'Претходно';
-$string['reload'] = 'Ð\92Ñ\87иÑ\82аÑ\98 Ð¿Ð¾Ð²Ñ\82оÑ\80но';
+$string['reload'] = 'Ð\92Ñ\87иÑ\82аÑ\98 Ð¾Ð´Ð½Ð¾Ð²о';
index 062aa99..2f51129 100644 (file)
@@ -39,5 +39,5 @@ $string['cliunknowoption'] = 'Ej identifierade alternativ:
   {$a}
 Vänligen använd --help alternativet.';
 $string['cliyesnoprompt'] = 'skriv in y (betyder ja) eller n (betyder nej)';
-$string['environmentrequireinstall'] = 'är nödvändig att installera/aktivera';
-$string['environmentrequireversion'] = 'version {$a->needed} är nödvändig och du använder {$a->current}';
+$string['environmentrequireinstall'] = 'måste vara installerat och aktiverat';
+$string['environmentrequireversion'] = 'version {$a->needed} krävs och du använder {$a->current}';
index 349aade..6843e1b 100644 (file)
Binary files a/lib/amd/build/paged_content_factory.min.js and b/lib/amd/build/paged_content_factory.min.js differ
index 9c4ea67..e541e28 100644 (file)
Binary files a/lib/amd/build/paged_content_factory.min.js.map and b/lib/amd/build/paged_content_factory.min.js.map differ
index f53bd41..3e20015 100644 (file)
Binary files a/lib/amd/build/permissionmanager.min.js and b/lib/amd/build/permissionmanager.min.js differ
index 8413c76..63e284b 100644 (file)
Binary files a/lib/amd/build/permissionmanager.min.js.map and b/lib/amd/build/permissionmanager.min.js.map differ
index 577ccec..359e16a 100644 (file)
@@ -166,9 +166,11 @@ function(
      * @return {int|array}
      */
     var buildItemsPerPagePagingBarContext = function(itemsPerPage) {
+        var context = [];
+
         if ($.isArray(itemsPerPage)) {
             // Convert the array into a format accepted by the template.
-            var context = itemsPerPage.map(function(num) {
+            context = itemsPerPage.map(function(num) {
                 if (typeof num === 'number') {
                     // If the item is just a plain number then convert it into
                     // an object with value and active keys.
@@ -190,11 +192,12 @@ function(
             if (!activeItems.length) {
                 context[0].active = true;
             }
-
-            return context;
         } else {
-            return itemsPerPage;
+            // Convert the integer into a format accepted by the template.
+            context = [{value: itemsPerPage, active: true}];
         }
+
+        return context;
     };
 
     /**
index 0772104..3f350d3 100644 (file)
@@ -204,7 +204,7 @@ define(['jquery', 'core/config', 'core/notification', 'core/templates', 'core/yu
                 .done(function(content) {
                     panel.set('bodyContent', content);
                     panel.show();
-                    $('div.role_buttons').on('click', 'input', function(e) {
+                    $('div.role_buttons').on('click', 'button', function(e) {
                         var roleid = $(e.currentTarget).data('role-id');
                         changePermissions(row, roleid, action);
                     });
index 4190bf2..0cf405b 100644 (file)
@@ -182,7 +182,7 @@ function cron_run_adhoc_tasks(int $timenow, $keepalive = 0, $checklimits = true)
 
         try {
             $task = \core\task\manager::get_next_adhoc_task(time(), $checklimits);
-        } catch (Exception $e) {
+        } catch (\Throwable $e) {
             if ($adhoclock) {
                 // Release the adhoc task runner lock.
                 $adhoclock->release();
@@ -260,7 +260,7 @@ function cron_run_inner_scheduled_task(\core\task\task_base $task) {
         }
         mtrace('Scheduled task complete: ' . $fullname);
         \core\task\manager::scheduled_task_complete($task);
-    } catch (Exception $e) {
+    } catch (\Throwable $e) {
         if ($DB && $DB->is_transaction_started()) {
             error_log('Database transaction aborted automatically in ' . get_class($task));
             $DB->force_transaction_rollback();
@@ -348,7 +348,7 @@ function cron_run_inner_adhoc_task(\core\task\adhoc_task $task) {
         }
         mtrace("Adhoc task complete: " . get_class($task));
         \core\task\manager::adhoc_task_complete($task);
-    } catch (Exception $e) {
+    } catch (\Throwable $e) {
         if ($DB && $DB->is_transaction_started()) {
             error_log('Database transaction aborted automatically in ' . get_class($task));
             $DB->force_transaction_rollback();
index 8c46237..25e5433 100644 (file)
@@ -2786,11 +2786,12 @@ function xmldb_main_upgrade($oldversion) {
     if ($oldversion < 2020101600.01) {
         // Delete orphaned course_modules_completion rows; these were not deleted properly
         // by remove_course_contents function.
-        $DB->delete_records_subquery('course_modules_completion', 'id', 'id',
-               "SELECT cmc.id
-                  FROM {course_modules_completion} cmc
-             LEFT JOIN {course_modules} cm ON cm.id = cmc.coursemoduleid
-                 WHERE cm.id IS NULL");
+        $DB->delete_records_select('course_modules_completion', "
+                NOT EXISTS (
+                        SELECT 1
+                          FROM {course_modules} cm
+                         WHERE cm.id = {course_modules_completion}.coursemoduleid
+                )");
         upgrade_main_savepoint(true, 2020101600.01);
     }
 
index f1b3e79..365dec7 100644 (file)
         {{^activepagenumber}}
             data-active-page-number="1"
         {{/activepagenumber}}
-        {{#showitemsperpageselector}}
-            {{#itemsperpage}}
-                {{#active}}
-                    data-items-per-page="{{value}}"
-                {{/active}}
-            {{/itemsperpage}}
-        {{/showitemsperpageselector}}
-        {{^showitemsperpageselector}}
-            data-items-per-page="{{itemsperpage}}"
-        {{/showitemsperpageselector}}
+        {{#itemsperpage}}
+            {{#active}}
+                data-items-per-page="{{value}}"
+            {{/active}}
+        {{/itemsperpage}}
         {{#arialabels.paginationnav}}
             aria-label="{{.}}"
         {{/arialabels.paginationnav}}
index dc74d24..da3825c 100644 (file)
 
     Context variables required for this template:
     * confirmation Confirmation text
-    * roles array of role details
+    * roles array of role details. Note: in this array, rolename must have been
+          prepared for output with format_string, or more likely one of the role API functions like role_fix_names.
 
     Example context (json):
     { "message": "Do you really want to remove Non-editing teacher from the list of allowed roles for capability View added and updated modules in recent activity block?",
-      "roles": [{"roleid": 1, "rolename": "manager", "disabled":"disabled"}]}
+      "roles": [{"roleid": 1, "rolename": "Manager", "disabled": "disabled"}]}
 }}
 <div class="popup_content" style="text-align:center;">
     {{message}} <hr/>
     <div class="role_buttons">
         {{#roles}}
-            <input type="button" value="{{rolename}}" class="btn btn-secondary mb-1" data-role-id="{{roleid}}" {{disabled}}/>
+            <button type="button" class="btn btn-secondary mb-1" data-role-id="{{roleid}}" {{disabled}}>{{{rolename}}}</button>
         {{/roles}}
     </div>
 </div>
index d2a1652..b308653 100644 (file)
@@ -27,7 +27,8 @@
     * action
 
     Context variables required for this template:
-    * rolename Name of the role rendered
+    * rolename Name of the role rendered - must have been prepared for output with format_string,
+                                           or more likely one of the role API functions like role_fix_names.
     * roleid Id of the role
     * action WEhich action is done on click
     * spanclass class attribute of span
@@ -43,7 +44,7 @@
      "linkclass": "preventlink",
      "adminurl" : "http://localhost/moodle/admin/"}
 }}
-<span style="display:inline-block;" class="{{spanclass}}">&nbsp;{{rolename}}&nbsp;
+<span style="display:inline-block;" class="{{spanclass}}">&nbsp;{{{rolename}}}&nbsp;
     <a href="{{adminurl}}roles/permissions.php" class="{{linkclass}}" data-role-id="{{roleid}}" data-action="{{action}}">
         {{#icon}}
             {{#pix}}{{icon}}, core, {{iconalt}}{{/pix}}
index 2991753..07f0705 100644 (file)
@@ -40,7 +40,7 @@
 <ul class="nav nav-tabs" role="tablist">
 <!-- First the top most node and immediate children -->
     <li class="nav-item">
-        <a class="nav-link active" href="#link{{node.key}}" data-toggle="tab" role="tab">{{node.text}}</a>
+        <a class="nav-link active" href="#link{{node.key}}" data-toggle="tab" role="tab" aria-selected="true">{{node.text}}</a>
     </li>
 <!-- Now the first level children with sub nodes -->
 {{#node.children}}
@@ -48,7 +48,7 @@
         {{#display}}
             {{^is_short_branch}}
                 <li class="nav-item">
-                    <a class="nav-link" href="#link{{key}}" data-toggle="tab" role="tab">{{text}}</a>
+                    <a class="nav-link" href="#link{{key}}" data-toggle="tab" role="tab" aria-selected="false" tabindex="-1">{{text}}</a>
                 </li>
             {{/is_short_branch}}
         {{/display}}
index 1e8e1e8..17b8a0e 100644 (file)
@@ -328,7 +328,6 @@ EOF;
             // The `before_subsequent_scenario_start_session` function will restart the session instead.
             return;
         }
-        self::$firstjavascriptscenarioseen = true;
 
         $docsurl = behat_command::DOCS_URL;
         $driverexceptionmsg = <<<EOF
@@ -475,6 +474,16 @@ EOF;
         $this->resize_window('medium');
     }
 
+    /**
+     * Mark the first Javascript Scenario as have been seen.
+     *
+     * @BeforeScenario
+     * @param BeforeScenarioScope $scope scope passed by event fired before scenario.
+     */
+    public function mark_first_js_scenario_as_seen(BeforeScenarioScope $scope) {
+        self::$firstjavascriptscenarioseen = true;
+    }
+
     /**
      * Hook to open the site root before the first step in the suite.
      * Yes, this is in a strange location and should be in the BeforeScenario hook, but failures in the test setUp lead
index 111003b..9b7ad6b 100644 (file)
@@ -76,6 +76,7 @@ M.mod_assign.init_grading_table = function(Y) {
         var batchform = Y.one('form.gradingbatchoperationsform');
         if (batchform) {
             batchform.on('submit', function(e) {
+                M.util.js_pending('mod_assign/module.js:batch:submit');
                 checkboxes = Y.all('td.c0 input');
                 var selectedusers = [];
                 checkboxes.each(function(node) {
@@ -102,8 +103,10 @@ M.mod_assign.init_grading_table = function(Y) {
                         confirmmessage = M.util.get_string('batchoperationconfirm' + operation.get('value'), 'assign');
                     }
                     if (!confirm(confirmmessage)) {
+                        M.util.js_complete('mod_assign/module.js:batch:submit');
                         e.preventDefault();
                     }
+                    // Note: Do not js_complete. The page being reloaded will empty it.
                 }
             });
         }
index 7d681e8..843061e 100644 (file)
Binary files a/mod/lti/amd/build/contentitem.min.js and b/mod/lti/amd/build/contentitem.min.js differ
index c9ddf82..198ab86 100644 (file)
Binary files a/mod/lti/amd/build/contentitem.min.js.map and b/mod/lti/amd/build/contentitem.min.js.map differ
index 3d10cb4..74fb54e 100644 (file)
@@ -163,7 +163,8 @@ define(
          */
         var configToVariant = (config) => {
             const variant = {};
-            ['name', 'toolurl', 'securetoolurl', 'instructorcustomparameters', 'icon', 'secureicon', 'launchcontainer'].forEach(
+            ['name', 'toolurl', 'securetoolurl', 'instructorcustomparameters', 'icon', 'secureicon',
+                'launchcontainer', 'lineitemresourceid', 'lineitemtag'].forEach(
                 function(name) {
                     variant[name] = config[name] || '';
                 }
index 44acf5e..06e27af 100644 (file)
@@ -1525,8 +1525,6 @@ function content_item_to_form(object $tool, object $typeconfig, object $item) :
         }
         $config->instructorcustomparameters = implode("\n", $customparameters);
     }
-    // Including a JSON version of the form data to support adding many items in one submit.
-    $config->contentitemjson = json_encode($item);
     return $config;
 }
 
index cc8ebc2..b1f21d9 100644 (file)
@@ -432,7 +432,7 @@ abstract class question_engine {
     public static function get_all_response_file_areas() {
         $variables = array();
         foreach (question_bank::get_all_qtypes() as $qtype) {
-            $variables += $qtype->response_file_areas();
+            $variables = array_merge($variables, $qtype->response_file_areas());
         }
 
         $areas = array();
index b13f152..c0a1c5f 100644 (file)
@@ -823,8 +823,8 @@ class manager {
             }
         }
 
-        // Add all supported block contexts, in a single query for performance.
-        if (!empty($areasbylevel[CONTEXT_BLOCK])) {
+        // Add all supported block contexts for course contexts that user can access, in a single query for performance.
+        if (!empty($areasbylevel[CONTEXT_BLOCK]) && !empty($coursecontextids)) {
             // Get list of all block types we care about.
             $blocklist = [];
             foreach ($areasbylevel[CONTEXT_BLOCK] as $areaid => $searchclass) {
index 0a52f5f..217dcb4 100644 (file)
@@ -791,7 +791,6 @@ class search_manager_testcase extends advanced_testcase {
         $this->assertEquals($contexts['block_html-content'], $limitedcontexts['block_html-content']);
 
         // Get block context ids for the blocks that appear.
-        global $DB;
         $blockcontextids = $DB->get_fieldset_sql('
             SELECT x.id
               FROM {block_instances} bi
@@ -811,6 +810,43 @@ class search_manager_testcase extends advanced_testcase {
         $this->assertCount(1, $contexts['block_html-content']);
     }
 
+    /**
+     * Tests retrieval of users search areas when limiting to a course the user is not enrolled in
+     */
+    public function test_search_users_accesses_limit_non_enrolled_course() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $user = $this->getDataGenerator()->create_user();
+        $this->setUser($user);
+
+        $search = testable_core_search::instance();
+        $search->add_core_search_areas();
+
+        $course = $this->getDataGenerator()->create_course();
+        $context = context_course::instance($course->id);
+
+        // Limit courses to search to only those the user is enrolled in.
+        set_config('searchallavailablecourses', 0);
+
+        $usercontexts = $search->get_areas_user_accesses([$course->id])->usercontexts;
+        $this->assertNotEmpty($usercontexts);
+        $this->assertArrayNotHasKey('core_course-course', $usercontexts);
+
+        // This config ensures the search will also include courses the user can view.
+        set_config('searchallavailablecourses', 1);
+
+        // Allow "Authenticated user" role to view the course without being enrolled in it.
+        $userrole = $DB->get_record('role', ['shortname' => 'user'], '*', MUST_EXIST);
+        role_change_permission($userrole->id, $context, 'moodle/course:view', CAP_ALLOW);
+
+        $usercontexts = $search->get_areas_user_accesses([$course->id])->usercontexts;
+        $this->assertNotEmpty($usercontexts);
+        $this->assertArrayHasKey('core_course-course', $usercontexts);
+        $this->assertEquals($context->id, reset($usercontexts['core_course-course']));
+    }
+
     /**
      * Test get_areas_user_accesses with regard to the 'all available courses' config option.
      *
index 447fd66..f3154ce 100644 (file)
Binary files a/theme/boost/amd/build/aria.min.js and b/theme/boost/amd/build/aria.min.js differ
index 2098af4..9878545 100644 (file)
Binary files a/theme/boost/amd/build/aria.min.js.map and b/theme/boost/amd/build/aria.min.js.map differ
index baacba5..4f9b804 100644 (file)
Binary files a/theme/boost/amd/build/loader.min.js and b/theme/boost/amd/build/loader.min.js differ
index bcde39c..f9e4a1b 100644 (file)
Binary files a/theme/boost/amd/build/loader.min.js.map and b/theme/boost/amd/build/loader.min.js.map differ
index b2b96b0..1acf867 100644 (file)
  * @copyright  2018 Damyon Wiese <damyon@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-define(['jquery', 'core/pending'], function($, Pending) {
-    return {
-        init: function() {
-            // Drop downs from bootstrap don't support keyboard accessibility by default.
-            var focusEnd = false,
-                setFocusEnd = function() {
-                    focusEnd = true;
-                },
-                getFocusEnd = function() {
-                    var result = focusEnd;
-                    focusEnd = false;
-                    return result;
-                };
-
-            // Special handling for "up" keyboard control.
-            $('[data-toggle="dropdown"]').keydown(function(e) {
-                var trigger = e.which || e.keyCode,
-                    expanded;
-
-                // Up key opens the menu at the end.
-                if (trigger == 38) {
-                    // Focus the end of the menu, not the beginning.
-                    setFocusEnd();
-                }
 
-                // Escape key only closes the menu, it doesn't open it.
-                if (trigger == 27) {
-                    expanded = $(e.target).attr('aria-expanded');
-                    e.preventDefault();
-                    if (expanded == "false") {
-                        $(e.target).click();
-                    }
-                }
+import $ from 'jquery';
+import Pending from 'core/pending';
 
-                // Space key or Enter key opens the menu.
-                if (trigger == 32 || trigger == 13) {
-                    // Cancel random scroll.
-                    e.preventDefault();
-                    // Open the menu instead.
-                    $(e.target).click();
-                }
-            });
+/**
+ * Drop downs from bootstrap don't support keyboard accessibility by default.
+ */
+const dropdownFix = () => {
+    let focusEnd = false;
+    const setFocusEnd = () => {
+        focusEnd = true;
+    };
+    const getFocusEnd = () => {
+        const result = focusEnd;
+        focusEnd = false;
+        return result;
+    };
+
+    // Special handling for "up" keyboard control.
+    document.addEventListener('keydown', e => {
+        if (e.target.matches('[data-toggle="dropdown"]')) {
+            const trigger = e.key;
+
+            // Up key opens the menu at the end.
+            if (trigger == 'ArrowUp') {
+                // Focus the end of the menu, not the beginning.
+                setFocusEnd();
+            }
+
+            // Space key or Enter key opens the menu.
+            if (trigger == ' ' || trigger == 'Enter') {
+                // Cancel random scroll.
+                e.preventDefault();
+                // Open the menu instead.
+                e.target.click();
+            }
+        }
+    });
+
+    // Special handling for navigation keys when menu is open.
+    const shiftFocus = element => {
+        const delayedFocus = pendingPromise => {
+            element.focus();
+            pendingPromise.resolve();
+        };
+        setTimeout(delayedFocus, 50, new Pending('core/aria:delayed-focus'));
+    };
+
+    $('.dropdown').on('shown.bs.dropdown', e => {
+        // We need to focus on the first menuitem.
+        const menu = e.target.querySelector('[role="menu"]');
+        let menuItems = false;
+        let foundMenuItem = false;
 
-            // Special handling for navigation keys when menu is open.
-            var shiftFocus = function(element) {
-                var delayedFocus = function(pendingPromise) {
-                    $(this).focus();
-                    pendingPromise.resolve();
-                }.bind(element);
-                setTimeout(delayedFocus, 50, new Pending('core/aria:delayed-focus'));
-            };
-
-            $('.dropdown').on('shown.bs.dropdown', function(e) {
-                // We need to focus on the first menuitem.
-                var menu = $(e.target).find('[role="menu"]'),
-                    menuItems = false,
-                    foundMenuItem = false;
-
-                if (menu) {
-                    menuItems = $(menu).find('[role="menuitem"]');
+        if (menu) {
+            menuItems = menu.querySelectorAll('[role="menuitem"]');
+        }
+        if (menuItems && menuItems.length > 0) {
+            if (getFocusEnd()) {
+                foundMenuItem = menuItems[menuItems.length - 1];
+            } else {
+                // The first menu entry, pretty reasonable.
+                foundMenuItem = menuItems[0];
+            }
+        }
+        if (foundMenuItem) {
+            shiftFocus(foundMenuItem);
+        }
+    });
+    // Search for menu items by finding the first item that has
+    // text starting with the typed character (case insensitive).
+    document.addEventListener('keypress', e => {
+        if (e.target.matches('.dropdown [role="menu"] [role="menuitem"]')) {
+            const menu = e.target.closest('[role="menu"]');
+            if (!menu) {
+                return;
+            }
+            const menuItems = menu.querySelectorAll('[role="menuitem"]');
+            if (!menuItems) {
+                return;
+            }
+
+            const trigger = e.key.toLowerCase();
+
+            for (let i = 0; i < menuItems.length; i++) {
+                const item = menuItems[i];
+                const itemText = item.text.trim().toLowerCase();
+                if (itemText.indexOf(trigger) == 0) {
+                    shiftFocus(item);
+                    break;
                 }
-                if (menuItems && menuItems.length > 0) {
-                    if (getFocusEnd()) {
-                        foundMenuItem = menuItems[menuItems.length - 1];
-                    } else {
-                        // The first menu entry, pretty reasonable.
-                        foundMenuItem = menuItems[0];
+            }
+        }
+    });
+
+    // Keyboard navigation for arrow keys, home and end keys.
+    document.addEventListener('keydown', e => {
+        if (e.target.matches('.dropdown [role="menu"] [role="menuitem"]')) {
+            const trigger = e.key;
+            let next = false;
+            const menu = e.target.closest('[role="menu"]');
+
+            if (!menu) {
+                return;
+            }
+            const menuItems = menu.querySelectorAll('[role="menuitem"]');
+            if (!menuItems) {
+                return;
+            }
+            // Down key.
+            if (trigger == 'ArrowDown') {
+                for (let i = 0; i < menuItems.length - 1; i++) {
+                    if (menuItems[i] == e.target) {
+                        next = menuItems[i + 1];
+                        break;
                     }
                 }
-                if (foundMenuItem) {
-                    shiftFocus(foundMenuItem);
-                }
-            });
-            // Search for menu items by finding the first item that has
-            // text starting with the typed character (case insensitive).
-            $('.dropdown [role="menu"] [role="menuitem"]').keypress(function(e) {
-                var trigger = String.fromCharCode(e.which || e.keyCode),
-                    menu = $(e.target).closest('[role="menu"]'),
-                    i = 0,
-                    menuItems = false,
-                    item,
-                    itemText;
-
-                if (!menu) {
-                    return;
-                }
-                menuItems = $(menu).find('[role="menuitem"]');
-                if (!menuItems) {
-                    return;
+                if (!next) {
+                    // Wrap to first item.
+                    next = menuItems[0];
                 }
 
-                trigger = trigger.toLowerCase();
-                for (i = 0; i < menuItems.length; i++) {
-                    item = $(menuItems[i]);
-                    itemText = item.text().trim().toLowerCase();
-                    if (itemText.indexOf(trigger) == 0) {
-                        shiftFocus(item);
+            } else if (trigger == 'ArrowUp') {
+                // Up key.
+                for (let i = 1; i < menuItems.length; i++) {
+                    if (menuItems[i] == e.target) {
+                        next = menuItems[i - 1];
                         break;
                     }
                 }
-            });
-
-            // Keyboard navigation for arrow keys, home and end keys.
-            $('.dropdown [role="menu"] [role="menuitem"]').keydown(function(e) {
-                var trigger = e.which || e.keyCode,
-                    next = false,
-                    menu = $(e.target).closest('[role="menu"]'),
-                    i = 0,
-                    menuItems = false;
-                if (!menu) {
-                    return;
-                }
-                menuItems = $(menu).find('[role="menuitem"]');
-                if (!menuItems) {
-                    return;
+                if (!next) {
+                    // Wrap to last item.
+                    next = menuItems[menuItems.length - 1];
                 }
-                // Down key.
-                if (trigger == 40) {
-                    for (i = 0; i < menuItems.length - 1; i++) {
-                        if (menuItems[i] == e.target) {
-                            next = menuItems[i + 1];
-                            break;
-                        }
-                    }
-                    if (!next) {
-                        // Wrap to first item.
-                        next = menuItems[0];
-                    }
 
-                } else if (trigger == 38) {
-                    // Up key.
-                    for (i = 1; i < menuItems.length; i++) {
-                        if (menuItems[i] == e.target) {
-                            next = menuItems[i - 1];
-                            break;
-                        }
-                    }
-                    if (!next) {
-                        // Wrap to last item.
-                        next = menuItems[menuItems.length - 1];
-                    }
+            } else if (trigger == 'Home') {
+                // Home key.
+                next = menuItems[0];
 
-                } else if (trigger == 36) {
-                    // Home key.
-                    next = menuItems[0];
+            } else if (trigger == 'End') {
+                // End key.
+                next = menuItems[menuItems.length - 1];
+            }
+            // Variable next is set if we do want to act on the keypress.
+            if (next) {
+                e.preventDefault();
+                shiftFocus(next);
+            }
+            return;
+        }
+    });
 
-                } else if (trigger == 35) {
-                    // End key.
-                    next = menuItems[menuItems.length - 1];
-                }
-                // Variable next is set if we do want to act on the keypress.
-                if (next) {
-                    e.preventDefault();
-                    shiftFocus(next);
-                }
-                return;
-            });
-            $('.dropdown').on('hidden.bs.dropdown', function(e) {
-                // We need to focus on the menu trigger.
-                var trigger = $(e.target).find('[data-toggle="dropdown"]');
-                if (trigger) {
-                    shiftFocus(trigger);
-                }
+    $('.dropdown').on('hidden.bs.dropdown', e => {
+        // We need to focus on the menu trigger.
+        const trigger = e.target.querySelector('[data-toggle="dropdown"]');
+        if (trigger) {
+            shiftFocus(trigger);
+        }
+    });
+};
+
+/**
+ * After page load, focus on any element with special autofocus attribute.
+ */
+const autoFocus = () => {
+    window.addEventListener("load", () => {
+        const alerts = document.querySelectorAll('[data-aria-autofocus="true"][role="alert"]');
+        Array.prototype.forEach.call(alerts, autofocusElement => {
+            // According to the specification an role="alert" region is only read out on change to the content
+            // of that region.
+            autofocusElement.innerHTML += ' ';
+            autofocusElement.removeAttribute('data-aria-autofocus');
+        });
+    });
+};
+
+/**
+ * Changes the focus to the correct tab based on the key that is pressed.
+ * @param {KeyboardEvent} e
+ */
+const updateTabFocus = e => {
+    const tabList = e.target.closest('[role="tablist"]');
+    const vertical = tabList.getAttribute('aria-orientation') == 'vertical';
+    const rtl = window.right_to_left();
+    const arrowNext = vertical ? 'ArrowDown' : (rtl ? 'ArrowLeft' : 'ArrowRight');
+    const arrowPrevious = vertical ? 'ArrowUp' : (rtl ? 'ArrowRight' : 'ArrowLeft');
+    const tabs = Array.prototype.filter.call(
+        tabList.querySelectorAll('[role="tab"]'),
+        tab => getComputedStyle(tab).display !== 'none'); // We only work with the visible tabs.
+
+    for (let i = 0; i < tabs.length; i++) {
+        tabs[i].index = i;
+    }
+
+    switch (e.key) {
+        case arrowNext:
+            e.preventDefault();
+            if (e.target.index !== undefined && tabs[e.target.index + 1]) {
+                tabs[e.target.index + 1].focus();
+            } else {
+                tabs[0].focus();
+            }
+            break;
+        case arrowPrevious:
+            e.preventDefault();
+            if (e.target.index !== undefined && tabs[e.target.index - 1]) {
+                tabs[e.target.index - 1].focus();
+            } else {
+                tabs[tabs.length - 1].focus();
+            }
+            break;
+        case 'Home':
+            e.preventDefault();
+            tabs[0].focus();
+            break;
+        case 'End':
+            e.preventDefault();
+            tabs[tabs.length - 1].focus();
+            break;
+        case 'Enter':
+        case ' ':
+            e.preventDefault();
+            $(e.target).tab('show');
+            tabs.forEach(tab => {
+                tab.tabIndex = -1;
             });
+            e.target.tabIndex = 0;
+    }
+};
+
+/**
+ * Fix accessibility issues regarding tab elements focus and their tab order in Bootstrap navs.
+ */
+const tabElementFix = () => {
+    document.addEventListener('keydown', e => {
+        if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End', 'Enter', ' '].includes(e.key)) {
+            if (e.target.matches('[role="tablist"] [role="tab"]')) {
+                updateTabFocus(e);
+            }
+        }
+    });
 
-            // After page load, focus on any element with special autofocus attribute.
-            window.addEventListener("load", () => {
-                const alerts = document.querySelectorAll('[data-aria-autofocus="true"][role="alert"]');
-                Array.prototype.forEach.call(alerts, autofocusElement => {
-                    // According to the specification an role="alert" region is only read out on change to the content
-                    // of that region.
-                    autofocusElement.innerHTML += ' ';
-                    autofocusElement.removeAttribute('data-aria-autofocus');
-                });
+    document.addEventListener('click', e => {
+        if (e.target.matches('[role="tablist"] [role="tab"]')) {
+            const tabs = e.target.closest('[role="tablist"]').querySelectorAll('[role="tab"]');
+            e.preventDefault();
+            $(e.target).tab('show');
+            tabs.forEach(tab => {
+                tab.tabIndex = -1;
             });
+            e.target.tabIndex = 0;
         }
-    };
-});
+    });
+};
+
+export const init = () => {
+    dropdownFix();
+    autoFocus();
+    tabElementFix();
+};
index a050c6f..f92a379 100644 (file)
@@ -25,7 +25,7 @@
  */
 
 import $ from 'jquery';
-import Aria from './aria';
+import * as Aria from './aria';
 import Bootstrap from './bootstrap/index';
 import Pending from 'core/pending';
 import Scroll from './scroll';
@@ -43,9 +43,12 @@ const rememberTabs = () => {
             location.hash = hash;
         }
     });
-    var hash = window.location.hash;
+    const hash = window.location.hash;
     if (hash) {
-       $('.nav-link[href="' + hash + '"]').tab('show');
+        const tab = document.querySelector('.nav-link[href="' + hash + '"]');
+        if (tab) {
+            tab.click();
+        }
     }
 };
 
@@ -83,6 +86,9 @@ const pendingPromise = new Pending('theme_boost/loader:init');
 // Add pending promise event listeners to relevant Bootstrap custom events.
 setupBootstrapPendingChecks();
 
+// Setup Aria helpers for Bootstrap features.
+Aria.init();
+
 // Remember the last visited tabs.
 rememberTabs();
 
@@ -98,9 +104,6 @@ enableTooltips();
 // Disables flipping the dropdowns up and getting hidden behind the navbar.
 $.fn.dropdown.Constructor.Default.flip = false;
 
-// Setup Aria helpers for Bootstrap features.
-Aria.init();
-
 pendingPromise.resolve();
 
 export {
index ec25e9f..162314a 100644 (file)
@@ -39,7 +39,9 @@
 <ul class="nav nav-tabs" role="tablist">
     {{#tabs}}
         <li class="nav-item">
-            <a href="#{{name}}" class="nav-link {{#active}}active{{/active}}" data-toggle="tab" role="tab">{{displayname}}</a>
+            <a href="#{{name}}" class="nav-link {{#active}}active{{/active}}" data-toggle="tab" role="tab"
+                {{#active}}aria-selected="true"{{/active}}
+                {{^active}}aria-selected="false" tabindex="-1"{{/active}}>{{displayname}}</a>
         </li>
     {{/tabs}}
 </ul>
index 344f840..53bc95e 100644 (file)
@@ -29,9 +29,9 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2020110900.04;              // 20201109      = branching date YYYYMMDD - do not modify!
+$version  = 2020110900.05;              // 20201109      = branching date YYYYMMDD - do not modify!
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.
-$release  = '3.10+ (Build: 20201204)';// Human-friendly version name
+$release  = '3.10+ (Build: 20201211)';// Human-friendly version name
 $branch   = '310';                      // This version's branch.
 $maturity = MATURITY_STABLE;             // This version's maturity level.