Merge branch 'master_MDL-70520' of https://github.com/golenkovm/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Wed, 6 Jan 2021 23:17:08 +0000 (00:17 +0100)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Wed, 6 Jan 2021 23:17:08 +0000 (00:17 +0100)
78 files changed:
admin/settings/server.php
admin/tool/dataprivacy/tests/behat/manage_purposes.feature
admin/tool/lp/tests/behat/plan_crud.feature
admin/tool/templatelibrary/amd/build/search.min.js
admin/tool/templatelibrary/amd/build/search.min.js.map
admin/tool/templatelibrary/amd/src/search.js
admin/tool/templatelibrary/classes/output/list_templates_page.php
admin/tool/templatelibrary/templates/list_templates_page.mustache
analytics/tests/behat/manage_models.feature
calendar/classes/external/calendar_event_exporter.php
course/management.php
course/tests/behat/coursetags.feature
course/tests/behat/rename_roles.feature
enrol/fee/classes/plugin.php
enrol/manual/tests/behat/quickenrolment.feature
grade/grading/form/rubric/styles.css
group/tests/behat/create_groups.feature
group/tests/behat/group_description.feature
install/lang/sv/error.php
install/lang/sv/install.php
lang/en/admin.php
lang/en/moodle.php
lib/adminlib.php
lib/amd/build/form-autocomplete.min.js
lib/amd/build/form-autocomplete.min.js.map
lib/amd/build/network.min.js
lib/amd/build/network.min.js.map
lib/amd/src/form-autocomplete.js
lib/amd/src/network.js
lib/behat/behat_field_manager.php
lib/behat/form_field/behat_form_autocomplete.php
lib/classes/oauth2/client.php
lib/classes/task/manager.php
lib/form/templates/element-advcheckbox.mustache
lib/form/templates/element-checkbox.mustache
lib/form/templates/element-radio.mustache
lib/form/templates/element-template.mustache
lib/moodlelib.php
lib/outputcomponents.php
lib/outputrenderers.php
lib/outputrequirementslib.php
lib/setup.php
lib/templates/form_autocomplete_input.mustache
lib/templates/search_input_auto.mustache
lib/templates/search_input_navbar.mustache
lib/tests/behat/behat_forms.php
lib/tests/outputcomponents_test.php
lib/tests/scheduled_task_test.php
mod/data/export_form.php
mod/data/lang/en/data.php
mod/data/lang/en/deprecated.txt
mod/data/tests/behat/view_entries.feature
mod/feedback/tests/behat/coursemapping.feature
mod/forum/tests/behat/advanced_search.feature
mod/lti/classes/local/ltiopenid/registration_helper.php
mod/lti/locallib.php
mod/lti/openid-configuration.php
mod/lti/tests/openidregistration_test.php
payment/classes/helper.php
question/type/ddmarker/amd/build/question.min.js
question/type/ddmarker/amd/build/question.min.js.map
question/type/ddmarker/amd/src/question.js
report/competency/tests/behat/breakdown_by_activity.feature
theme/boost/config.php
theme/boost/layout/embedded.php
theme/boost/scss/moodle/blocks.scss
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/question.scss
theme/boost/style/moodle.css
theme/boost/templates/embedded.mustache
theme/classic/style/moodle.css
theme/upgrade.txt
user/tests/behat/edit_user_roles.feature
user/tests/behat/filter_participants.feature
user/tests/behat/filter_participants_showall.feature
user/tests/behat/full_name_display.feature
user/tests/behat/view_participants_groups.feature
version.php

index 11541bb..beb70f3 100644 (file)
@@ -73,6 +73,22 @@ if ($hassiteconfig) {
     $temp->add(new admin_setting_configduration('sessiontimeout', new lang_string('sessiontimeout', 'admin'),
         new lang_string('configsessiontimeout', 'admin'), 8 * 60 * 60));
 
+    $sessiontimeoutwarning = new admin_setting_configduration('sessiontimeoutwarning',
+        new lang_string('sessiontimeoutwarning', 'admin'),
+        new lang_string('configsessiontimeoutwarning', 'admin'), 20 * 60);
+
+    $sessiontimeoutwarning->set_validate_function(function(int $value): string {
+        global $CFG;
+        // Check sessiontimeoutwarning is less than sessiontimeout.
+        if ($CFG->sessiontimeout <= $value) {
+            return get_string('configsessiontimeoutwarningcheck', 'admin');
+        } else {
+            return '';
+        }
+    });
+
+    $temp->add($sessiontimeoutwarning);
+
     $temp->add(new admin_setting_configtext('sessioncookie', new lang_string('sessioncookie', 'admin'),
         new lang_string('configsessioncookie', 'admin'), '', PARAM_ALPHANUM));
     $temp->add(new admin_setting_configtext('sessioncookiepath', new lang_string('sessioncookiepath', 'admin'),
index 6619bda..ff4b208 100644 (file)
@@ -10,16 +10,12 @@ Feature: Manage data storage purposes
     And I open the action menu in "region-main" "region"
     And I choose "Purposes" in the open action menu
     And I press "Add purpose"
-    And I set the field "Name" to "Purpose 1"
-    And I set the field "Description" to "Purpose 1 description"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Lawful bases" "form_row"
-    And I click on "Contract (GDPR Art. 6.1(b))" "list_item"
-    And I click on "Legal obligation (GDPR Art 6.1(c))" "list_item"
-    And I press the escape key
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Sensitive personal data processing reasons" "form_row"
-    And I click on "Explicit consent (GDPR Art. 9.2(a))" "list_item"
-    And I press the escape key
-    And I set the field "retentionperiodnumber" to "2"
+    And I set the following fields to these values:
+      | Name                                       | Purpose 1                                                      |
+      | Description                                | Purpose 1 description                                          |
+      | Lawful bases                               | Contract (GDPR Art. 6.1(b)),Legal obligation (GDPR Art 6.1(c)) |
+      | Sensitive personal data processing reasons | Explicit consent (GDPR Art. 9.2(a))                            |
+      | retentionperiodnumber                      | 2                                                              |
     When I press "Save"
     Then I should see "Purpose 1" in the "List of data purposes" "table"
     And I should see "Contract (GDPR Art. 6.1(b))" in the "Purpose 1" "table_row"
@@ -31,14 +27,13 @@ Feature: Manage data storage purposes
   Scenario: Update a data storage purpose
     Given I open the action menu in "Purpose 1" "table_row"
     And I choose "Edit" in the open action menu
-    And I set the field "Name" to "Purpose 1 edited"
-    And I set the field "Description" to "Purpose 1 description edited"
-    And I click on "Legal obligation (GDPR Art 6.1(c))" "text" in the ".form-autocomplete-selection" "css_element"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Lawful bases" "form_row"
-    And I click on "Vital interests (GDPR Art. 6.1(d))" "list_item"
-    And I press the escape key
-    And I set the field "retentionperiodnumber" to "3"
-    And I click on "protected" "checkbox"
+    And I set the following fields to these values:
+      | Name                                       | Purpose 1 edited                                                |
+      | Description                                | Purpose 1 description edited                                    |
+      | Lawful bases                               | Contract (GDPR Art. 6.1(b)), Vital interests (GDPR Art. 6.1(d)) |
+      | Sensitive personal data processing reasons | Explicit consent (GDPR Art. 9.2(a))                             |
+      | retentionperiodnumber                      | 3                                                               |
+      | protected                                  | 1                                                               |
     When I press "Save changes"
     Then I should see "Purpose 1 edited" in the "List of data purposes" "table"
     And I should see "Purpose 1 description edited" in the "Purpose 1 edited" "table_row"
index 55c085a..9f3dc07 100644 (file)
@@ -53,9 +53,7 @@ Feature: Manage plearning plan
     And I follow "Home"
     And I navigate to "Competencies > Learning plan templates" in site administration
     And I click on ".template-cohorts" "css_element" in the "Science template cohort" "table_row"
-    And I click on ".form-autocomplete-downarrow" "css_element"
-    And I click on "cohort plan" item in the autocomplete list
-    And I press the escape key
+    And I set the field "Select cohorts to sync" to "cohort plan"
     When I click on "Add cohorts" "button"
     Then I should see "2 learning plans were created."
     And I follow "Learning plan templates"
index 74f8c50..9645fa6 100644 (file)
Binary files a/admin/tool/templatelibrary/amd/build/search.min.js and b/admin/tool/templatelibrary/amd/build/search.min.js differ
index 0245bb8..c16fd85 100644 (file)
Binary files a/admin/tool/templatelibrary/amd/build/search.min.js.map and b/admin/tool/templatelibrary/amd/build/search.min.js.map differ
index 9caf344..1865457 100644 (file)
@@ -52,9 +52,8 @@ define(['jquery', 'core/ajax', 'core/log', 'core/notification', 'core/templates'
         } else {
             $('[data-region="list-templates"] [data-action="clearsearch"]').addClass('d-none');
         }
-        // Trigger the search.
-        document.location.hash = searchStr;
 
+        // Trigger the search.
         ajax.call([
             {methodname: 'tool_templatelibrary_list_templates',
               args: {component: componentStr, search: searchStr, themename: themename},
@@ -96,7 +95,6 @@ define(['jquery', 'core/ajax', 'core/log', 'core/notification', 'core/templates'
         $(this).addClass('d-none');
     });
 
-    $('[data-region="input"]').val(document.location.hash.replace('#', ''));
     refreshSearch(config.theme);
     return {};
 });
index ebd4500..71035d8 100644 (file)
@@ -40,6 +40,22 @@ use tool_templatelibrary\api;
  */
 class list_templates_page implements renderable, templatable {
 
+    /** @var string $component The currently selected component */
+    protected $component;
+    /** @var string $search The current search */
+    protected $search;
+
+    /**
+     * Template page constructor
+     *
+     * @param string $component
+     * @param string $search
+     */
+    public function __construct(string $component = '', string $search = '') {
+        $this->component = $component;
+        $this->search = $search;
+    }
+
     /**
      * Export this data so it can be used as the context for a mustache template.
      *
@@ -75,6 +91,7 @@ class list_templates_page implements renderable, templatable {
             $components[$type]->plugins[$component] = (object) [
                 'name' => $pluginname,
                 'component' => $component,
+                'selected' => ($component === $this->component),
             ];
         }
 
@@ -87,6 +104,7 @@ class list_templates_page implements renderable, templatable {
 
         return (object) [
             'allcomponents' => array_values($components),
+            'search' => $this->search,
         ];
     }
 }
index 71d8042..0accf0a 100644 (file)
         {{/label}}
 
         {{$element}}
-            <select id="selectcomponent" class="form-control" data-field="component">
+            <select id="selectcomponent" name="component" class="form-control" data-field="component">
                 <option value="">{{#str}}all, tool_templatelibrary{{/str}}</option>
                 {{#allcomponents}}
                     <optgroup label="{{type}}">
                         {{#plugins}}
-                            <option value="{{component}}">{{name}}</option>
+                            <option value="{{component}}" {{#selected}}selected{{/selected}}>{{name}}</option>
                         {{/plugins}}
                     </optgroup>
                 {{/allcomponents}}
     {{< core_form/element-template }}
         {{$element}}
             {{< core/search_input_auto }}
-                {{$label}}{{{ searchstring }}}{{/label}}
-                {{$placeholder}}{{#str}}
-                    search, core
-                {{/str}}{{/placeholder}}
+                {{$label}}
+                    {{#str}} search, tool_templatelibrary {{/str}}
+                {{/label}}
+                {{$value}}{{ search }}{{/value}}
             {{/ core/search_input_auto }}
         {{/element}}
     {{/ core_form/element-template }}
index 816a253..d68ed78 100644 (file)
@@ -55,10 +55,7 @@ Feature: Manage analytics models
     And I choose "Create model" in the open action menu
     And I set the field "Enabled" to "Enable"
     And I select "__core_course__analytics__target__course_completion" from the "target" singleselect
-    And I open the autocomplete suggestions list
-    And I click on "Read actions amount" item in the autocomplete list
-    And I open the autocomplete suggestions list
-    And I click on "Any write action in the course" item in the autocomplete list
+    And I set the field "Indicators" to "Read actions amount, Any write action in the course"
     And I select "__core__analytics__time_splitting__single_range" from the "timesplitting" singleselect
     And I press "Save changes"
     Then I should see "No predictions available yet" in the "Students at risk of not meeting the course completion conditions" "table_row"
index e943e9c..bfa2368 100644 (file)
@@ -116,7 +116,11 @@ class calendar_event_exporter extends event_exporter_base {
         $values['islastday'] = false;
         $today = $this->related['type']->timestamp_to_date_array($this->related['today']);
 
-        $values['popupname'] = $this->event->get_name();
+        if ($hascourse) {
+            $values['popupname'] = external_format_string($this->event->get_name(), \context_course::instance($course->id), true);
+        } else {
+            $values['popupname'] = external_format_string($this->event->get_name(), \context_system::instance(), true);
+        }
 
         $times = $this->event->get_times();
         if ($duration = $times->get_duration()) {
index 5afc685..94e02aa 100644 (file)
@@ -455,7 +455,7 @@ if ($viewmode === 'default' || $viewmode === 'combined') {
     }
 }
 if ($viewmode === 'default' || $viewmode === 'combined') {
-    $class .= ' viewmode-cobmined';
+    $class .= ' viewmode-combined';
 } else {
     $class .= ' viewmode-'.$viewmode;
 }
index c2655fc..3506893 100644 (file)
@@ -38,7 +38,7 @@ Feature: Tagging courses
     And I expand all fieldsets
     Then "Mathematics" "autocomplete_suggestions" should exist
     And I set the following fields to these values:
-      | Tags | Algebra |
+      | Tags | Mathematics, Algebra |
     And I press "Save and display"
     And I am on "Course 2" course homepage
     And I navigate to "Edit settings" in current page administration
@@ -75,7 +75,7 @@ Feature: Tagging courses
     And I navigate to "Course tags" in current page administration
     Then I should see "Mathematics" in the ".form-autocomplete-selection" "css_element"
     And I set the following fields to these values:
-      | Tags | Algebra |
+      | Tags | Mathematics, Algebra |
     And I press "Save changes"
     And I am on "Course 2" course homepage
     And I navigate to "Course tags" in current page administration
index 1b3c581..00f7c81 100644 (file)
@@ -31,9 +31,11 @@ Feature: Rename roles within a course
     And "Learner" "button" should exist
     And I navigate to course participants
     And I set the field "type" in the "Filter 1" "fieldset" to "Roles"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset"
-    And I should see "Tutor (Non-editing teacher)" in the ".form-autocomplete-suggestions" "css_element"
+    And I open the autocomplete suggestions list in the "Filter 1" "fieldset"
     And I should see "Learner (Student)" in the ".form-autocomplete-suggestions" "css_element"
+    And I press the escape key
+    And I set the field "Type or select..." in the "Filter 1" "fieldset" to "Tutor (Non-editing teacher)"
+
     And I click on "Student 1's role assignments" "link"
     And I click on ".form-autocomplete-downarrow" "css_element" in the "Student 1" "table_row"
     And "Tutor (Non-editing teacher)" "autocomplete_suggestions" should exist
@@ -53,6 +55,6 @@ Feature: Rename roles within a course
     And "Learner" "button" should not exist
     And I navigate to course participants
     And I set the field "type" in the "Filter 1" "fieldset" to "Roles"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset"
+    And I open the autocomplete suggestions list in the "Filter 1" "fieldset"
     And I should see "Non-editing teacher" in the ".form-autocomplete-suggestions" "css_element"
     And I should see "Student" in the ".form-autocomplete-suggestions" "css_element"
index 78cbbcd..549247c 100644 (file)
@@ -45,6 +45,10 @@ class enrol_fee_plugin extends enrol_plugin {
             $currencies[$c] = new lang_string($c, 'core_currencies');
         }
 
+        uasort($currencies, function($a, $b) {
+            return strcmp($a, $b);
+        });
+
         return $currencies;
     }
 
index cd3c979..564f69f 100644 (file)
@@ -130,18 +130,17 @@ Feature: Teacher can search and enrol users one by one into the course
   Scenario: Searching for a non-existing user
     Given I navigate to course participants
     And I press "Enrol users"
-    And I set the field "Select users" to "qwertyuiop"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Select users" "form_row"
+    And I click on "Select users" "field"
+    And I type "qwertyuiop"
     Then I should see "No suggestions"
 
   @javascript
   Scenario: If there are less than 100 matching users, all are displayed for selection
     Given I navigate to course participants
     And I press "Enrol users"
-    When I set the field "Select users" to "example.com"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Select users" "form_row"
-    And I click on "Student 099" item in the autocomplete list
-    Then I should see "Student 099"
+    When I click on "Select users" "field"
+    And I type "example.com"
+    Then "Student 099" "autocomplete_suggestions" should exist
 
   @javascript
   Scenario: If there are more than 100 matching users, inform there are too many.
@@ -151,8 +150,8 @@ Feature: Teacher can search and enrol users one by one into the course
       | student101  | Student   | 101      | student101@example.com  |
     And I navigate to course participants
     And I press "Enrol users"
-    When I set the field "Select users" to "example.com"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Select users" "form_row"
+    When I click on "Select users" "field"
+    And I type "example.com"
     Then I should see "Too many users (>100) to show"
 
   @javascript
@@ -161,8 +160,8 @@ Feature: Teacher can search and enrol users one by one into the course
       | maxusersperpage | 5 |
     And I navigate to course participants
     And I press "Enrol users"
-    When I set the field "Select users" to "student00"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Select users" "form_row"
+    When I click on "Select users" "field"
+    And I type "student00"
     Then I should see "Too many users (>5) to show"
 
   @javascript
@@ -177,18 +176,18 @@ Feature: Teacher can search and enrol users one by one into the course
     When I am on "Course 001" course homepage
     Then I navigate to course participants
     And I press "Enrol users"
-    When I set the field "Select users" to "student100@example.com"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Select users" "form_row"
+    And I click on "Select users" "field"
+    And I type "student100@example.com"
     Then I should see "student100@example.com, CITY1, GB, 1234567892, 1234567893, ABC1, ABC2"
     # Remove identity field in setting User policies
     And the following config values are set as admin:
       | showuseridentity | idnumber,email,phone1,phone2,department,institution |
-    When I am on "Course 001" course homepage
+    And I am on "Course 001" course homepage
     And I navigate to course participants
     And I press "Enrol users"
-    When I set the field "Select users" to "student100@example.com"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Select users" "form_row"
-    Then I should see "student100@example.com, 1234567892, 1234567893, ABC1, ABC2"
+    And I click on "Select users" "field"
+    And I type "student100@example.com"
+    And I should see "student100@example.com, 1234567892, 1234567893, ABC1, ABC2"
 
 # The following tests are commented out as a result of MDL-66339.
 #  @javascript
index 460fbb9..e45913b 100644 (file)
 }
 
 .gradingform_rubric .criterion .description {
-    width: 150px;
+    min-width: 150px;
     font-weight: bold;
 }
 
index 7e50089..2b4a65a 100644 (file)
@@ -50,15 +50,12 @@ Feature: Organize students into groups
     And the "members" select box should not contain "Student 1 (student1@example.com)"
     And I navigate to course participants
     And I set the field "type" in the "Filter 1" "fieldset" to "Groups"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset"
-    And I click on "Group 1" "list_item"
+    And I set the field "Type or select..." in the "Filter 1" "fieldset" to "Group 1"
     And I click on "Apply filters" "button"
     And I should see "Student 0"
     And I should see "Student 1"
     And I should not see "Student 2"
-    And I click on "Group 1" "autocomplete_selection"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset"
-    And I click on "Group 2" "list_item"
+    And I set the field "Type or select..." in the "Filter 1" "fieldset" to "Group 2"
     And I click on "Apply filters" "button"
     And I should see "Student 2"
     And I should see "Student 3"
index e63ee05..087fecb 100644 (file)
@@ -46,8 +46,7 @@ Feature: The description of a group can be viewed by students and teachers
     And I should see "Description for Group A"
     And ".groupinfobox" "css_element" should exist
     And I set the field "type" in the "Filter 1" "fieldset" to "Groups"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset"
-    And I click on "Group B" "list_item"
+    And I set the field "Type or select..." in the "Filter 1" "fieldset" to "Group B"
     And I click on "Apply filters" "button"
     And I click on "Student 2" "link" in the "participants" "table"
     And I click on "Group B" "link"
@@ -96,8 +95,7 @@ Feature: The description of a group can be viewed by students and teachers
     And I should see "Description for Group A"
     And ".groupinfobox" "css_element" should exist
     And I set the field "type" in the "Filter 1" "fieldset" to "Groups"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset"
-    And I click on "Group B" "list_item"
+    And I set the field "Type or select..." in the "Filter 1" "fieldset" to "Group B"
     And I click on "Apply filters" "button"
     And I click on "Student 2" "link" in the "participants" "table"
     And I click on "Group B" "link"
index 7434b4e..aa6b691 100644 (file)
@@ -45,7 +45,7 @@ $string['dmlexceptiononinstall'] = '<p>Ett databasfel har inträffat [{$a->error
 $string['downloadedfilecheckfailed'] = 'Det gick inte att kontrollera den nedladdade filen';
 $string['invalidmd5'] = 'Kontrollvariabeln var felaktig - försök igen';
 $string['missingrequiredfield'] = 'Det saknas några obligatoriska fält';
-$string['remotedownloaderror'] = 'Nedladdningen av en komponent till din server misslyckades, var snäll och verifiera inställningar för proxy. <br /><br />Du måste ladda ner <a href="{$a->url}">{$a->url}</a> filen manuellt, kopiera den till "{$a->dest}" på din server och packa upp den där.';
+$string['remotedownloaderror'] = 'Nedladdningen av en komponent till din server misslyckades. Vänligen verifiera inställningar för proxy. <br /><br />Du måste ladda ner <a href="{$a->url}">{$a->url}</a> filen manuellt, kopiera den till "{$a->dest}" på din server och packa upp den där.';
 $string['wrongdestpath'] = 'Fel sökväg';
 $string['wrongsourcebase'] = 'Fel bas-URL till källan';
 $string['wrongzipfilename'] = 'Fel namn på ZIP-filen';
index 01e1eb1..cb25140 100644 (file)
@@ -32,8 +32,8 @@ defined('MOODLE_INTERNAL') || die();
 $string['admindirname'] = 'Katalog/mapp för administration';
 $string['availablelangs'] = 'Tillgängliga språkpaket';
 $string['chooselanguagehead'] = 'Välj ett språk';
-$string['chooselanguagesub'] = 'Var snäll och välj ett språk  för installationen. Du kommer att ha möjlighet att välja språk för webbplatsen och användarna på en senare skärm.';
-$string['clialreadyinstalled'] = 'Filen config.php finns redan, var snäll och använd admin/cli/upgrade.php om Du vill uppgradera Din webbplats.';
+$string['chooselanguagesub'] = 'Vänligen välj ett språk för installationen. Du kommer att ha möjlighet att välja språk för webbplatsen och användarna på en senare skärm.';
+$string['clialreadyinstalled'] = 'Filen config.php finns redan. Vänligen använd admin/cli/upgrade.php om Du vill uppgradera Din webbplats.';
 $string['cliinstallheader'] = 'Installationsprogram av typ kommandorad {$a} för Moodle ';
 $string['databasehost'] = 'Värd för databas';
 $string['databasename'] = 'Namn på databas';
index 4e1f6c3..92781b5 100644 (file)
@@ -348,6 +348,8 @@ $string['configsessioncookie'] = 'This setting customises the name of the cookie
 $string['configsessioncookiedomain'] = 'This allows you to change the domain that the Moodle cookies are available from. This is useful for Moodle customisations (e.g. authentication or enrolment plugins) that need to share Moodle session information with a web application on another subdomain. <strong>WARNING: it is strongly recommended to leave this setting at the default (empty) - an incorrect value will prevent all logins to the site.</strong>';
 $string['configsessioncookiepath'] = 'If you need to change where browsers send the Moodle cookies, you can change this setting to specify a subdirectory of your web site.  Otherwise the default \'/\' should be fine.';
 $string['configsessiontimeout'] = 'If people logged in to this site are idle for a long time (without loading pages) then they are automatically logged out (their session is ended).  This variable specifies how long this time should be.';
+$string['configsessiontimeoutwarning'] = 'If people logged in to this site are idle for a long time (without loading pages) then they are warned about their session is about to end.  This variable specifies how long this time should be.';
+$string['configsessiontimeoutwarningcheck'] = 'Session timeout warning must be less than session timeout';
 $string['configshowicalsource'] = 'Show source information for iCal events';
 $string['configshowcommentscount'] = 'Show comments count, it will cost one more query when display comments link';
 $string['configshowsiteparticipantslist'] = 'All of these site students and site teachers will be listed on the site participants list. Who shall be allowed to see this site participants list?';
@@ -1191,6 +1193,7 @@ $string['sessioncookiedomain'] = 'Cookie domain';
 $string['sessioncookiepath'] = 'Cookie path';
 $string['sessionhandling'] = 'Session handling';
 $string['sessiontimeout'] = 'Timeout';
+$string['sessiontimeoutwarning'] = 'Timeout Warning';
 $string['settingdependenton'] = 'This setting may be hidden, based on the value of <strong>{$a}</strong>.';
 $string['settingfileuploads'] = 'File uploading is required for normal operation, please enable it in PHP configuration.';
 $string['settingmemorylimit'] = 'Insufficient memory detected, please set higher memory limit in PHP settings.';
index 8fbcca4..95c040d 100644 (file)
@@ -1180,6 +1180,7 @@ $string['loginstepsnone'] = '<p>Hi!</p>
 <p>All you need to do is make up a username and password and use it in the form on this page!</p>
 <p>If someone else has already chosen your username then you\'ll have to try again using a different username.</p>';
 $string['loginto'] = 'Log in to {$a}';
+$string['loginagain'] = 'Log in again';
 $string['logout'] = 'Log out';
 $string['logoutconfirm'] = 'Do you really want to log out?';
 $string['logs'] = 'Logs';
index ff74c9a..1177b59 100644 (file)
@@ -3814,6 +3814,8 @@ class admin_setting_configduration extends admin_setting {
 
     /** @var int default duration unit */
     protected $defaultunit;
+    /** @var callable|null Validation function */
+    protected $validatefunction = null;
 
     /**
      * Constructor
@@ -3837,6 +3839,36 @@ class admin_setting_configduration extends admin_setting {
         parent::__construct($name, $visiblename, $description, $defaultsetting);
     }
 
+    /**
+     * Sets a validate function.
+     *
+     * The callback will be passed one parameter, the new setting value, and should return either
+     * an empty string '' if the value is OK, or an error message if not.
+     *
+     * @param callable|null $validatefunction Validate function or null to clear
+     * @since Moodle 3.10
+     */
+    public function set_validate_function(?callable $validatefunction = null) {
+        $this->validatefunction = $validatefunction;
+    }
+
+    /**
+     * Validate the setting. This uses the callback function if provided; subclasses could override
+     * to carry out validation directly in the class.
+     *
+     * @param int $data New value being set
+     * @return string Empty string if valid, or error message text
+     * @since Moodle 3.10
+     */
+    protected function validate_setting(int $data): string {
+        // If validation function is specified, call it now.
+        if ($this->validatefunction) {
+            return call_user_func($this->validatefunction, $data);
+        } else {
+            return '';
+        }
+    }
+
     /**
      * Returns selectable units.
      * @static
@@ -3922,6 +3954,12 @@ class admin_setting_configduration extends admin_setting {
             return get_string('errorsetting', 'admin');
         }
 
+        // Validate the new setting.
+        $error = $this->validate_setting($seconds);
+        if ($error) {
+            return $error;
+        }
+
         $result = $this->config_write($this->name, $seconds);
         return ($result ? '' : get_string('errorsetting', 'admin'));
     }
index 23e42cb..71d7b14 100644 (file)
Binary files a/lib/amd/build/form-autocomplete.min.js and b/lib/amd/build/form-autocomplete.min.js differ
index 5c1fe60..89cf819 100644 (file)
Binary files a/lib/amd/build/form-autocomplete.min.js.map and b/lib/amd/build/form-autocomplete.min.js.map differ
index 81e4444..680602f 100644 (file)
Binary files a/lib/amd/build/network.min.js and b/lib/amd/build/network.min.js differ
index b2204e1..df849d1 100644 (file)
Binary files a/lib/amd/build/network.min.js.map and b/lib/amd/build/network.min.js.map differ
index f7f0f21..ffe8b07 100644 (file)
@@ -1173,6 +1173,9 @@ function($, log, str, templates, notification, LoadingIcon, Aria) {
                 originalSelect.hide();
                 var container = originalSelect.parent();
 
+                // Ensure that the data-fieldtype is set for behat.
+                input.find('input').attr('data-fieldtype', 'autocomplete');
+
                 container.append(layout);
                 container.find('[data-region="form_autocomplete-input"]').replaceWith(input);
                 container.find('[data-region="form_autocomplete-suggestions"]').replaceWith(suggestions);
index 5e21a6a..fcf1a0b 100644 (file)
@@ -32,14 +32,66 @@ define(['jquery', 'core/ajax', 'core/config', 'core/notification', 'core/str'],
     var sessionTimeout = false;
     // 1/10 of session timeout, max of 10 minutes.
     var checkFrequency = Math.min((Config.sessiontimeout / 10), 600) * 1000;
-    // 1/5 of sessiontimeout.
-    var warningLimit = checkFrequency * 2;
-
+    // Check if sessiontimeoutwarning is set or double the checkFrequency.
+    var warningLimit = (Config.sessiontimeoutwarning > 0) ? (Config.sessiontimeoutwarning * 1000) : (checkFrequency * 2);
+    // First wait is minimum of remaining time or half of the session timeout.
+    var firstWait = (Config.sessiontimeoutwarning > 0) ?
+        Math.min((Config.sessiontimeout - Config.sessiontimeoutwarning) * 1000, checkFrequency * 5) : checkFrequency * 5;
     /**
      * The session time has expired - we can't extend it now.
+     * @param {Modal} modal
      */
-    var timeoutSessionExpired = function() {
+    var timeoutSessionExpired = function(modal) {
         sessionTimeout = true;
+        warningDisplayed = false;
+        closeModal(modal);
+        displaySessionExpired();
+    };
+
+    /**
+     * Close modal - this relies on modal object passed from Notification.confirm.
+     *
+     * @param {Modal} modal
+     */
+    var closeModal = function(modal) {
+        modal.destroy();
+    };
+
+    /**
+     * The session time has expired - we can't extend it now.
+     * @return {Promise}
+     */
+    var displaySessionExpired = function() {
+        // Check again if its already extended before displaying session expired popup in case multiple tabs are open.
+        var request = {
+            methodname: 'core_session_time_remaining',
+            args: { }
+        };
+
+        return Ajax.call([request], true, true, true)[0].then(function(args) {
+            if (args.timeremaining * 1000 > warningLimit) {
+                return false;
+            } else {
+                return Str.get_strings([
+                    {key: 'sessionexpired', component: 'error'},
+                    {key: 'sessionerroruser', component: 'error'},
+                    {key: 'loginagain', component: 'moodle'},
+                    {key: 'cancel', component: 'moodle'}
+                ]).then(function(strings) {
+                    Notification.confirm(
+                        strings[0], // Title.
+                        strings[1], // Message.
+                        strings[2], // Login Again.
+                        strings[3], // Cancel.
+                        function() {
+                            location.reload();
+                            return true;
+                        }
+                    );
+                    return true;
+                }).catch(Notification.exception);
+            }
+        });
     };
 
     /**
@@ -55,23 +107,14 @@ define(['jquery', 'core/ajax', 'core/config', 'core/notification', 'core/str'],
 
         if (sessionTimeout) {
             // We timed out before we extended the session.
-            return Str.get_strings([
-                {key: 'sessionexpired', component: 'error'},
-                {key: 'sessionerroruser', component: 'error'}
-            ]).then(function(strings) {
-                Notification.alert(
-                    strings[0], // Title.
-                    strings[1] // Message.
-                );
-                return true;
-            }).fail(Notification.exception);
+            return displaySessionExpired();
         } else {
             return Ajax.call([request], true, true, false, requestTimeout)[0].then(function() {
                 if (keepAliveFrequency > 0) {
                     setTimeout(touchSession, keepAliveFrequency);
                 }
                 return true;
-            }).fail(function() {
+            }).catch(function() {
                 Notification.alert('', keepAliveMessage);
             });
         }
@@ -88,27 +131,14 @@ define(['jquery', 'core/ajax', 'core/config', 'core/notification', 'core/str'],
             methodname: 'core_session_time_remaining',
             args: { }
         };
-
         sessionTimeout = false;
         return Ajax.call([request], true, true, true)[0].then(function(args) {
             if (args.userid <= 0) {
                 return false;
             }
-            if (args.timeremaining < 0) {
-                Str.get_strings([
-                    {key: 'sessionexpired', component: 'error'},
-                    {key: 'sessionerroruser', component: 'error'}
-                ]).then(function(strings) {
-                    Notification.alert(
-                        strings[0], // Title.
-                        strings[1] // Message.
-                    );
-                    return true;
-                }).fail(Notification.exception);
-
-            } else if (args.timeremaining * 1000 < warningLimit && !warningDisplayed) {
-                // If we don't extend the session before the timeout - warn.
-                setTimeout(timeoutSessionExpired, args.timeremaining * 1000);
+            if (args.timeremaining <= 0) {
+                return displaySessionExpired();
+            } else if (args.timeremaining * 1000 <= warningLimit && !warningDisplayed) {
                 warningDisplayed = true;
                 Str.get_strings([
                     {key: 'norecentactivity', component: 'moodle'},
@@ -116,7 +146,7 @@ define(['jquery', 'core/ajax', 'core/config', 'core/notification', 'core/str'],
                     {key: 'extendsession', component: 'moodle'},
                     {key: 'cancel', component: 'moodle'}
                 ]).then(function(strings) {
-                    Notification.confirm(
+                     return Notification.confirm(
                         strings[0], // Title.
                         strings[1], // Message.
                         strings[2], // Extend session.
@@ -124,17 +154,20 @@ define(['jquery', 'core/ajax', 'core/config', 'core/notification', 'core/str'],
                         function() {
                             touchSession();
                             warningDisplayed = false;
-                            // First wait is half the session timeout.
-                            setTimeout(checkSession, checkFrequency * 5);
+                            // First wait is minimum of remaining time or half of the session timeout.
+                            setTimeout(checkSession, firstWait);
                             return true;
                         },
                         function() {
-                            warningDisplayed = false;
+                            // User has cancelled notification.
                             setTimeout(checkSession, checkFrequency);
                         }
                     );
-                    return true;
-                }).fail(Notification.exception);
+                }).then(modal => {
+                    // If we don't extend the session before the timeout - warn.
+                    setTimeout(timeoutSessionExpired, args.timeremaining * 1000, modal);
+                    return;
+                }).catch(Notification.exception);
             } else {
                 setTimeout(checkSession, checkFrequency);
             }
@@ -151,8 +184,8 @@ define(['jquery', 'core/ajax', 'core/config', 'core/notification', 'core/str'],
         if (keepAliveFrequency > 0) {
             setTimeout(touchSession, keepAliveFrequency);
         } else {
-            // First wait is half the session timeout.
-            setTimeout(checkSession, checkFrequency * 5);
+            // First wait is minimum of remaining time or half of the session timeout.
+            setTimeout(checkSession, firstWait);
         }
     };
 
index 1a807a2..193d2af 100644 (file)
@@ -101,7 +101,6 @@ class behat_field_manager {
      * @return behat_form_field
      */
     public static function get_field_instance($type, NodeElement $fieldnode, Session $session) {
-
         global $CFG;
 
         // If the field is not part of a moodleform, we should still try to find out
@@ -152,6 +151,10 @@ class behat_field_manager {
             $type = $fieldnode->getAttribute('type');
             switch ($type) {
                 case 'text':
+                    if ($fieldtype = $fieldnode->getAttribute('data-fieldtype')) {
+                        return self::normalise_fieldtype($fieldtype);
+                    }
+                    return 'text';
                 case 'password':
                 case 'email':
                 case 'file':
index 238bbee..b8144f0 100644 (file)
@@ -48,43 +48,75 @@ class behat_form_autocomplete extends behat_form_text {
             throw new coding_exception('Setting the value of an autocomplete field requires javascript.');
         }
 
-        // Set the value of the autocomplete's input.
-        // If this autocomplete offers suggestions then these should be fetched by setting the value and waiting for the
-        // JS to finish fetching those suggestions.
+        // Clear all current selections.
+        $rootnode = $this->field->getParent()->getParent();
+        $selections = $rootnode->findAll('css', '.form-autocomplete-selection [role=option]');
+        foreach (array_reverse($selections) as $selection) {
+            $selection->click();
+            $this->wait_for_pending_js();
+        }
 
-        $istagelement = $this->field->hasAttribute('data-tags') && $this->field->getAttribute('data-tags');
+        $allowscreation = $this->field->hasAttribute('data-tags') && !empty($this->field->getAttribute('data-tags'));
+        $hasmultiple = $this->field->hasAttribute('data-multiple') && !empty($this->field->getAttribute('data-multiple'));
 
-        if ($istagelement && false !== strpos($value, ',')) {
-            // Commas have a special meaning as a value separator in 'tag' autocomplete elements.
+        if ($hasmultiple && false !== strpos($value, ',')) {
+            // Commas have a special meaning as a value separator in 'multiple' autocomplete elements.
             // To handle this we break the value up by comma, and enter it in chunks.
             $values = explode(',', $value);
 
             while ($value = array_shift($values)) {
-                $this->set_value($value);
+                $this->add_value(trim($value), $allowscreation);
             }
         } else {
-            $this->field->setValue($value);
-            $this->wait_for_pending_js();
+            $this->add_value(trim($value), $allowscreation);
+        }
+    }
 
-            // If the autocomplete found suggestions, then it will have:
-            // 1) marked itself as expanded; and
-            // 2) have an aria-selected suggestion in the list.
-            $expanded = $this->field->getAttribute('aria-expanded');
-            $suggestion = $this->field->getParent()->find('css', '.form-autocomplete-suggestions > [aria-selected="true"]');
-
-            if ($expanded && null !== $suggestion) {
-                // A suggestion was found.
-                // Click on the first item in the list.
-                $suggestion->click();
-            } else {
-                // Press the return key to create a new tag.
-                behat_base::type_keys($this->session, [behat_keys::ENTER]);
-            }
-            $this->wait_for_pending_js();
+    /**
+     * Add a value to the autocomplete.
+     *
+     * @param   string $value
+     * @param   bool $allowscreation
+     */
+    protected function add_value(string $value, bool $allowscreation): void {
+        $value = trim($value);
 
-            // Press the escape to close the autocomplete suggestions list.
-            behat_base::type_keys($this->session, [behat_keys::ESCAPE]);
-            $this->wait_for_pending_js();
+        // Click into the field.
+        $this->field->click();
+
+        // Remove any existing text.
+        do {
+            behat_base::type_keys($this->session, [behat_keys::BACKSPACE, behat_keys::DELETE]);
+        } while (strlen($this->field->getValue()) > 0);
+        $this->wait_for_pending_js();
+
+        // Type in the new value.
+        behat_base::type_keys($this->session, str_split($value));
+        $this->wait_for_pending_js();
+
+        // If the autocomplete found suggestions, then it will have:
+        // 1) marked itself as expanded; and
+        // 2) have an aria-selected suggestion in the list.
+        $expanded = $this->field->getAttribute('aria-expanded');
+        $suggestion = $this->field->getParent()->getParent()->find('css', '.form-autocomplete-suggestions > [aria-selected="true"]');
+
+        if ($expanded && null !== $suggestion) {
+            // A suggestion was found.
+            // Click on the first item in the list.
+            $suggestion->click();
+        } else if ($allowscreation) {
+            // Press the return key to create a new entry.
+            behat_base::type_keys($this->session, [behat_keys::ENTER]);
+        } else {
+            throw new \InvalidArgumentException(
+                "Unable to find '{$value}' in the list of options, and unable to create a new option"
+            );
         }
+
+        $this->wait_for_pending_js();
+
+        // Press the escape to close the autocomplete suggestions list.
+        behat_base::type_keys($this->session, [behat_keys::ESCAPE]);
+        $this->wait_for_pending_js();
     }
 }
index 574ea94..6c49aaa 100644 (file)
@@ -473,7 +473,7 @@ class client extends \oauth2_client {
 
         // Store the access token and, if provided by the server, the new refresh token.
         $this->store_token($receivedtokens['access_token']);
-        if (isset($receivedtokens['refreshtoken'])) {
+        if (isset($receivedtokens['refresh_token'])) {
             $systemaccount->set('refreshtoken', $receivedtokens['refresh_token']->token);
             $systemaccount->update();
         }
index 0c28dd6..befe2af 100644 (file)
@@ -680,7 +680,6 @@ class manager {
 
         $where = "(lastruntime IS NULL OR lastruntime < :timestart1)
                   AND (nextruntime IS NULL OR nextruntime < :timestart2)
-                  AND disabled = 0
                   ORDER BY lastruntime, id ASC";
         $params = array('timestart1' => $timestart, 'timestart2' => $timestart);
         $records = $DB->get_records_select('task_scheduled', $where, $params);
@@ -689,14 +688,15 @@ class manager {
 
         foreach ($records as $record) {
 
+            $task = self::scheduled_task_from_record($record);
+            // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
+            // Also check to see if task is disabled or enabled after applying overrides.
+            if (!$task || $task->get_disabled()) {
+                continue;
+            }
+
             if ($lock = $cronlockfactory->get_lock(($record->classname), 0)) {
                 $classname = '\\' . $record->classname;
-                $task = self::scheduled_task_from_record($record);
-                // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
-                if (!$task) {
-                    $lock->release();
-                    continue;
-                }
 
                 $task->set_lock($lock);
 
index d64ad69..7524c00 100644 (file)
                     {{{label}}}
                 {{/text}}
             </label>
-            <span class="ml-2 d-flex align-items-center align-self-start">
+            <div class="ml-2 d-flex align-items-center align-self-start">
                 {{#required}}
                     <div class="text-danger" title="{{#str}}required{{/str}}">
                     {{#pix}}req, core, {{#str}}required{{/str}}{{/pix}}
                     </div>
                 {{/required}}
                 {{{helpbutton}}}
-            </span>
+            </div>
         </div>
         <div class="form-control-feedback invalid-feedback" id="{{element.iderror}}" {{#error}} style="display: block;"{{/error}}>
             {{{error}}}
index 4aacd4f..b3497a4 100644 (file)
                     {{{label}}}
                 {{/text}}
             </label>
-            <span class="ml-2 d-flex align-items-center align-self-start">
+            <div class="ml-2 d-flex align-items-center align-self-start">
                 {{#required}}
                     <div class="text-danger" title="{{#str}}required{{/str}}">
                     {{#pix}}req, core, {{#str}}required{{/str}}{{/pix}}
                     </div>
                 {{/required}}
                 {{{helpbutton}}}
-            </span>
+            </div>
         </div>
         <div class="form-control-feedback invalid-feedback" id="{{element.iderror}}" {{#error}} style="display: block;"{{/error}}>
             {{{error}}}
index 17e557e..0efeff5 100644 (file)
                         {{{label}}}
                     {{/text}}
                 </label>
-                <span class="ml-2 d-flex align-items-center align-self-start">
+                <div class="ml-2 d-flex align-items-center align-self-start">
                     {{#required}}
                         <div class="text-danger" title="{{#str}}required{{/str}}">
                         {{#pix}}req, core, {{#str}}required{{/str}}{{/pix}}
                         </div>
                     {{/required}}
                     {{{helpbutton}}}
-                </span>
+                </div>
         </div>
         <div class="form-control-feedback invalid-feedback" id="{{element.iderror}}" {{#error}} style="display: block;"{{/error}}>
             {{{error}}}
index afa6576..b77408b 100644 (file)
                 </span>
             {{/element.staticlabel}}
         {{/ label }}{{/ label}}
-        <span class="ml-1 ml-md-auto d-flex align-items-center align-self-start">
+        <div class="ml-1 ml-md-auto d-flex align-items-center align-self-start">
             {{#required}}
                 <div class="text-danger" title="{{#str}}required{{/str}}">
                 {{#pix}}req, core, {{#str}}required{{/str}}{{/pix}}
                 </div>
             {{/required}}
             {{{helpbutton}}}
-        </span>
+        </div>
     </div>
     <div class="col-md-9 form-inline align-items-start felement" data-fieldtype="{{element.type}}">
         {{$ element }}
index b0f5114..59fcd1c 100644 (file)
@@ -3906,13 +3906,19 @@ function get_enabled_auth_plugins($fix=false) {
 
     if ($fix) {
         $auths = array_unique($auths);
+        $oldauthconfig = implode(',', $auths);
         foreach ($auths as $k => $authname) {
-            if (!exists_auth_plugin($authname) or in_array($authname, $default)) {
+            $authplugindoesnotexist = !exists_auth_plugin($authname);
+            if ($authplugindoesnotexist || in_array($authname, $default)) {
+                if ($authplugindoesnotexist) {
+                    debugging(get_string('authpluginnotfound', 'debug', $authname));
+                }
                 unset($auths[$k]);
             }
         }
         $newconfig = implode(',', $auths);
         if (!isset($CFG->auth) or $newconfig != $CFG->auth) {
+            add_to_config_log('auth', $oldauthconfig, $newconfig, 'core');
             set_config('auth', $newconfig);
         }
     }
index b6277cb..ff5428b 100644 (file)
@@ -3407,6 +3407,15 @@ class block_contents {
     public function add_class($class) {
         $this->attributes['class'] .= ' '.$class;
     }
+
+    /**
+     * Check if the block is a fake block.
+     *
+     * @return boolean
+     */
+    public function is_fake() {
+        return isset($this->attributes['data-block']) && $this->attributes['data-block'] == '_fake';
+    }
 }
 
 
index 6b61cab..abfb603 100644 (file)
@@ -1817,9 +1817,10 @@ class core_renderer extends renderer_base {
      * Output all the blocks in a particular region.
      *
      * @param string $region the name of a region on this page.
+     * @param boolean $fakeblocksonly Output fake block only.
      * @return string the HTML to be output.
      */
-    public function blocks_for_region($region) {
+    public function blocks_for_region($region, $fakeblocksonly = false) {
         $blockcontents = $this->page->blocks->get_content_for_region($region, $this);
         $lastblock = null;
         $zones = array();
@@ -1832,10 +1833,16 @@ class core_renderer extends renderer_base {
 
         foreach ($blockcontents as $bc) {
             if ($bc instanceof block_contents) {
+                if ($fakeblocksonly && !$bc->is_fake()) {
+                    // Skip rendering real blocks if we only want to show fake blocks.
+                    continue;
+                }
                 $output .= $this->block($bc, $region);
                 $lastblock = $bc->title;
             } else if ($bc instanceof block_move_target) {
-                $output .= $this->block_move_target($bc, $zones, $lastblock, $region);
+                if (!$fakeblocksonly) {
+                    $output .= $this->block_move_target($bc, $zones, $lastblock, $region);
+                }
             } else {
                 throw new coding_exception('Unexpected type of thing (' . get_class($bc) . ') found in list of block contents.');
             }
@@ -3939,9 +3946,12 @@ EOD;
      *
      * @since Moodle 2.5.1 2.6
      * @param string $region The region to get HTML for.
+     * @param array $classes Wrapping tag classes.
+     * @param string $tag Wrapping tag.
+     * @param boolean $fakeblocksonly Include fake blocks only.
      * @return string HTML.
      */
-    public function blocks($region, $classes = array(), $tag = 'aside') {
+    public function blocks($region, $classes = array(), $tag = 'aside', $fakeblocksonly = false) {
         $displayregion = $this->page->apply_theme_region_manipulations($region);
         $classes = (array)$classes;
         $classes[] = 'block-region';
@@ -3952,7 +3962,7 @@ EOD;
             'data-droptarget' => '1'
         );
         if ($this->page->blocks->region_has_content($displayregion, $this)) {
-            $content = $this->blocks_for_region($displayregion);
+            $content = $this->blocks_for_region($displayregion, $fakeblocksonly);
         } else {
             $content = '';
         }
@@ -5084,9 +5094,10 @@ class core_renderer_maintenance extends core_renderer {
      * @param string $region
      * @param array $classes
      * @param string $tag
+     * @param boolean $fakeblocksonly
      * @return string
      */
-    public function blocks($region, $classes = array(), $tag = 'aside') {
+    public function blocks($region, $classes = array(), $tag = 'aside', $fakeblocksonly = false) {
         return '';
     }
 
@@ -5094,9 +5105,10 @@ class core_renderer_maintenance extends core_renderer {
      * Does nothing. The maintenance renderer cannot produce blocks.
      *
      * @param string $region
+     * @param boolean $fakeblocksonly Output fake block only.
      * @return string
      */
-    public function blocks_for_region($region) {
+    public function blocks_for_region($region, $fakeblocksonly = false) {
         return '';
     }
 
index 7bca2a7..d18b9f5 100644 (file)
@@ -319,20 +319,21 @@ class page_requirements_manager {
             }
 
             $this->M_cfg = array(
-                'wwwroot'             => $CFG->wwwroot,
-                'sesskey'             => sesskey(),
-                'sessiontimeout'      => $CFG->sessiontimeout,
-                'themerev'            => theme_get_revision(),
-                'slasharguments'      => (int)(!empty($CFG->slasharguments)),
-                'theme'               => $page->theme->name,
-                'iconsystemmodule'    => $iconsystem->get_amd_name(),
-                'jsrev'               => $this->get_jsrev(),
-                'admin'               => $CFG->admin,
-                'svgicons'            => $page->theme->use_svg_icons(),
-                'usertimezone'        => usertimezone(),
-                'contextid'           => $contextid,
-                'langrev'             => get_string_manager()->get_revision(),
-                'templaterev'         => $this->get_templaterev()
+                'wwwroot'               => $CFG->wwwroot,
+                'sesskey'               => sesskey(),
+                'sessiontimeout'        => $CFG->sessiontimeout,
+                'sessiontimeoutwarning' => $CFG->sessiontimeoutwarning,
+                'themerev'              => theme_get_revision(),
+                'slasharguments'        => (int)(!empty($CFG->slasharguments)),
+                'theme'                 => $page->theme->name,
+                'iconsystemmodule'      => $iconsystem->get_amd_name(),
+                'jsrev'                 => $this->get_jsrev(),
+                'admin'                 => $CFG->admin,
+                'svgicons'              => $page->theme->use_svg_icons(),
+                'usertimezone'          => usertimezone(),
+                'contextid'             => $contextid,
+                'langrev'               => get_string_manager()->get_revision(),
+                'templaterev'           => $this->get_templaterev()
             );
             if ($CFG->debugdeveloper) {
                 $this->M_cfg['developerdebug'] = true;
index 1326f13..210bdee 100644 (file)
@@ -801,6 +801,10 @@ if (CLI_SCRIPT) {
 if (empty($CFG->sessiontimeout)) {
     $CFG->sessiontimeout = 8 * 60 * 60;
 }
+// Set sessiontimeoutwarning 20 minutes.
+if (empty($CFG->sessiontimeoutwarning)) {
+    $CFG->sessiontimeoutwarning = 20 * 60;
+}
 \core\session\manager::start();
 
 // Set default content type and encoding, developers are still required to use
index ac76ed5..ae7d194 100644 (file)
 }}
 {{#showSuggestions}}
 <div class="d-md-inline-block mr-md-2 position-relative">
-    <input type="text" id="{{inputId}}" class="form-control" list="{{suggestionsId}}" placeholder="{{placeholder}}" role="combobox" aria-expanded="false" autocomplete="off" autocorrect="off" autocapitalize="off" aria-autocomplete="list" aria-owns="{{suggestionsId}} {{selectionId}}" {{#tags}}data-tags="1"{{/tags}}/>
+    <input type="text"{{!
+    }} id="{{inputId}}"{{!
+    }} class="form-control"{{!
+    }} list="{{suggestionsId}}"{{!
+    }} placeholder="{{placeholder}}"{{!
+    }} role="combobox"{{!
+    }} aria-expanded="false"{{!
+    }} autocomplete="off"{{!
+    }} autocorrect="off"{{!
+    }} autocapitalize="off"{{!
+    }} aria-autocomplete="list"{{!
+    }} aria-owns="{{suggestionsId}} {{selectionId}}"{{!
+    }} {{#tags}}data-tags="1"{{/tags}}{{!
+    }} {{#multiple}}data-multiple="multiple"{{/multiple}}{{!
+    }}>
     <span class="form-autocomplete-downarrow position-absolute p-1" id="{{downArrowId}}">&#x25BC;</span>
 </div>
 {{/showSuggestions}}
 {{^showSuggestions}}
 <div class="d-md-inline-block mr-md-2">
-    <input type="text" id="{{inputId}}" class="form-control" placeholder="{{placeholder}}" role="textbox" aria-owns="{{selectionId}}" {{#tags}}data-tags="1"{{/tags}}/>
+    <input type="text"{{!
+    }} id="{{inputId}}"{{!
+    }} class="form-control"{{!
+    }} placeholder="{{placeholder}}"{{!
+    }} role="textbox"{{!
+    }} aria-owns="{{selectionId}}"{{!
+    }} {{#tags}}data-tags="1"{{/tags}}{{!
+    }} {{#multiple}}data-multiple="multiple"{{/multiple}}{{!
+    }}>
 </div>
 {{/showSuggestions}}
 
index 7e89b09..d2cdc48 100644 (file)
@@ -49,6 +49,7 @@
            class="form-control withclear"
            placeholder="{{$placeholder}}{{#str}} search, core {{/str}}{{/placeholder}}"
            name="search"
+           value="{{$value}}{{/value}}"
            autocomplete="off"
         >
         <button
index 6f561ec..73953d8 100644 (file)
@@ -95,7 +95,7 @@ function(
     var container = $('#searchinput-navbar-' + uniqid);
     var opensearch = container.find('[data-action="opensearch"]');
     var input = container.find('[data-region="input"]');
-    var submit = container.find('[data-action="submit"');
+    var submit = container.find('[data-action="submit"]');
 
     submit.on('click', function(e) {
         if (input.val() === '') {
@@ -113,4 +113,4 @@ function(
         input.focus();
     });
 });
-{{/js}}
\ No newline at end of file
+{{/js}}
index 2532f94..2b92271 100644 (file)
@@ -693,18 +693,21 @@ class behat_forms extends behat_base {
         $xpathtarget = "//ul[@class='form-autocomplete-suggestions']//*[contains(concat('|', string(.), '|'),'|" . $item . "|')]";
 
         $this->execute('behat_general::i_click_on', [$xpathtarget, 'xpath_element']);
-
-        $this->execute('behat_general::i_press_key_in_element', ['13', 'body', 'xpath_element']);
     }
 
     /**
      * Open the auto-complete suggestions list (Assuming there is only one on the page.).
      *
-     * @Given /^I open the autocomplete suggestions list$/
+     * @Given I open the autocomplete suggestions list
+     * @Given I open the autocomplete suggestions list in the :container :containertype
      */
-    public function i_open_the_autocomplete_suggestions_list() {
+    public function i_open_the_autocomplete_suggestions_list($container = null, $containertype = null) {
         $csstarget = ".form-autocomplete-downarrow";
-        $this->execute('behat_general::i_click_on', [$csstarget, 'css_element']);
+        if ($container && $containertype) {
+            $this->execute('behat_general::i_click_on', [$csstarget, 'css_element', $container, $containertype]);
+        } else {
+            $this->execute('behat_general::i_click_on', [$csstarget, 'css_element']);
+        }
     }
 
     /**
index c46383b..0ff1c55 100644 (file)
@@ -650,4 +650,33 @@ EOF;
         $this->assertTrue(in_array(['name' => 'class', 'value' => $labelclass], $data->labelattributes));
         $this->assertTrue(in_array(['name' => 'style', 'value' => $labelstyle], $data->labelattributes));
     }
+
+    /**
+     * Data provider for test_block_contents_is_fake().
+     *
+     * @return array
+     */
+    public function block_contents_is_fake_provider() {
+        return [
+            'Null' => [null, false],
+            'Not set' => [false, false],
+            'Fake' => ['_fake', true],
+            'Real block' => ['activity_modules', false],
+        ];
+    }
+
+    /**
+     * Test block_contents is_fake() method.
+     *
+     * @dataProvider block_contents_is_fake_provider
+     * @param mixed $value Value for the data-block attribute
+     * @param boolean $expected The expected result
+     */
+    public function test_block_contents_is_fake($value, $expected) {
+        $bc = new block_contents(array());
+        if ($value !== false) {
+            $bc->attributes['data-block'] = $value;
+        }
+        $this->assertEquals($expected, $bc->is_fake());
+    }
 }
index aa03f51..d57c352 100644 (file)
@@ -640,6 +640,47 @@ class core_scheduled_task_testcase extends advanced_testcase {
         }
     }
 
+    /**
+     * Check that an overridden task is sent to be processed.
+     */
+    public function test_scheduled_task_overridden_task_can_run(): void {
+        global $CFG, $DB;
+
+        $this->resetAfterTest();
+
+        // Delete all existing scheduled tasks.
+        $DB->delete_records('task_scheduled');
+
+        // Add overrides to the config.
+        $CFG->scheduled_tasks = [
+            '\core\task\scheduled_test_task' => [
+                'disabled' => 1
+            ],
+            '\core\task\scheduled_test2_task' => [
+                'disabled' => 0
+            ],
+        ];
+
+        // A task that runs once per hour.
+        $record = new stdClass();
+        $record->component = 'test_scheduled_task';
+        $record->classname = '\core\task\scheduled_test_task';
+        $record->disabled = 0;
+        $DB->insert_record('task_scheduled', $record);
+
+        // And disabled test.
+        $record->classname = '\core\task\scheduled_test2_task';
+        $record->disabled = 1;
+        $DB->insert_record('task_scheduled', $record);
+
+        $now = time();
+
+        $scheduledtask = \core\task\manager::get_next_scheduled_task($now);
+        $this->assertInstanceOf('\core\task\scheduled_test2_task', $scheduledtask);
+        $scheduledtask->execute();
+        \core\task\manager::scheduled_task_complete($scheduledtask);
+    }
+
     /**
      * Assert that the specified tasks are equal.
      *
index edf9986..75f27f4 100644 (file)
@@ -31,25 +31,31 @@ class mod_data_export_form extends moodleform {
     }
 
     function definition() {
-        global $CFG;
         $mform =& $this->_form;
-        $mform->addElement('header', 'notice', get_string('chooseexportformat', 'data'));
-        $choices = csv_import_reader::get_delimiter_list();
-        $key = array_search(';', $choices);
-        if (! $key === FALSE) {
-            // array $choices contains the semicolon -> drop it (because its encrypted form also contains a semicolon):
-            unset($choices[$key]);
-        }
+        $mform->addElement('header', 'exportformat', get_string('chooseexportformat', 'data'));
+
+        $optionattrs = ['class' => 'mt-1 mb-1'];
+
+        // Export format type radio group.
         $typesarray = array();
-        $str = get_string('csvwithselecteddelimiter', 'data');
-        $typesarray[] = $mform->createElement('radio', 'exporttype', null, $str . '&nbsp;', 'csv');
-        $typesarray[] = $mform->createElement('select', 'delimiter_name', null, $choices);
-        //temporarily commenting out Excel export option. See MDL-19864
+        $typesarray[] = $mform->createElement('radio', 'exporttype', null, get_string('csvwithselecteddelimiter', 'data'), 'csv',
+            $optionattrs);
+        // Temporarily commenting out Excel export option. See MDL-19864.
         //$typesarray[] = $mform->createElement('radio', 'exporttype', null, get_string('excel', 'data'), 'xls');
-        $typesarray[] = $mform->createElement('radio', 'exporttype', null, get_string('ods', 'data'), 'ods');
-        $mform->addGroup($typesarray, 'exportar', '', array(''), false);
+        $typesarray[] = $mform->createElement('radio', 'exporttype', null, get_string('ods', 'data'), 'ods', $optionattrs);
+        $mform->addGroup($typesarray, 'exportar', get_string('exportformat', 'data'), null, false);
         $mform->addRule('exportar', null, 'required');
         $mform->setDefault('exporttype', 'csv');
+
+        // CSV delimiter list.
+        $choices = csv_import_reader::get_delimiter_list();
+        $key = array_search(';', $choices);
+        if ($key !== false) {
+            // Array $choices contains the semicolon -> drop it (because its encrypted form also contains a semicolon):
+            unset($choices[$key]);
+        }
+        $mform->addElement('select', 'delimiter_name', get_string('fielddelimiter', 'data'), $choices);
+        $mform->hideIf('delimiter_name', 'exporttype', 'neq', 'csv');
         if (array_key_exists('cfg', $choices)) {
             $mform->setDefault('delimiter_name', 'cfg');
         } else if (get_string('listsep', 'langconfig') == ';') {
@@ -57,38 +63,56 @@ class mod_data_export_form extends moodleform {
         } else {
             $mform->setDefault('delimiter_name', 'comma');
         }
-        $mform->addElement('header', 'notice', get_string('chooseexportfields', 'data'));
+
+        // Fields to be exported.
+        $mform->addElement('header', 'exportfieldsheader', get_string('chooseexportfields', 'data'));
+        $mform->setExpanded('exportfieldsheader');
         $numfieldsthatcanbeselected = 0;
-        foreach($this->_datafields as $field) {
-            if($field->text_export_supported()) {
+        $exportfields = [];
+        $unsupportedfields = [];
+        foreach ($this->_datafields as $field) {
+            $label = get_string('fieldnametype', 'data', (object)['name' => $field->field->name, 'type' => $field->name()]);
+            if ($field->text_export_supported()) {
                 $numfieldsthatcanbeselected++;
-                $html = '<div title="' . s($field->field->description) . '" ' .
-                        'class="d-inline-block">' . $field->field->name . '</div>';
-                $name = ' (' . $field->name() . ')';
-                $mform->addElement('advcheckbox', 'field_' . $field->field->id, $html, $name, array('group' => 1));
+                $exportfields[] = $mform->createElement('advcheckbox', 'field_' . $field->field->id, '', $label,
+                    array_merge(['group' => 1], $optionattrs));
                 $mform->setDefault('field_' . $field->field->id, 1);
             } else {
-                $a = new stdClass();
-                $a->fieldtype = $field->name();
-                $str = get_string('unsupportedexport', 'data', $a);
-                $mform->addElement('static', 'unsupported' . $field->field->id, $field->field->name, $str);
+                $unsupportedfields[] = $label;
             }
         }
+        $mform->addGroup($exportfields, 'exportfields', get_string('selectfields', 'data'), ['<br>'], false);
+
         if ($numfieldsthatcanbeselected > 1) {
             $this->add_checkbox_controller(1, null, null, 1);
         }
+
+        // List fields that cannot be exported.
+        if (!empty($unsupportedfields)) {
+            $unsupportedfieldslist = html_writer::tag('p', get_string('unsupportedfieldslist', 'data'), ['class' => 'mt-1']);
+            $unsupportedfieldslist .= html_writer::alist($unsupportedfields);
+            $mform->addElement('static', 'unsupportedfields', get_string('unsupportedfields', 'data'), $unsupportedfieldslist);
+        }
+
+        // Export options.
+        $mform->addElement('header', 'exportoptionsheader', get_string('exportoptions', 'data'));
+        $mform->setExpanded('exportoptionsheader');
+        $exportoptions = [];
         if (core_tag_tag::is_enabled('mod_data', 'data_records')) {
-            $mform->addElement('checkbox', 'exporttags', get_string('includetags', 'data'));
+            $exportoptions[] = $mform->createElement('checkbox', 'exporttags', get_string('includetags', 'data'), '', $optionattrs);
             $mform->setDefault('exporttags', 1);
         }
         $context = context_module::instance($this->_cm->id);
         if (has_capability('mod/data:exportuserinfo', $context)) {
-            $mform->addElement('checkbox', 'exportuser', get_string('includeuserdetails', 'data'));
+            $exportoptions[] = $mform->createElement('checkbox', 'exportuser', get_string('includeuserdetails', 'data'), '',
+                $optionattrs);
         }
-        $mform->addElement('checkbox', 'exporttime', get_string('includetime', 'data'));
+        $exportoptions[] = $mform->createElement('checkbox', 'exporttime', get_string('includetime', 'data'), '', $optionattrs);
         if ($this->_data->approval) {
-            $mform->addElement('checkbox', 'exportapproval', get_string('includeapproval', 'data'));
+            $exportoptions[] = $mform->createElement('checkbox', 'exportapproval', get_string('includeapproval', 'data'), '',
+                $optionattrs);
         }
+        $mform->addGroup($exportoptions, 'exportoptions', get_string('selectexportoptions', 'data'), ['<br>'], false);
 
         $this->add_action_buttons(true, get_string('exportentries', 'data'));
     }
index d96f7f5..1c5be54 100644 (file)
@@ -83,7 +83,7 @@ $string['csvfailed'] = 'Unable to read the raw data from the CSV file';
 $string['csvfile'] = 'CSV file';
 $string['csvimport'] = 'CSV file import';
 $string['csvimport_help'] = 'Entries may be imported via a plain text file with a list of field names as the first line, then the data, with one record per line.';
-$string['csvwithselecteddelimiter'] = '<acronym title="Comma Separated Values">CSV</acronym> text with selected delimiter:';
+$string['csvwithselecteddelimiter'] = '<abbr title="Comma Separated Values">CSV</abbr>';
 $string['data:addinstance'] = 'Add a new database';
 $string['data:approve'] = 'Approve and undo approved entries';
 $string['data:comment'] = 'Write comments';
@@ -153,6 +153,8 @@ $string['exportaszip'] = 'Export as zip';
 $string['exportaszip_help'] = 'The export as zip feature allows you to save the templates and fields as a preset zip for download. The zip may then be imported to another course.';
 $string['exportedtozip'] = 'Exported to temporary zip...';
 $string['exportentries'] = 'Export entries';
+$string['exportformat'] = 'Export format';
+$string['exportoptions'] = 'Export options';
 $string['exportownentries'] = 'Export your own entries only? ({$a->mine}/{$a->all})';
 $string['failedpresetdelete'] = 'Error deleting a preset!';
 $string['fieldadded'] = 'Field added';
@@ -169,6 +171,7 @@ $string['fieldmappings'] = 'Field mappings';
 $string['fieldmappings_help'] = 'This menu allows you to keep the data from the existing database. To preserve the data in a field, you must map it to a new field, where the data will appear. Any field can also be left blank, with no information copied into it. Any old field not mapped to a new one will be lost and all its data removed.
 You can only map fields of the same type, so each drop-down menu will have different fields in it. Also, you must be careful not to try and map one old field to more than one new field.';
 $string['fieldname'] = 'Field name';
+$string['fieldnametype'] = '{$a->name} ({$a->type})';
 $string['fieldnotmatched'] = 'The following fields in your file are not known in this database: {$a}';
 $string['fieldoptions'] = 'Options (one per line)';
 $string['fields'] = 'Fields';
@@ -195,8 +198,8 @@ $string['headerlisttemplate'] = 'Defines browsing interface for multiple entries
 $string['headerrsstemplate'] = 'Defines appearance of entries in RSS feeds';
 $string['headersingletemplate'] = 'Defines browsing interface for a single entry';
 $string['checkbox'] = 'Checkbox';
-$string['chooseexportfields'] = 'Choose the fields you wish to export:';
-$string['chooseexportformat'] = 'Choose the format you wish to export to:';
+$string['chooseexportfields'] = 'Choose the fields you wish to export';
+$string['chooseexportformat'] = 'Choose the format you wish to export to';
 $string['chooseorupload'] = 'Choose file';
 $string['expired'] = 'Sorry, this activity closed on {$a} and is no longer available';
 $string['importentries'] = 'Import entries';
@@ -290,7 +293,7 @@ $string['number'] = 'Number';
 $string['numberrssarticles'] = 'Entries in the RSS feed';
 $string['numnotapproved'] = 'Pending';
 $string['numrecords'] = '{$a} entries';
-$string['ods'] = '<acronym title="OpenDocument Spreadsheet">ODS</acronym> (OpenOffice)';
+$string['ods'] = '<abbr title="OpenDocument Spreadsheet">ODS</abbr>&nbsp;(OpenOffice)';
 $string['openafterclose'] = 'You have specified an open date after the close date';
 $string['optionaldescription'] = 'Short description (optional)';
 $string['optionalfilename'] = 'Filename (optional)';
@@ -366,6 +369,8 @@ $string['search'] = 'Search';
 $string['search:activity'] = 'Database - activity information';
 $string['search:entry'] = 'Database - entries';
 $string['selectedrequired'] = 'All selected required';
+$string['selectfields'] = 'Select fields';
+$string['selectexportoptions'] = 'Select export options';
 $string['showall'] = 'Show all entries';
 $string['single'] = 'View single';
 $string['singletemplate'] = 'Single template';
@@ -386,7 +391,8 @@ $string['timemodified'] = 'Time modified';
 $string['todatabase'] = 'to this database.';
 $string['type'] = 'Field type';
 $string['undefinedprocessactionmethod'] = 'No action method defined in Data_Preset to handle action "{$a}".';
-$string['unsupportedexport'] = '({$a->fieldtype}) cannot be exported.';
+$string['unsupportedfields'] = 'Unsupported fields';
+$string['unsupportedfieldslist'] = 'The following fields cannot be exported:';
 $string['updatefield'] = 'Update an existing field';
 $string['uploadfile'] = 'Upload file';
 $string['uploadrecords'] = 'Upload entries from a file';
@@ -406,3 +412,6 @@ $string['viewfromdate'] = 'Read only from';
 $string['viewtodate'] = 'Read only to';
 $string['viewtodatevalidation'] = 'The read only to date cannot be before the read only from date.';
 $string['wrongdataid'] = 'Wrong data id provided';
+
+// Deprecated since Moodle 3.11.
+$string['unsupportedexport'] = '({$a->fieldtype}) cannot be exported.';
index 61bdfee..95b4a93 100644 (file)
@@ -94,7 +94,6 @@ Feature: Users can view and search database entries
       | Test field name   | Student original entry tagged   |
       | Test field 2 name | Student original entry tagged 2 |
     And I set the field with xpath "//div[@class='datatagcontrol']//input[@type='text']" to "Tag1"
-    And I click on "[data-value='Tag1']" "css_element"
     And I press "Save and view"
     And I should see "Student original entry"
     And I should see "Tag1" in the "div.tag_list" "css_element"
index 112ead2..cb66f94 100644 (file)
@@ -136,8 +136,7 @@ Feature: Mapping courses in a feedback
     And I am on site homepage
     And I follow "Course feedback"
     And I follow "Map feedback to courses"
-    And I set the field "Courses" to "Course 2"
-    And I set the field "Courses" to "Course 3"
+    And I set the field "Courses" to "Course 2, Course 3"
     And I press "Save changes"
     And I should see "Course mapping has been changed"
     And I log out
@@ -224,7 +223,6 @@ Feature: Mapping courses in a feedback
     And I should see "1 (33.33 %)" in the "option d" "table_row"
     And I should see "2 (66.67 %)" in the "option e" "table_row"
     And I should see "0" in the "option f" "table_row"
-    And I log out
 
   Scenario: Site feedback deletion hides feedback block completely
     When I log in as "manager"
@@ -239,4 +237,3 @@ Feature: Mapping courses in a feedback
     Then "Feedback" "block" should not exist
     And I am on "Course 1" course homepage
     And "Feedback" "block" should not exist
-    And I log out
index dbcca3b..b26e61a 100644 (file)
@@ -130,7 +130,6 @@ Feature: The forum search allows users to perform advanced searches for forum po
     And I press "Search forums"
     And I should see "Advanced search"
     And I set the field "Is tagged with" to "SearchedTag"
-    And I press the enter key
     When I press "Search forums"
     Then I should see "My subject"
     And I should not see "Your subjective"
index 448fc01..ffedbbf 100644 (file)
@@ -101,14 +101,23 @@ class registration_helper {
         $ltitoolconfiguration = self::get_parameter($registrationpayload,
             'https://purl.imsglobal.org/spec/lti-tool-configuration', true);
 
-        $domain = self::get_parameter($ltitoolconfiguration, 'domain', true);
-        $targetlinkuri = self::get_parameter($ltitoolconfiguration, 'target_link_uri', true);
+        $domain = self::get_parameter($ltitoolconfiguration, 'domain', false);
+        $targetlinkuri = self::get_parameter($ltitoolconfiguration, 'target_link_uri', false);
         $customparameters = self::get_parameter($ltitoolconfiguration, 'custom_parameters', false);
         $scopes = explode(" ", self::get_parameter($registrationpayload, 'scope', false) ?? '');
         $claims = self::get_parameter($ltitoolconfiguration, 'claims', false);
         $messages = $ltitoolconfiguration['messages'] ?? [];
         $description = self::get_parameter($ltitoolconfiguration, 'description', false);
 
+        // Validate domain and target link.
+        if (empty($domain)) {
+            throw new registration_exception('missing_domain', 400);
+        }
+        $targetlinkuri = $targetlinkuri ?: 'https://'.$domain;
+        if ($domain !== lti_get_domain_from_url($targetlinkuri)) {
+            throw new registration_exception('domain_targetlinkuri_mismatch', 400);
+        }
+
         // Validate response type.
         // According to specification, for this scenario, id_token must be explicitly set.
         if (!in_array('id_token', $responsetypes)) {
@@ -245,7 +254,7 @@ class registration_helper {
         $registrationresponse['initiate_login_uri'] = $config->lti_initiatelogin;
         $registrationresponse['grant_types'] = ['client_credentials', 'implicit'];
         $registrationresponse['redirect_uris'] = explode(PHP_EOL, $config->lti_redirectionuris);
-        $registrationresponse['application_type'] = ['web'];
+        $registrationresponse['application_type'] = 'web';
         $registrationresponse['token_endpoint_auth_method'] = 'private_key_jwt';
         $registrationresponse['client_name'] = $config->lti_typename;
         $registrationresponse['logo_uri'] = $config->lti_icon ?? '';
@@ -288,12 +297,12 @@ class registration_helper {
         $registrationresponse['scope'] = implode(' ', $scopesresponse);
 
         $claimsresponse = ['sub', 'iss'];
-        if ($config->lti_sendname = LTI_SETTING_ALWAYS) {
+        if ($config->lti_sendname == LTI_SETTING_ALWAYS) {
             $claimsresponse[] = 'name';
             $claimsresponse[] = 'family_name';
-            $claimsresponse[] = 'middle_name';
+            $claimsresponse[] = 'given_name';
         }
-        if ($config->lti_sendemailaddr = LTI_SETTING_ALWAYS) {
+        if ($config->lti_sendemailaddr == LTI_SETTING_ALWAYS) {
             $claimsresponse[] = 'email';
         }
         $lticonfigurationresponse['claims'] = $claimsresponse;
index 33854f1..12969d1 100644 (file)
@@ -2321,7 +2321,8 @@ function lti_get_lti_types_by_course($courseid, $coursevisible = null) {
                 FROM {lti_types}
                WHERE coursevisible $coursevisiblesql
                  AND ($coursecond)
-                 AND state = :active";
+                 AND state = :active
+            ORDER BY name ASC";
 
     return $DB->get_records_sql($query,
         array('siteid' => $SITE->id, 'courseid' => $courseid, 'active' => LTI_TOOL_STATE_CONFIGURED) + $coursevisparams);
index 6de875d..7d616c3 100644 (file)
@@ -51,7 +51,7 @@ $conf = [
     'https://purl.imsglobal.org/spec/lti-platform-configuration' => [
         'product_family_code' => 'moodle',
         'version' => $CFG->release,
-        'messages_supported' => ['LtiResourceLink', 'LtiDeepLinkingRequest'],
+        'messages_supported' => ['LtiResourceLinkRequest', 'LtiDeepLinkingRequest'],
         'placements' => ['AddContentMenu'],
         'variables' => array_keys(lti_get_capabilities())
     ]
index af67829..4f789f5 100644 (file)
@@ -162,6 +162,8 @@ EOD;
         $this->assertEquals(LTI_SETTING_DELEGATE, $config->lti_acceptgrades);
         $this->assertEquals(1, $config->ltiservice_memberships);
         $this->assertEquals(0, $config->ltiservice_toolsettings);
+        $this->assertEquals('client.example.org', $config->lti_tooldomain);
+        $this->assertEquals('https://client.example.org/lti', $config->lti_toolurl);
         $this->assertEquals(LTI_SETTING_ALWAYS, $config->lti_sendname);
         $this->assertEquals(LTI_SETTING_ALWAYS, $config->lti_sendemailaddr);
         $this->assertEquals(1, $config->lti_contentitem);
@@ -236,6 +238,51 @@ EOD;
         registration_helper::registration_to_config($registration, 'TheClientId');
     }
 
+    /**
+     * Validation Test: no domain nor targetlinkuri is rejected.
+     */
+    public function test_validation_missing_domain_targetlinkuri() {
+        $registration = json_decode($this->registrationminimaljson, true);
+        $this->expectException(registration_exception::class);
+        $this->expectExceptionCode(400);
+        unset($registration['https://purl.imsglobal.org/spec/lti-tool-configuration']['domain']);
+        unset($registration['https://purl.imsglobal.org/spec/lti-tool-configuration']['target_link_uri']);
+        registration_helper::registration_to_config($registration, 'TheClientId');
+    }
+
+    /**
+     * Validation Test: mismatch between domain and targetlinkuri is rejected.
+     */
+    public function test_validation_domain_targetlinkuri_match() {
+        $registration = json_decode($this->registrationminimaljson, true);
+        $this->expectException(registration_exception::class);
+        $this->expectExceptionCode(400);
+        $registration['https://purl.imsglobal.org/spec/lti-tool-configuration']['domain'] = 'not.the.right.domain';
+        registration_helper::registration_to_config($registration, 'TheClientId');
+    }
+
+    /**
+     * Validation Test: domain is required.
+     */
+    public function test_validation_domain_targetlinkuri_onlylink() {
+        $registration = json_decode($this->registrationminimaljson, true);
+        unset($registration['https://purl.imsglobal.org/spec/lti-tool-configuration']['domain']);
+        $this->expectException(registration_exception::class);
+        $this->expectExceptionCode(400);
+        $config = registration_helper::registration_to_config($registration, 'TheClientId');
+    }
+
+    /**
+     * Validation Test: base url (targetlinkuri) is built from domain if not present.
+     */
+    public function test_validation_domain_targetlinkuri_onlydomain() {
+        $registration = json_decode($this->registrationminimaljson, true);
+        unset($registration['https://purl.imsglobal.org/spec/lti-tool-configuration']['target_link_uri']);
+        $config = registration_helper::registration_to_config($registration, 'TheClientId');
+        $this->assertEquals('client.example.org', $config->lti_tooldomain);
+        $this->assertEquals('https://client.example.org', $config->lti_toolurl);
+    }
+
     /**
      * Test the transformation from lti config to OpenId LTI Client Registration response.
      */
@@ -265,5 +312,34 @@ EOD;
         $dlmsg = $lti['messages'][0];
         $this->assertEquals($dlmsgorig['type'], $dlmsg['type']);
         $this->assertEquals($dlmsgorig['target_link_uri'], $dlmsg['target_link_uri']);
+        $this->assertTrue(in_array('iss', $lti['claims']));
+        $this->assertTrue(in_array('sub', $lti['claims']));
+        $this->assertTrue(in_array('email', $lti['claims']));
+        $this->assertTrue(in_array('family_name', $lti['claims']));
+        $this->assertTrue(in_array('given_name', $lti['claims']));
+        $this->assertTrue(in_array('name', $lti['claims']));
     }
+
+    /**
+     * Test the transformation from lti config to OpenId LTI Client Registration response for the minimal version.
+     */
+    public function test_config_to_registration_minimal() {
+        $orig = json_decode($this->registrationminimaljson, true);
+        $reg = registration_helper::config_to_registration(registration_helper::registration_to_config($orig, 'clid'), 12);
+        $this->assertEquals('clid', $reg['client_id']);
+        $this->assertEquals($orig['response_types'], $reg['response_types']);
+        $this->assertEquals($orig['initiate_login_uri'], $reg['initiate_login_uri']);
+        $this->assertEquals($orig['redirect_uris'], $reg['redirect_uris']);
+        $this->assertEquals($orig['jwks_uri'], $reg['jwks_uri']);
+        $this->assertEquals('', $reg['scope']);
+        $ltiorig = $orig['https://purl.imsglobal.org/spec/lti-tool-configuration'];
+        $lti = $reg['https://purl.imsglobal.org/spec/lti-tool-configuration'];
+        $this->assertTrue(in_array('iss', $lti['claims']));
+        $this->assertTrue(in_array('sub', $lti['claims']));
+        $this->assertFalse(in_array('email', $lti['claims']));
+        $this->assertFalse(in_array('family_name', $lti['claims']));
+        $this->assertFalse(in_array('given_name', $lti['claims']));
+        $this->assertFalse(in_array('name', $lti['claims']));
+    }
+
 }
index f0792b1..b5dc3c1 100644 (file)
@@ -49,7 +49,7 @@ class helper {
             /** @var \paygw_paypal\gateway $classname */
             $classname = '\paygw_' . $plugin . '\gateway';
 
-            $currencies += component_class_callback($classname, 'get_supported_currencies', [], []);
+            $currencies = array_merge($currencies, component_class_callback($classname, 'get_supported_currencies', [], []));
         }
 
         $currencies = array_unique($currencies);
index 9b4d45e..aaf8d2e 100644 (file)
Binary files a/question/type/ddmarker/amd/build/question.min.js and b/question/type/ddmarker/amd/build/question.min.js differ
index 8562ce1..fa7d568 100644 (file)
Binary files a/question/type/ddmarker/amd/build/question.min.js.map and b/question/type/ddmarker/amd/build/question.min.js.map differ
index 5d516de..ca8b9bc 100644 (file)
@@ -473,8 +473,10 @@ define(['jquery', 'core/dragdrop', 'qtype_ddmarker/shapes', 'core/key_codes'], f
             var drag = $(draghome);
             var placeHolder = drag.clone();
             placeHolder.removeClass();
-            placeHolder.addClass('marker choice' +
-                thisQ.getChoiceNoFromElement(drag) + ' dragno' + thisQ.getDragNo(drag) + ' dragplaceholder');
+            placeHolder.addClass('marker');
+            placeHolder.addClass('choice' + thisQ.getChoiceNoFromElement(drag));
+            placeHolder.addClass(thisQ.getDragNoClass(drag, false));
+            placeHolder.addClass('dragplaceholder');
             drag.before(placeHolder);
         });
     };
@@ -489,6 +491,26 @@ define(['jquery', 'core/dragdrop', 'qtype_ddmarker/shapes', 'core/key_codes'], f
         return this.getClassnameNumericSuffix(drag, 'dragno');
     };
 
+    /**
+     * Get the drag number prefix of a drag.
+     *
+     * @param {jQuery} drag the drag.
+     * @param {Boolean} includeSelector include the CSS selector prefix or not.
+     * @return {String} Class name
+     */
+    DragDropMarkersQuestion.prototype.getDragNoClass = function(drag, includeSelector) {
+        var className = 'dragno' + this.getDragNo(drag);
+        if (this.isInfiniteDrag(drag)) {
+            className = 'infinite';
+        }
+
+        if (includeSelector) {
+            return '.' + className;
+        }
+
+        return className;
+    };
+
     /**
      * Get drag clone for a given drag.
      *
@@ -497,7 +519,7 @@ define(['jquery', 'core/dragdrop', 'qtype_ddmarker/shapes', 'core/key_codes'], f
      */
     DragDropMarkersQuestion.prototype.getDragClone = function(drag) {
         return this.getRoot().find('.draghomes' + ' span.marker' +
-            '.choice' + this.getChoiceNoFromElement(drag) + '.dragno' + this.getDragNo(drag) + '.dragplaceholder');
+            '.choice' + this.getChoiceNoFromElement(drag) + this.getDragNoClass(drag, true) + '.dragplaceholder');
     };
 
     /**
@@ -555,20 +577,21 @@ define(['jquery', 'core/dragdrop', 'qtype_ddmarker/shapes', 'core/key_codes'], f
         var inputNode = this.getInput(drag),
             noOfDrags = Number(this.getClassnameNumericSuffix(inputNode, 'noofdrags')),
             displayedDragsInDropArea = this.getRoot().find('div.droparea .marker.choice' +
-                this.getChoiceNoFromElement(drag) + '.dragno' + this.getDragNo(drag)).length,
+                this.getChoiceNoFromElement(drag) + this.getDragNoClass(drag, true)).length,
             displayedDragsInDragHomes = this.getRoot().find('div.draghomes .marker.choice' +
-                this.getChoiceNoFromElement(drag) + '.dragno' + this.getDragNo(drag)).not('.dragplaceholder').length;
+                this.getChoiceNoFromElement(drag) + this.getDragNoClass(drag, true)).not('.dragplaceholder').length;
 
-        if (displayedDragsInDropArea < noOfDrags && displayedDragsInDragHomes === 0) {
-            var dragclone = drag.clone();
-            dragclone.addClass('unneeded')
+        if ((this.isInfiniteDrag(drag) ||
+                !this.isInfiniteDrag(drag) && displayedDragsInDropArea < noOfDrags) && displayedDragsInDragHomes === 0) {
+            var dragClone = drag.clone();
+            dragClone.addClass('unneeded')
                 .css('top', '')
                 .css('left', '')
                 .css('transform', '');
             this.getDragClone(drag)
                 .removeClass('active')
-                .after(dragclone);
-            questionManager.addEventHandlersToMarker(dragclone);
+                .after(dragClone);
+            questionManager.addEventHandlersToMarker(dragClone);
         }
     };
 
@@ -578,11 +601,12 @@ define(['jquery', 'core/dragdrop', 'qtype_ddmarker/shapes', 'core/key_codes'], f
      * @param {jQuery} drag the item to place.
      */
     DragDropMarkersQuestion.prototype.removeDragIfNeeded = function(drag) {
-        var displayeddrags = this.getRoot().find('div.draghomes .marker.choice' +
-            this.getChoiceNoFromElement(drag) + '.dragno' + this.getDragNo(drag)).not('.dragplaceholder').length;
-        if (displayeddrags > 1) {
-            this.getRoot().find('div.draghomes .marker.choice' +
-                this.getChoiceNoFromElement(drag) + '.dragno' + this.getDragNo(drag)).not('.dragplaceholder').first().remove();
+        var dragsInHome = this.getRoot().find('div.draghomes .marker.choice' +
+            this.getChoiceNoFromElement(drag) + this.getDragNoClass(drag, true)).not('.dragplaceholder');
+        var displayedDrags = dragsInHome.length;
+        while (displayedDrags > 1) {
+            dragsInHome.first().remove();
+            displayedDrags--;
         }
     };
 
@@ -631,6 +655,15 @@ define(['jquery', 'core/dragdrop', 'qtype_ddmarker/shapes', 'core/key_codes'], f
         });
     };
 
+    /**
+     * Check if the given drag is in infinite mode or not.
+     *
+     * @param {jQuery} drag The drag item need to check.
+     */
+    DragDropMarkersQuestion.prototype.isInfiniteDrag = function(drag) {
+        return drag.hasClass('infinite');
+    };
+
     /**
      * Singleton that tracks all the DragDropToTextQuestions on this page, and deals
      * with event dispatching.
index 62368c9..db8a055 100644 (file)
@@ -47,7 +47,6 @@ Feature: See the competencies for an activity
   Scenario: Go to the competency breakdown report
     When I navigate to "Reports > Competency breakdown" in current page administration
     And I set the field "Filter competencies by resource or activity" to "PageName1"
-    And I press the enter key
     Then I should see "Test-Comp1"
     And I should not see "Test-Comp2"
     And I click on "Not rated" "link"
@@ -55,7 +54,6 @@ Feature: See the competencies for an activity
     And I set the field "Rating" to "A"
     And I click on "Rate" "button" in the ".competency-grader" "css_element"
     And I click on "Close" "button"
-    And I set the field "Filter competencies by resource or activity" to "No filters applied"
-    And I press the enter key
+    And I click on "PageName1" "autocomplete_selection"
     And I should see "Test-Comp1"
     And I should see "Test-Comp2"
index 613162f..8952583 100644 (file)
@@ -112,7 +112,8 @@ $THEME->layouts = [
     // Embeded pages, like iframe/object embeded in moodleform - it needs as much space as possible.
     'embedded' => array(
         'file' => 'embedded.php',
-        'regions' => array()
+        'regions' => array('side-pre'),
+        'defaultregion' => 'side-pre',
     ),
     // Used during upgrade and install, and for the 'This site is undergoing maintenance' message.
     // This must not have any blocks, links, or API calls that would lead to database or cache interaction.
index 5469e1b..762d7f3 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
+$fakeblockshtml = $OUTPUT->blocks('side-pre', array(), 'aside', true);
+$hasfakeblocks = strpos($fakeblockshtml, 'data-block="_fake"') !== false;
+
 $templatecontext = [
-    'sitename' => format_string($SITE->shortname, true, ['context' => context_course::instance(SITEID), "escape" => false]),
-    'output' => $OUTPUT
+    'output' => $OUTPUT,
+    'hasfakeblocks' => $hasfakeblocks,
+    'fakeblocks' => $fakeblockshtml,
 ];
 
 echo $OUTPUT->render_from_template('theme_boost/embedded', $templatecontext);
index 7a9d4bf..95ac0b7 100644 (file)
@@ -374,3 +374,33 @@ body.drawer-open-left #region-main.has-blocks {
     border: 2px dashed $gray-800;
     margin: 4px 0;
 }
+
+.pagelayout-embedded {
+    .has-fake-blocks {
+        padding: 1rem;
+        display: flex;
+    }
+
+    .has-fake-blocks .embedded-main {
+        order: 0;
+        width: calc(100% - #{$blocks-column-width});
+        margin-right: 1rem;
+    }
+
+    .embedded-blocks {
+        order: 1;
+        width: $blocks-column-width;
+    }
+
+    @media (max-width: 767.98px) {
+        .has-fake-blocks {
+            display: block;
+        }
+        .has-fake-blocks .embedded-main {
+            width: 100%;
+        }
+        .embedded-blocks {
+            width: 100%;
+        }
+    }
+}
index ca67117..987fed9 100644 (file)
@@ -1258,12 +1258,6 @@ body#page-lib-editor-tinymce-plugins-moodlemedia-preview {
     overflow: auto;
 }
 
-.moodle-dialogue-base .moodle-dialogue-fullscreen .closebutton {
-    width: 28px;
-    height: 16px;
-    background-size: 100%;
-}
-
 .moodle-dialogue-base .moodle-dialogue-wrap {
     background-color: #fff;
     border: 1px solid #ccc;
index ea0931a..e5a42f3 100644 (file)
@@ -354,14 +354,14 @@ body.path-question-type {
 
 .que .correctness {
     &.correct {
-        background-color: $success;
+        @include badge-variant($success);
     }
     &.partiallycorrect {
-        background-color: $warning;
+        @include badge-variant($warning);
     }
     &.notanswered,
     &.incorrect {
-        background-color: $danger;
+        @include badge-variant($danger);
     }
 }
 
index 5241ce3..045bdff 100644 (file)
@@ -10694,11 +10694,6 @@ body#page-lib-editor-tinymce-plugins-moodlemedia-preview {
 .moodle-dialogue-base .moodle-dialogue-fullscreen .moodle-dialogue-content {
   overflow: auto; }
 
-.moodle-dialogue-base .moodle-dialogue-fullscreen .closebutton {
-  width: 28px;
-  height: 16px;
-  background-size: 100%; }
-
 .moodle-dialogue-base .moodle-dialogue-wrap {
   background-color: #fff;
   border: 1px solid #ccc; }
@@ -12775,6 +12770,27 @@ input[disabled] {
   border: 2px dashed #343a40;
   margin: 4px 0; }
 
+.pagelayout-embedded .has-fake-blocks {
+  padding: 1rem;
+  display: flex; }
+
+.pagelayout-embedded .has-fake-blocks .embedded-main {
+  order: 0;
+  width: calc(100% - 360px);
+  margin-right: 1rem; }
+
+.pagelayout-embedded .embedded-blocks {
+  order: 1;
+  width: 360px; }
+
+@media (max-width: 767.98px) {
+  .pagelayout-embedded .has-fake-blocks {
+    display: block; }
+  .pagelayout-embedded .has-fake-blocks .embedded-main {
+    width: 100%; }
+  .pagelayout-embedded .embedded-blocks {
+    width: 100%; } }
+
 .navbar {
   max-height: 50px; }
 
@@ -15763,13 +15779,34 @@ body.path-question-type {
   margin: 0 0 0.5em; }
 
 .que .correctness.correct {
+  color: #fff;
   background-color: #357a32; }
+  a.que .correctness.correct:hover, a.que .correctness.correct:focus {
+    color: #fff;
+    background-color: #255623; }
+  a.que .correctness.correct:focus, a.que .correctness.correct.focus {
+    outline: 0;
+    box-shadow: 0 0 0 0.2rem rgba(53, 122, 50, 0.5); }
 
 .que .correctness.partiallycorrect {
+  color: #212529;
   background-color: #f0ad4e; }
+  a.que .correctness.partiallycorrect:hover, a.que .correctness.partiallycorrect:focus {
+    color: #212529;
+    background-color: #ec971f; }
+  a.que .correctness.partiallycorrect:focus, a.que .correctness.partiallycorrect.focus {
+    outline: 0;
+    box-shadow: 0 0 0 0.2rem rgba(240, 173, 78, 0.5); }
 
 .que .correctness.notanswered, .que .correctness.incorrect {
+  color: #fff;
   background-color: #ca3120; }
+  a.que .correctness.notanswered:hover, a.que .correctness.notanswered:focus, .que .correctness.incorrect:hover, .que .correctness.incorrect:focus {
+    color: #fff;
+    background-color: #9e2619; }
+  a.que .correctness.notanswered:focus, a.que .correctness.notanswered.focus, .que .correctness.incorrect:focus, .que .correctness.incorrect.focus {
+    outline: 0;
+    box-shadow: 0 0 0 0.2rem rgba(202, 49, 32, 0.5); }
 
 .que .qtext {
   margin-bottom: 1.5em; }
index 5d7a26d..a9a419f 100644 (file)
 
     Context variables required for this template:
     * output - The core renderer for the page
+    * hasfakeblocks - true if there are fake blocks on this page
+    * fakeblocks - HTML for the fake blocks
 
     Example context (json):
     {
         "output": {
             "doctype": "<!DOCTYPE html>",
+            "htmlattributes": "The attributes that should be added to the <html> tag",
             "page_title": "Test page",
             "favicon": "favicon.ico",
-            "main_content": "<h1>Headings make html validators happier</h1>"
-         }
+            "standard_head_html": "The standard tags that should be included in the <head> tag",
+            "body_attributes": "The attributes to use within the body tag",
+            "standard_top_of_body_html": "The standard tags that should be output just inside the start of the <body> tag",
+            "main_content": "<h1>Headings make html validators happier</h1>",
+            "standard_end_of_body_html": "The standard tags that should be output after everything else"
+         },
+         "hasfakeblocks": true,
+         "fakeblocks": "<h2>Fake blocks html goes here</h2>"
     }
 }}
 {{{ output.doctype }}}
 {{> core/local/toast/wrapper}}
 
 {{{ output.standard_top_of_body_html }}}
-<div id="page">
-    <div id="page-content" class="d-block">
+<div id="page" {{#hasfakeblocks}}class="has-fake-blocks"{{/hasfakeblocks}}>
+    {{#hasfakeblocks}}
+        <section class="embedded-blocks" aria-label="{{#str}}blocks{{/str}}">
+            {{{ fakeblocks }}}
+        </section>
+    {{/hasfakeblocks}}
+    <section class="embedded-main">
         {{{ output.main_content }}}
-    </div>
+    </section>
 </div>
 {{{ output.standard_end_of_body_html }}}
 </body>
index 0f07f2b..c104721 100644 (file)
@@ -10898,11 +10898,6 @@ body#page-lib-editor-tinymce-plugins-moodlemedia-preview {
 .moodle-dialogue-base .moodle-dialogue-fullscreen .moodle-dialogue-content {
   overflow: auto; }
 
-.moodle-dialogue-base .moodle-dialogue-fullscreen .closebutton {
-  width: 28px;
-  height: 16px;
-  background-size: 100%; }
-
 .moodle-dialogue-base .moodle-dialogue-wrap {
   background-color: #fff;
   border: 1px solid #ccc; }
@@ -12989,6 +12984,27 @@ input[disabled] {
   border: 2px dashed #343a40;
   margin: 4px 0; }
 
+.pagelayout-embedded .has-fake-blocks {
+  padding: 1rem;
+  display: flex; }
+
+.pagelayout-embedded .has-fake-blocks .embedded-main {
+  order: 0;
+  width: calc(100% - 360px);
+  margin-right: 1rem; }
+
+.pagelayout-embedded .embedded-blocks {
+  order: 1;
+  width: 360px; }
+
+@media (max-width: 767.98px) {
+  .pagelayout-embedded .has-fake-blocks {
+    display: block; }
+  .pagelayout-embedded .has-fake-blocks .embedded-main {
+    width: 100%; }
+  .pagelayout-embedded .embedded-blocks {
+    width: 100%; } }
+
 .navbar {
   max-height: 50px; }
 
@@ -15984,13 +16000,34 @@ body.path-question-type {
   margin: 0 0 0.5em; }
 
 .que .correctness.correct {
+  color: #fff;
   background-color: #357a32; }
+  a.que .correctness.correct:hover, a.que .correctness.correct:focus {
+    color: #fff;
+    background-color: #255623; }
+  a.que .correctness.correct:focus, a.que .correctness.correct.focus {
+    outline: 0;
+    box-shadow: 0 0 0 0.2rem rgba(53, 122, 50, 0.5); }
 
 .que .correctness.partiallycorrect {
+  color: #212529;
   background-color: #f0ad4e; }
+  a.que .correctness.partiallycorrect:hover, a.que .correctness.partiallycorrect:focus {
+    color: #212529;
+    background-color: #ec971f; }
+  a.que .correctness.partiallycorrect:focus, a.que .correctness.partiallycorrect.focus {
+    outline: 0;
+    box-shadow: 0 0 0 0.2rem rgba(240, 173, 78, 0.5); }
 
 .que .correctness.notanswered, .que .correctness.incorrect {
+  color: #fff;
   background-color: #ca3120; }
+  a.que .correctness.notanswered:hover, a.que .correctness.notanswered:focus, .que .correctness.incorrect:hover, .que .correctness.incorrect:focus {
+    color: #fff;
+    background-color: #9e2619; }
+  a.que .correctness.notanswered:focus, a.que .correctness.notanswered.focus, .que .correctness.incorrect:focus, .que .correctness.incorrect.focus {
+    outline: 0;
+    box-shadow: 0 0 0 0.2rem rgba(202, 49, 32, 0.5); }
 
 .que .qtext {
   margin-bottom: 1.5em; }
index a67d8a8..b52dbc4 100644 (file)
@@ -1,6 +1,9 @@
 This files describes API changes in /theme/* themes,
 information provided here is intended especially for theme designer.
 
+=== 3.11 ===
+* The classname 'viewmode-cobmined' in course/management.php has been changed to 'viewmode-combined'
+
 === 3.10 ===
 * The Bootstrap legacy css utilities from Bootstrap 2 and 4alpha have been removed.
 The syntax for the new Bootstrap 4.5 utility classes is {property}{sides}-{breakpoint}-{size} for sm, md, lg, and xl.
index dbf1130..90fdfea 100644 (file)
@@ -25,9 +25,8 @@ Feature: Edit user roles
     And I am on "Course 1" course homepage
     And I navigate to course participants
     And I click on "Student 1's role assignments" "link"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "student1" "table_row"
-    And I click on "Non-editing teacher" item in the autocomplete list
-    And I press the escape key
+    And I type "Non-editing teacher"
+    And I press the enter key
     When I click on "Save changes" "link"
     Then I should see "Student, Non-editing teacher" in the "Student 1" "table_row"
 
@@ -37,7 +36,6 @@ Feature: Edit user roles
     And I am on "Course 1" course homepage
     And I navigate to course participants
     And I click on "Student 1's role assignments" "link"
-    And I click on ".form-autocomplete-selection [aria-selected=true]" "css_element"
-    And I press the escape key
+    And I click on "Student" "autocomplete_selection"
     When I click on "Save changes" "link"
     Then I should see "No roles" in the "Student 1" "table_row"
index b957693..a7d6715 100644 (file)
@@ -11,13 +11,13 @@ Feature: Course participants can be filtered
       | Course 2 | C2        |     0     | ##4 months ago## |
       | Course 3 | C3        |     0     | ##3 months ago## |
     And the following "users" exist:
-      | username | firstname | lastname | email                | idnumber | country | city   | maildisplay |
-      | student1 | Student   | 1        | student1@example.com | SID1     |         | SCITY1 | 0           |
-      | student2 | Student   | 2        | student2@example.com | SID2     | GB      | SCITY2 | 1           |
-      | student3 | Student   | 3        | student3@example.com | SID3     | AU      | SCITY3 | 0           |
-      | student4 | Student   | 4        | student4@moodle.com  | SID4     | AT      | SCITY4 | 0           |
-      | student5 | Trendy    | Learnson | trendy@learnson.com  | SID5     | AU      | SCITY5 | 0           |
-      | teacher1 | Teacher   | 1        | teacher1@example.org | TID1     | US      | TCITY1 | 0           |
+      | username | firstname | lastname | email                     | idnumber | country | city   | maildisplay |
+      | student1 | Student   | 1        | student1@example.com      | SID1     |         | SCITY1 | 0           |
+      | student2 | Student   | 2        | student2@example.com      | SID2     | GB      | SCITY2 | 1           |
+      | student3 | Student   | 3        | student3@example.com      | SID3     | AU      | SCITY3 | 0           |
+      | student4 | Student   | 4        | student4@moodle.com       | SID4     | AT      | SCITY4 | 0           |
+      | student5 | Trendy    | Learnson | trendy@learnson.com       | SID5     | AU      | SCITY5 | 0           |
+      | patricia | Patricia  | Pea      | patricia.pea1@example.org | TID1     | US      | TCITY1 | 0           |
     And the following "course enrolments" exist:
       | user     | course | role           | status | timeend       |
       | student1 | C1     | student        |    0   |               |
@@ -31,9 +31,9 @@ Feature: Course participants can be filtered
       | student1 | C3     | student        |    0   |               |
       | student2 | C3     | student        |    0   |               |
       | student3 | C3     | student        |    0   |               |
-      | teacher1 | C1     | editingteacher |    0   |               |
-      | teacher1 | C2     | editingteacher |    0   |               |
-      | teacher1 | C3     | editingteacher |    0   |               |
+      | patricia | C1     | editingteacher |    0   |               |
+      | patricia | C2     | editingteacher |    0   |               |
+      | patricia | C3     | editingteacher |    0   |               |
     And the following "last access times" exist:
       | user     | course | lastaccess      |
       | student1 | C1     | ##yesterday##   |
@@ -58,23 +58,22 @@ Feature: Course participants can be filtered
 
   @javascript
   Scenario: No filters applied
-    Given I log in as "teacher1"
+    Given I log in as "patricia"
     And I am on "Course 1" course homepage
     And I navigate to course participants
     Then I should see "Student 1" in the "participants" "table"
     And I should see "Student 2" in the "participants" "table"
     And I should see "Student 3" in the "participants" "table"
-    And I should see "Teacher 1" in the "participants" "table"
+    And I should see "Patricia Pea" in the "participants" "table"
 
   @javascript
   Scenario Outline: Filter users for a course with a single value
-    Given I log in as "teacher1"
+    Given I log in as "patricia"
     And I am on "Course 1" course homepage
     And I navigate to course participants
     And I set the field "Match" in the "Filter 1" "fieldset" to "<matchtype>"
     And I set the field "type" in the "Filter 1" "fieldset" to "<filtertype>"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset"
-    And I click on "<filtervalue>" "list_item"
+    And I set the field "Type or select..." in the "Filter 1" "fieldset" to "<filtervalue>"
     When I click on "Apply filters" "button"
     Then I should see "<expected1>" in the "participants" "table"
     And I should see "<expected2>" in the "participants" "table"
@@ -84,30 +83,28 @@ Feature: Course participants can be filtered
     # Note the 'XX-IGNORE-XX' elements are for when there is less than 2 'not expected' items.
 
     Examples:
-      | matchtype | filtertype             | filtervalue | expected1 | expected2 | expected3 | notexpected1 | notexpected2 |
-      | Any       | Groups                 | No group    | Student 1 | Student 4 | Teacher 1 | Student 2    | Student 3    |
-      | All       | Groups                 | No group    | Student 1 | Student 4 | Teacher 1 | Student 2    | Student 3    |
-      | None      | Groups                 | No group    | Student 2 | Student 3 |           | Student 1    | Teacher 1    |
-      | Any       | Role                   | Student     | Student 1 | Student 2 | Student 3 | Teacher 1    | XX-IGNORE-XX |
-      | All       | Role                   | Student     | Student 1 | Student 2 | Student 3 | Teacher 1    | XX-IGNORE-XX |
-      | None      | Role                   | Student     | Teacher 1 |           |           | Student 1    | Student 2    |
-      | Any       | Status                 | Active      | Student 1 | Student 3 | Teacher 1 | Student 2    | Student 4    |
-      | All       | Status                 | Active      | Student 1 | Student 3 | Teacher 1 | Student 2    | Student 4    |
-      | None      | Status                 | Active      | Student 2 | Student 4 |           | Student 1    | Student 3    |
-      | Any       | Inactive for more than | 1 week      | Student 3 | Student 4 |           | Student 1    | Student 2    |
-      | All       | Inactive for more than | 1 week      | Student 3 | Student 4 |           | Student 1    | Student 2    |
-      | None      | Inactive for more than | 1 week      | Student 1 | Student 2 | Teacher 1 | Student 3    | XX-IGNORE-XX |
+      | matchtype | filtertype             | filtervalue | expected1    | expected2 | expected3    | notexpected1 | notexpected2 |
+      | Any       | Groups                 | No group    | Student 1    | Student 4 | Patricia Pea | Student 2    | Student 3    |
+      | All       | Groups                 | No group    | Student 1    | Student 4 | Patricia Pea | Student 2    | Student 3    |
+      | None      | Groups                 | No group    | Student 2    | Student 3 |              | Student 1    | Patricia Pea |
+      | Any       | Role                   | Student     | Student 1    | Student 2 | Student 3    | Patricia Pea | XX-IGNORE-XX |
+      | All       | Role                   | Student     | Student 1    | Student 2 | Student 3    | Patricia Pea | XX-IGNORE-XX |
+      | None      | Role                   | Student     | Patricia Pea |           |              | Student 1    | Student 2    |
+      | Any       | Status                 | Active      | Student 1    | Student 3 | Patricia Pea | Student 2    | Student 4    |
+      | All       | Status                 | Active      | Student 1    | Student 3 | Patricia Pea | Student 2    | Student 4    |
+      | None      | Status                 | Active      | Student 2    | Student 4 |              | Student 1    | Student 3    |
+      | Any       | Inactive for more than | 1 week      | Student 3    | Student 4 |              | Student 1    | Student 2    |
+      | All       | Inactive for more than | 1 week      | Student 3    | Student 4 |              | Student 1    | Student 2    |
+      | None      | Inactive for more than | 1 week      | Student 1    | Student 2 | Patricia Pea | Student 3    | XX-IGNORE-XX |
 
   @javascript
   Scenario Outline: Filter users for a course with multiple values for a single filter
-    Given I log in as "teacher1"
+    Given I log in as "patricia"
     And I am on "Course 1" course homepage
     And I navigate to course participants
     And I set the field "Match" in the "Filter 1" "fieldset" to "<matchtype>"
     And I set the field "type" in the "Filter 1" "fieldset" to "<filtertype>"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset"
-    And I click on "<filtervalue1>" "list_item"
-    And I click on "<filtervalue2>" "list_item"
+    And I set the field "Type or select..." in the "Filter 1" "fieldset" to "<filtervalue1>,<filtervalue2>"
     When I click on "Apply filters" "button"
     Then I should see "<expected1>" in the "participants" "table"
     And I should see "<expected2>" in the "participants" "table"
@@ -120,16 +117,15 @@ Feature: Course participants can be filtered
       | matchtype | filtertype | filtervalue1 | filtervalue2 | expected1 | expected2 | expected3 | notexpected1 | notexpected2 |
       | Any       | Groups     | Group 1      | Group 2      | Student 2 | Student 3 |           | Student 1    | XX-IGNORE-XX |
       | All       | Groups     | Group 1      | Group 2      | Student 2 |           |           | Student 1    | Student 3    |
-      | None      | Groups     | Group 1      | Group 2      | Student 1 | Teacher 1 |           | Student 2    | Student 3    |
+      | None      | Groups     | Group 1      | Group 2      | Student 1 | Patricia Pea |           | Student 2    | Student 3    |
 
   @javascript
   Scenario Outline: Filter users which are group members in several courses
-    Given I log in as "teacher1"
+    Given I log in as "patricia"
     And I am on "Course 3" course homepage
     And I navigate to course participants
     And I set the field "type" in the "Filter 1" "fieldset" to "<filtertype>"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset"
-    And I click on "<filtervalue>" "list_item"
+    And I set the field "Type or select..." in the "Filter 1" "fieldset" to "<filtervalue>"
     When I click on "Apply filters" "button"
     Then I should see "<expected1>" in the "participants" "table"
     And I should see "<expected2>" in the "participants" "table"
@@ -145,62 +141,84 @@ Feature: Course participants can be filtered
 
   @javascript
   Scenario: In separate groups mode, a student in a single group can only view and filter by users in their own group
-    Given I log in as "teacher1"
+    Given I log in as "patricia"
     And I am on "Course 1" course homepage
     And I navigate to course participants
+
     # Unsuspend student 2 for to improve coverage of this test.
     And I click on "Edit enrolment" "icon" in the "Student 2" "table_row"
     And I set the field "Status" to "Active"
     And I click on "Save changes" "button"
     And I log out
+
+    # Default view should have groups filter pre-set.
+    # Match:
+    #   Groups Any ["Group 2"].
+
     When I log in as "student3"
     And I am on "Course 1" course homepage
     And I navigate to course participants
-    # Default view should have groups filter pre-set.
+
     Then I should see "Student 2" in the "participants" "table"
     And I should see "Student 3" in the "participants" "table"
-    And I should not see "Student 1" in the "participants" "table"
     And I should see "Group 2" in the "Filter 1" "fieldset"
+    But I should not see "Student 1" in the "participants" "table"
     And I should not see "Group 1" in the "Filter 1" "fieldset"
-    And I should see "Student 2" in the "participants" "table"
-    And I should see "Student 3" in the "participants" "table"
-    And I should not see "Student 1" in the "participants" "table"
+
     # Testing result of removing groups filter row.
-    And I click on "Remove filter row" "button" in the "Filter 1" "fieldset"
-    And I should see "Student 2" in the "participants" "table"
+    # Match any available user.
+    When I click on "Remove filter row" "button" in the "Filter 1" "fieldset"
+
+    Then I should see "Student 2" in the "participants" "table"
     And I should see "Student 3" in the "participants" "table"
-    And I should not see "Student 1" in the "participants" "table"
+    But I should not see "Student 1" in the "participants" "table"
+
     # Testing result of applying groups filter manually.
-    And I set the field "Match" in the "Filter 1" "fieldset" to "Any"
+    # Match:
+    #   Group Any ["Group 2"].
+
+    # Match Groups Any ["Group 2"]
+    Given I set the field "Match" in the "Filter 1" "fieldset" to "Any"
     And I set the field "type" in the "Filter 1" "fieldset" to "Groups"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset"
-    And I should see "Group 2" in the ".form-autocomplete-suggestions" "css_element"
+    And I set the field "Type or select..." in the "Filter 1" "fieldset" to "Group 2"
+
+    And I open the autocomplete suggestions list in the "Filter 1" "fieldset"
     And I should not see "Group 1" in the ".form-autocomplete-suggestions" "css_element"
-    And I click on "Group 2" "list_item"
+
     And I click on "Apply filters" "button"
-    And I should see "Student 2" in the "participants" "table"
+
+    Then I should see "Student 2" in the "participants" "table"
     And I should see "Student 3" in the "participants" "table"
-    And I should not see "Student 1" in the "participants" "table"
+    But I should not see "Student 1" in the "participants" "table"
+
     # Testing result of removing groups filter by clearing all filters.
-    And I click on "Clear filters" "button"
-    And I should see "Student 2" in the "participants" "table"
+    # Match any available user.
+    When I click on "Clear filters" "button"
+
+    Then I should see "Student 2" in the "participants" "table"
     And I should see "Student 3" in the "participants" "table"
-    And I should not see "Student 1" in the "participants" "table"
+    But I should not see "Student 1" in the "participants" "table"
 
   @javascript
   Scenario: In separate groups mode, a student in multiple groups can only view and filter by users in their own groups
-    Given I log in as "teacher1"
+    Given I log in as "patricia"
     And I am on "Course 1" course homepage
     And I navigate to course participants
+
     # Unsuspend student 2 for to improve coverage of this test.
     And I click on "Edit enrolment" "icon" in the "Student 2" "table_row"
     And I set the field "Status" to "Active"
     And I click on "Save changes" "button"
     And I log out
+
     When I log in as "student2"
     And I am on "Course 1" course homepage
     And I navigate to course participants
+
     # Default view should have groups filter pre-set.
+    # Match:
+    #   Groups Any ["Group 1", "Group 2"].
+
     Then I should see "Student 2" in the "participants" "table"
     And I should see "Student 3" in the "participants" "table"
     And I should not see "Student 1" in the "participants" "table"
@@ -209,291 +227,416 @@ Feature: Course participants can be filtered
     And I should see "Student 2" in the "participants" "table"
     And I should see "Student 3" in the "participants" "table"
     And I should not see "Student 1" in the "participants" "table"
+
     # Testing result of removing groups filter row.
-    And I click on "Remove filter row" "button" in the "Filter 1" "fieldset"
-    And I should see "Student 2" in the "participants" "table"
+    # Match any available user.
+    When I click on "Remove filter row" "button" in the "Filter 1" "fieldset"
+
+    Then I should see "Student 2" in the "participants" "table"
     And I should see "Student 3" in the "participants" "table"
-    And I should not see "Student 1" in the "participants" "table"
+    But I should not see "Student 1" in the "participants" "table"
+
     # Testing result of applying groups filter manually.
+    # Match:
+    #   Groups Any ["Group 1"].
+
+    # Match Groups Any ["Group 1"]
     And I set the field "Match" in the "Filter 1" "fieldset" to "Any"
     And I set the field "type" in the "Filter 1" "fieldset" to "Groups"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset"
-    And I should see "Group 1" in the ".form-autocomplete-suggestions" "css_element"
+
+    And I open the autocomplete suggestions list in the "Filter 1" "fieldset"
     And I should see "Group 2" in the ".form-autocomplete-suggestions" "css_element"
-    And I click on "Group 1" "list_item"
+    And I press the escape key
+
+    And I set the field "Type or select..." in the "Filter 1" "fieldset" to "Group 1"
+
     And I click on "Apply filters" "button"
     And I should see "Student 2" in the "participants" "table"
-    And I should not see "Student 1" in the "participants" "table"
+    But I should not see "Student 1" in the "participants" "table"
     And I should not see "Student 3" in the "participants" "table"
+
     # Testing result of removing groups filter by clearing all filters.
-    And I click on "Clear filters" "button"
-    And I should see "Student 2" in the "participants" "table"
+    # Match any available user.
+    When I click on "Clear filters" "button"
+
+    Then I should see "Student 2" in the "participants" "table"
     And I should see "Student 3" in the "participants" "table"
-    And I should not see "Student 1" in the "participants" "table"
+    But I should not see "Student 1" in the "participants" "table"
 
   @javascript
   Scenario: Filter users who have no role in a course
-    Given I log in as "teacher1"
+    Given I log in as "patricia"
     And I am on "Course 1" course homepage
     And I navigate to course participants
+
+    # Remove the user role.
     And I click on "Student 1's role assignments" "link"
     And I click on ".form-autocomplete-selection [aria-selected=true]" "css_element"
     And I press the escape key
     And I click on "Save changes" "link"
-    And I set the field "type" in the "Filter 1" "fieldset" to "Roles"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset"
-    And I click on "No roles" "list_item"
-    When I click on "Apply filters" "button"
+
+    # Match:
+    #   Roles All ["No roles"].
+
+    # Match Roles All ["No roles"].
+    When I set the field "type" in the "Filter 1" "fieldset" to "Roles"
+    And I set the field "Type or select..." in the "Filter 1" "fieldset" to "No roles"
+
+    And I click on "Apply filters" "button"
+
     Then I should see "Student 1" in the "participants" "table"
-    And I should not see "Student 2" in the "participants" "table"
+    But I should not see "Student 2" in the "participants" "table"
     And I should not see "Student 3" in the "participants" "table"
     And I should not see "Student 4" in the "participants" "table"
-    And I should not see "Teacher 1" in the "participants" "table"
+    And I should not see "Patricia Pea" in the "participants" "table"
 
   @javascript
   Scenario: Multiple filters applied (All filterset match type)
-    Given I log in as "teacher1"
+    Given I log in as "patricia"
     And I am on "Course 1" course homepage
     And I navigate to course participants
-    And I set the field "Match" in the "Filter 1" "fieldset" to "All"
+
+    # Match Any:
+    #   Roles All ["Student"] and
+    #   Status Any ["Active"].
+
+    # Match Roles All ["Student"].
+    When I set the field "Match" in the "Filter 1" "fieldset" to "All"
     And I set the field "type" in the "Filter 1" "fieldset" to "Roles"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset"
-    And I click on "Student" "list_item"
+    And I set the field "Type or select..." in the "Filter 1" "fieldset" to "Student"
+
+    # Match Status All ["Active"].
     And I click on "Add condition" "button"
     # Set filterset to match all.
     And I set the field "Match" to "All"
     And I set the field "Match" in the "Filter 2" "fieldset" to "Any"
     And I set the field "type" in the "Filter 2" "fieldset" to "Status"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 2" "fieldset"
-    And I click on "Active" "list_item"
-    When I click on "Apply filters" "button"
+    And I set the field "Type or select..." in the "Filter 2" "fieldset" to "Active"
+
+    And I click on "Apply filters" "button"
+
     Then I should see "Student 1" in the "participants" "table"
     And I should see "Student 3" in the "participants" "table"
-    And I should not see "Student 2" in the "participants" "table"
+    But I should not see "Student 2" in the "participants" "table"
     And I should not see "Student 4" in the "participants" "table"
-    And I should not see "Teacher 1" in the "participants" "table"
-    # Add more filters.
-    And I click on "Add condition" "button"
+    And I should not see "Patricia Pea" in the "participants" "table"
+
+    # Match Any:
+    #   Roles All ["Student"]; and
+    #   Status Any ["Active"]; and
+    #   Enrolment method Any ["Manual"]; and
+    #   Groups Any ["Group 2"].
+
+    # Match enrolment method Any ["Manual"]
+    When I click on "Add condition" "button"
     And I set the field "Match" in the "Filter 3" "fieldset" to "Any"
     And I set the field "type" in the "Filter 3" "fieldset" to "Enrolment methods"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 3" "fieldset"
-    And I click on "Manual enrolments" "list_item"
+    And I set the field "Type or select..." in the "Filter 3" "fieldset" to "Manual enrolments"
+
+    # Match Groups Any ["Group 2"]
     And I click on "Add condition" "button"
     And I set the field "Match" in the "Filter 4" "fieldset" to "All"
     And I set the field "type" in the "Filter 4" "fieldset" to "Groups"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 4" "fieldset"
-    And I click on "Group 2" "list_item"
+    And I set the field "Type or select..." in the "Filter 4" "fieldset" to "Group 2"
     And I click on "Apply filters" "button"
-    And I should see "Student 3" in the "participants" "table"
-    But I should not see "Teacher 1" in the "participants" "table"
+
+    Then I should see "Student 3" in the "participants" "table"
+    But I should not see "Patricia Pea" in the "participants" "table"
     And I should not see "Student 1" in the "participants" "table"
     And I should not see "Student 2" in the "participants" "table"
     And I should not see "Student 4" in the "participants" "table"
+
     # Change the active status filter to inactive.
+    # Match Any:
+    #   Roles All ["Student"]; and
+    #   Status Any ["Inactive"]; and
+    #   Enrolment method Any ["Manual"]; and
+    #   Groups Any ["Group 2"].
+
+    # Match Status All ["Inactive"].
     And I click on "Active" "autocomplete_selection"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 2" "fieldset"
-    And I click on "Inactive" "list_item"
+    And I set the field "Type or select..." in the "Filter 2" "fieldset" to "Inactive"
     And I click on "Apply filters" "button"
+
     Then I should see "Student 2" in the "participants" "table"
     But I should not see "Student 4" in the "participants" "table"
     And I should not see "Student 1" in the "participants" "table"
     And I should not see "Student 3" in the "participants" "table"
-    And I should not see "Teacher 1" in the "participants" "table"
+    And I should not see "Patricia Pea" in the "participants" "table"
+
     # Set both statuses (match any).
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 2" "fieldset"
-    And I click on "Active" "list_item"
+    # Match Any:
+    #   Roles All ["Student"]; and
+    #   Status Any ["Active", "Inactive"]; and
+    #   Enrolment method Any ["Manual"]; and
+    #   Groups Any ["Group 2"].
+
+    # Match Status Any ["Active", "Inactive"].
+    And I set the field "Type or select..." in the "Filter 2" "fieldset" to "Active,Inactive"
     And I click on "Apply filters" "button"
-    And I should see "Student 2" in the "participants" "table"
+
+    Then I should see "Student 2" in the "participants" "table"
     And I should see "Student 3" in the "participants" "table"
-    And I should not see "Student 1" in the "participants" "table"
+    But I should not see "Student 1" in the "participants" "table"
     And I should not see "Student 4" in the "participants" "table"
-    # Switch to match all.
-    And I set the field "Match" in the "Filter 2" "fieldset" to "All"
+
+    # Set both statuses (match all).
+    # Match Any:
+    #   Roles All ["Student"]; and
+    #   Status Any ["Active", "Inactive"]; and
+    #   Enrolment method Any ["Manual"]; and
+    #   Groups Any ["Group 2"].
+
+    # Match Status All ["Active", "Inactive"].
+    When I set the field "Match" in the "Filter 2" "fieldset" to "All"
     And I click on "Apply filters" "button"
-    And I should see "Nothing to display"
+
+    Then I should see "Nothing to display"
 
   @javascript
   Scenario: Multiple filters applied (Any filterset match type)
-    Given I log in as "teacher1"
-    #Avoid 'Teacher' list item collisions with profile dropdown.
-    And I open my profile in edit mode
-    And I set the field "First name" to "Patricia"
-    And I press "Update profile"
+    Given I log in as "patricia"
     And I am on "Course 1" course homepage
     And I navigate to course participants
-    And I set the field "Match" in the "Filter 1" "fieldset" to "All"
+
+    # Match Any:
+    #   Roles All ["Teacher"] and
+    #   Status Any ["Active"].
+
+    # Match Roles all Roles ["Teacher"].
+    When I set the field "Match" in the "Filter 1" "fieldset" to "All"
     And I set the field "type" in the "Filter 1" "fieldset" to "Roles"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset"
-    And I click on "Teacher" "list_item"
+    And I set the field "Type or select..." in the "Filter 1" "fieldset" to "Teacher"
+
+    # Match Status Any ["Active"].
     And I click on "Add condition" "button"
-    # Set filterset to match any.
-    And I set the field "Match" to "Any"
     And I set the field "Match" in the "Filter 2" "fieldset" to "Any"
     And I set the field "type" in the "Filter 2" "fieldset" to "Status"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 2" "fieldset"
-    And I click on "Active" "list_item"
-    When I click on "Apply filters" "button"
+    And I set the field "Type or select..." in the "Filter 2" "fieldset" to "Active"
+
+    # Set filterset to match any.
+    And I set the field "Match" to "Any"
+    And I click on "Apply filters" "button"
+
     Then I should see "Student 1" in the "participants" "table"
-    And I should see "Patricia 1" in the "participants" "table"
+    And I should see "Patricia Pea" in the "participants" "table"
     And I should see "Student 3" in the "participants" "table"
-    And I should not see "Student 2" in the "participants" "table"
+    But I should not see "Student 2" in the "participants" "table"
     And I should not see "Student 4" in the "participants" "table"
-    And I set the field "Match" in the "Filter 2" "fieldset" to "None"
+
+    # Match Any:
+    #   Roles All ["Teacher"] and
+    #   Status None ["Active"].
+
+    # Match Status Not ["Active"]
+    When I set the field "Match" in the "Filter 2" "fieldset" to "None"
     And I click on "Apply filters" "button"
-    And I should see "Student 2" in the "participants" "table"
+
+    Then I should see "Student 2" in the "participants" "table"
     And I should see "Student 4" in the "participants" "table"
-    And I should see "Patricia 1" in the "participants" "table"
-    And I should not see "Student 1" in the "participants" "table"
+    And I should see "Patricia Pea" in the "participants" "table"
+    But I should not see "Student 1" in the "participants" "table"
     And I should not see "Student 3" in the "participants" "table"
+
     # Add a keyword filter.
-    And I click on "Add condition" "button"
+    # Match Any:
+    #   Roles All ["Teacher"]; and
+    #   Status None ["Active"]; and
+    #   Keyword Any ["patricia"].
+
+    # Match Keyword Any ["patricia"].
+    When I click on "Add condition" "button"
     And I set the field "Match" in the "Filter 3" "fieldset" to "Any"
     And I set the field "type" in the "Filter 3" "fieldset" to "Keyword"
-    And I set the field "Type..." to "teacher1"
-    And I press the enter key
+    And I set the field "Type..." in the "Filter 3" "fieldset" to "patricia"
+
     And I click on "Apply filters" "button"
-    And I should see "Student 2" in the "participants" "table"
+
+    Then I should see "Student 2" in the "participants" "table"
     And I should see "Student 4" in the "participants" "table"
-    And I should see "Patricia 1" in the "participants" "table"
-    And I should not see "Student 1" in the "participants" "table"
+    And I should see "Patricia Pea" in the "participants" "table"
+    But I should not see "Student 1" in the "participants" "table"
     And I should not see "Student 3" in the "participants" "table"
 
   @javascript
   Scenario: Multiple filters applied (None filterset match type)
-    Given I log in as "teacher1"
-    #Avoid 'Teacher' list item collisions with profile dropdown.
-    And I open my profile in edit mode
-    And I set the field "First name" to "Patricia"
-    And I press "Update profile"
+    Given I log in as "patricia"
     And I am on "Course 1" course homepage
     And I navigate to course participants
-    And I set the field "Match" in the "Filter 1" "fieldset" to "All"
+
+    # Match None:
+    #   Roles All ["Teacher"] and
+    #   Status Any ["Active"].
+
+    # Set the Roles to "All" ["Teacher"].
+    When I set the field "Match" in the "Filter 1" "fieldset" to "All"
     And I set the field "type" in the "Filter 1" "fieldset" to "Roles"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset"
-    And I click on "Teacher" "list_item"
+    And I set the field "Type or select..." in the "Filter 1" "fieldset" to "Teacher"
+
+    # Set the Status to "Any" ["Active"].
     And I click on "Add condition" "button"
-    # Set filterset to match none.
-    And I set the field "Match" to "None"
     And I set the field "Match" in the "Filter 2" "fieldset" to "Any"
     And I set the field "type" in the "Filter 2" "fieldset" to "Status"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 2" "fieldset"
-    And I click on "Active" "list_item"
-    When I click on "Apply filters" "button"
+    And I set the field "Type or select..." in the "Filter 2" "fieldset" to "Active"
+
+    # Set filterset to match None.
+    And I set the field "Match" to "None"
+    And I click on "Apply filters" "button"
+
     Then I should see "Student 2" in the "participants" "table"
     And I should see "Student 4" in the "participants" "table"
-    And I should not see "Student 1" in the "participants" "table"
+    But I should not see "Student 1" in the "participants" "table"
     And I should not see "Student 3" in the "participants" "table"
-    And I should not see "Patricia 1" in the "participants" "table"
-    And I set the field "Match" in the "Filter 2" "fieldset" to "None"
+    And I should not see "Patricia Pea" in the "participants" "table"
+
+    # Match None:
+    #   Roles All ["Teacher"] and
+    #   Status None ["Active"]
+    # Set the Status to "None ["Active"]
+    When I set the field "Match" in the "Filter 2" "fieldset" to "None"
     And I click on "Apply filters" "button"
-    And I should see "Student 1" in the "participants" "table"
+    Then I should see "Student 1" in the "participants" "table"
     And I should see "Student 3" in the "participants" "table"
-    And I should not see "Student 2" in the "participants" "table"
+    But I should not see "Student 2" in the "participants" "table"
     And I should not see "Student 4" in the "participants" "table"
-    And I should not see "Patricia 1" in the "participants" "table"
-    # Add a keyword filter.
-    And I click on "Add condition" "button"
-    And I set the field "Match" in the "Filter 3" "fieldset" to "Any"
+    And I should not see "Patricia Pea" in the "participants" "table"
+
+    # Match None:
+    #   Roles All ["Teacher"] and
+    #   Status None ["Active"] and
+    #   Keyword Any ["3@"]
+    # Set the Keyword to "Any" ["3@"]
+    When I click on "Add condition" "button"
+    Then I set the field "Match" in the "Filter 3" "fieldset" to "Any"
     And I set the field "type" in the "Filter 3" "fieldset" to "Keyword"
-    And I set the field "Type..." to "3@"
-    And I press the enter key
-    And I click on "Apply filters" "button"
-    And I should see "Student 1" in the "participants" "table"
+    And I set the field "Type..." in the "Filter 3" "fieldset" to "3@"
+
+    When I click on "Apply filters" "button"
+    Then I should see "Student 1" in the "participants" "table"
     And I should not see "Student 2" in the "participants" "table"
     And I should not see "Student 3" in the "participants" "table"
     And I should not see "Student 4" in the "participants" "table"
-    And I should not see "Patricia 1" in the "participants" "table"
-    And I set the field "Match" in the "Filter 3" "fieldset" to "None"
+    And I should not see "Patricia Pea" in the "participants" "table"
+
+    # Match None:
+    #   Roles All ["Teacher"] and
+    #   Status None ["Active"] and
+    #   Keyword None ["3@"].
+
+    # Set the Keyword to "None" ["3@"]
+    When I set the field "Match" in the "Filter 3" "fieldset" to "None"
     And I click on "Apply filters" "button"
-    And I should see "Student 3" in the "participants" "table"
-    And I should not see "Student 1" in the "participants" "table"
+
+    Then I should see "Student 3" in the "participants" "table"
+    But I should not see "Student 1" in the "participants" "table"
     And I should not see "Student 2" in the "participants" "table"
     And I should not see "Student 4" in the "participants" "table"
-    And I should not see "Patricia 1" in the "participants" "table"
+    And I should not see "Patricia Pea" in the "participants" "table"
 
   @javascript
   Scenario: Filter match by one or more keywords and modified match types
-    Given I log in as "teacher1"
+    Given I log in as "patricia"
     And I am on "Course 1" course homepage
     And I navigate to course participants
-    And I set the field "Match" in the "Filter 1" "fieldset" to "Any"
+
+    # Match:
+    #   Keyword Any ["1@example"].
+
+    # Set the Keyword to "Any" ["1@example"]
+    When I set the field "Match" in the "Filter 1" "fieldset" to "Any"
     And I set the field "type" in the "Filter 1" "fieldset" to "Keyword"
-    And I set the field "Type..." to "1@example"
-    And I press the enter key
-    When I click on "Apply filters" "button"
+    And I set the field "Type..." in the "Filter 1" "fieldset" to "1@example"
+    And I click on "Apply filters" "button"
+
     Then I should see "Student 1" in the "participants" "table"
-    And I should see "Teacher 1" in the "participants" "table"
-    And I should not see "Student 2" in the "participants" "table"
+    And I should see "Patricia Pea" in the "participants" "table"
+
+    But I should not see "Student 2" in the "participants" "table"
     And I should not see "Student 3" in the "participants" "table"
     And I should not see "Student 4" in the "participants" "table"
-    And I set the field "Match" in the "Filter 1" "fieldset" to "None"
-    And I click on "Apply filters" "button"
-    And I should see "Student 2" in the "participants" "table"
-    And I should see "Student 3" in the "participants" "table"
-    And I should see "Student 4" in the "participants" "table"
-    And I should not see "Student 1" in the "participants" "table"
-    And I should not see "Teacher 1" in the "participants" "table"
-    And I set the field "Match" in the "Filter 1" "fieldset" to "All"
+
+    # Match:
+    #   Keyword All ["1@example"].
+    When I set the field "Match" in the "Filter 1" "fieldset" to "All"
     And I click on "Apply filters" "button"
-    And I should see "Student 1" in the "participants" "table"
-    And I should see "Teacher 1" in the "participants" "table"
-    And I should not see "Student 2" in the "participants" "table"
+
+    Then I should see "Student 1" in the "participants" "table"
+    And I should see "Patricia Pea" in the "participants" "table"
+    But I should not see "Student 2" in the "participants" "table"
     And I should not see "Student 3" in the "participants" "table"
     And I should not see "Student 4" in the "participants" "table"
-    And I set the field "Match" in the "Filter 1" "fieldset" to "None"
+
+    # Match:
+    #   Keyword None ["1@example"].
+    When I set the field "Match" in the "Filter 1" "fieldset" to "None"
     And I click on "Apply filters" "button"
-    And I should see "Student 2" in the "participants" "table"
+
+    Then I should see "Student 2" in the "participants" "table"
     And I should see "Student 3" in the "participants" "table"
     And I should see "Student 4" in the "participants" "table"
-    And I should not see "Student 1" in the "participants" "table"
-    And I should not see "Teacher 1" in the "participants" "table"
-    # Add a second keyword filter value
-    And I set the field "Type..." to "moodle"
-    And I press the enter key
+    But I should not see "Student 1" in the "participants" "table"
+    And I should not see "Patricia Pea" in the "participants" "table"
+
+    # Set two keyword values.
+    # Match:
+    #   Keyword None ["1@example", "moodle"].
+    When I set the field "Type..." to "1@example, moodle"
     And I click on "Apply filters" "button"
-    And I should see "Student 2" in the "participants" "table"
+
+    Then I should see "Student 2" in the "participants" "table"
     And I should see "Student 3" in the "participants" "table"
-    And I should not see "Student 1" in the "participants" "table"
-    And I should not see "Teacher 1" in the "participants" "table"
+    But I should not see "Student 1" in the "participants" "table"
+    And I should not see "Patricia Pea" in the "participants" "table"
     And I should not see "Student 4" in the "participants" "table"
-    And I set the field "Match" in the "Filter 1" "fieldset" to "Any"
+
+    # Set two keyword values.
+    # Match:
+    #   Keyword Any ["1@example", "moodle"].
+    When I set the field "Match" in the "Filter 1" "fieldset" to "Any"
     And I click on "Apply filters" "button"
-    And I should see "Student 1" in the "participants" "table"
-    And I should see "Teacher 1" in the "participants" "table"
+
+    Then I should see "Student 1" in the "participants" "table"
+    And I should see "Patricia Pea" in the "participants" "table"
     And I should see "Student 4" in the "participants" "table"
-    And I should not see "Student 2" in the "participants" "table"
+    But I should not see "Student 2" in the "participants" "table"
     And I should not see "Student 3" in the "participants" "table"
-    And I set the field "Match" in the "Filter 1" "fieldset" to "All"
+
+    # Match:
+    #   Keyword All ["1@example", "moodle"].
+    When I set the field "Match" in the "Filter 1" "fieldset" to "All"
     And I click on "Apply filters" "button"
-    And I should see "Nothing to display"
+
+    Then I should see "Nothing to display"
 
   @javascript
   Scenario: Reorder users without losing filter
-    Given I log in as "teacher1"
+    Given I log in as "patricia"
     And I am on "Course 1" course homepage
     And I navigate to course participants
-    And I set the field "type" in the "Filter 1" "fieldset" to "Roles"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset"
-    And I click on "Student" "list_item"
+
+    When I set the field "type" in the "Filter 1" "fieldset" to "Roles"
+    And I set the field "Type or select..." in the "Filter 1" "fieldset" to "Student"
     And I click on "Apply filters" "button"
+
     And I should see "Student 1" in the "participants" "table"
     And I should see "Student 2" in the "participants" "table"
     And I should see "Student 3" in the "participants" "table"
     And I should see "Student 4" in the "participants" "table"
-    And I should not see "Teacher 1" in the "participants" "table"
+    And I should not see "Patricia Pea" in the "participants" "table"
+
     When I click on "Surname" "link"
     Then I should see "Student 1" in the "participants" "table"
     And I should see "Student 2" in the "participants" "table"
     And I should see "Student 3" in the "participants" "table"
     And I should see "Student 4" in the "participants" "table"
-    And I should not see "Teacher 1" in the "participants" "table"
+    But I should not see "Patricia Pea" in the "participants" "table"
 
   @javascript
   Scenario: Only possible to add filter rows for the number of filters available
-    Given I log in as "teacher1"
+    Given I log in as "patricia"
     And I am on "Course 1" course homepage
     And I navigate to course participants
-    And I set the field "type" in the "Filter 1" "fieldset" to "Keyword"
+    When I set the field "type" in the "Filter 1" "fieldset" to "Keyword"
     And I click on "Add condition" "button"
     And I set the field "type" in the "Filter 2" "fieldset" to "Status"
     And I click on "Add condition" "button"
@@ -504,11 +647,12 @@ Feature: Course participants can be filtered
     And I set the field "type" in the "Filter 5" "fieldset" to "Groups"
     And I click on "Add condition" "button"
     And I set the field "type" in the "Filter 6" "fieldset" to "Inactive for more than"
-    And the "Add condition" "button" should be disabled
+
+    Then the "Add condition" "button" should be disabled
 
   @javascript
   Scenario: Rendering filter options for teachers in a course that don't support groups
-    Given I log in as "teacher1"
+    Given I log in as "patricia"
     And I am on "Course 2" course homepage
     When I navigate to course participants
     Then I should see "Roles" in the "type" "field"
@@ -526,51 +670,76 @@ Feature: Course participants can be filtered
 
   @javascript
   Scenario: Filter by user identity fields
-    Given I log in as "teacher1"
+    Given I log in as "patricia"
     And the following config values are set as admin:
         | showuseridentity | idnumber,email,city,country |
     And I am on "Course 1" course homepage
     And I navigate to course participants
-    And I set the field "type" in the "Filter 1" "fieldset" to "Keyword"
-    # Search by email (only).
-    And I set the field "Type..." to "student1@example.com"
-    And I press the enter key
-    When I click on "Apply filters" "button"
+
+    # Search by email (only) - should only see visible email + own.
+    # Match:
+    #   Keyword Any ["student1@example.com"].
+
+    # Set the Keyword to "Any" ["student1@example.com"]
+    When I set the field "type" in the "Filter 1" "fieldset" to "Keyword"
+    And I set the field "Type..." in the "Filter 1" "fieldset" to "student1@example.com"
+    And I click on "Apply filters" "button"
+
     Then I should see "Student 1" in the "participants" "table"
-    And I should not see "Student 2" in the "participants" "table"
-    And I should not see "Teacher 1" in the "participants" "table"
+    But I should not see "Student 2" in the "participants" "table"
+    And I should not see "Patricia Pea" in the "participants" "table"
+
     # Search by idnumber (only).
+    # Match:
+    #   Keyword Any ["SID"].
+
+    # Set the Keyword to "Any" ["SID"]
     And I click on "student1@example.com" "autocomplete_selection"
-    And I set the field "Type..." to "SID"
-    And I press the enter key
+    And I set the field "Type..." in the "Filter 1" "fieldset" to "SID"
     And I click on "Apply filters" "button"
-    And I should see "Student 1" in the "participants" "table"
+
+    Then I should see "Student 1" in the "participants" "table"
     And I should see "Student 2" in the "participants" "table"
     And I should see "Student 3" in the "participants" "table"
     And I should see "Student 4" in the "participants" "table"
-    And I should not see "Teacher 1" in the "participants" "table"
+    But I should not see "Patricia Pea" in the "participants" "table"
+
     # Search by city (only).
+    # Match:
+    #   Keyword Any ["SCITY"].
+
+    # Set the Keyword to "Any" ["SCITY"]
     And I click on "SID" "autocomplete_selection"
-    And I set the field "Type..." to "SCITY"
-    And I press the enter key
+    And I set the field "Type..." in the "Filter 1" "fieldset" to "SCITY"
     And I click on "Apply filters" "button"
-    And I should see "Student 1" in the "participants" "table"
+
+    Then I should see "Student 1" in the "participants" "table"
     And I should see "Student 2" in the "participants" "table"
     And I should see "Student 3" in the "participants" "table"
     And I should see "Student 4" in the "participants" "table"
-    And I should not see "Teacher 1" in the "participants" "table"
+    But I should not see "Patricia Pea" in the "participants" "table"
+
     # Search by country text (only) - should not match.
+    # Match:
+    #   Keyword Any ["GB"].
+
+    # Set the Keyword to "Any" ["GB"]
     And I click on "SCITY" "autocomplete_selection"
-    And I set the field "Type..." to "GB"
-    And I press the enter key
+    And I set the field "Type..." in the "Filter 1" "fieldset" to "GB"
     And I click on "Apply filters" "button"
-    And I should see "Nothing to display"
+
+    Then I should see "Nothing to display"
+
     # Check no match.
+    # Match:
+    #   Keyword Any ["NOTHING"].
+
+    # Set the Keyword to "Any" ["NOTHING"]
     And I click on "GB" "autocomplete_selection"
-    And I set the field "Type..." to "NOTHING"
-    And I press the enter key
+    And I set the field "Type..." in the "Filter 1" "fieldset" to "NOTHING"
     And I click on "Apply filters" "button"
-    And I should see "Nothing to display"
+
+    Then I should see "Nothing to display"
 
   @javascript
   Scenario: Filter by user identity fields when cannot see the field data
@@ -580,215 +749,290 @@ Feature: Course participants can be filtered
     And the following config values are set as admin:
       | showuseridentity | idnumber,email,city,country |
     And I log out
-    And I log in as "teacher1"
+
+    And I log in as "patricia"
     And I am on "Course 1" course homepage
     And I navigate to course participants
+
+    # Match:
+    #   Keyword Any ["@example.com"].
+
     # Search by email (only) - should only see visible email + own.
-    And I set the field "type" in the "Filter 1" "fieldset" to "Keyword"
-    And I set the field "Type..." to "@example."
-    And I press the enter key
-    When I click on "Apply filters" "button"
-    Then I should not see "Student 1" in the "participants" "table"
-    And I should see "Student 2" in the "participants" "table"
+    # Set the Keyword to "Any" ["@example.com"]
+    When I set the field "type" in the "Filter 1" "fieldset" to "Keyword"
+    And I set the field "Type..." in the "Filter 1" "fieldset" to "@example."
+    And I click on "Apply filters" "button"
+
+    Then I should see "Student 2" in the "participants" "table"
+    And I should see "Patricia Pea" in the "participants" "table"
+    But I should not see "Student 1" in the "participants" "table"
     And I should not see "Student 3" in the "participants" "table"
     And I should not see "Student 4" in the "participants" "table"
-    And I should see "Teacher 1" in the "participants" "table"
+
     # Search for other fields - should only see own results.
+
+    # Match:
+    #   Keyword Any ["SID"].
+    # Set the Keyword to "Any" ["SID"]
     And I click on "@example." "autocomplete_selection"
-    And I set the field "Type..." to "SID"
-    And I press the enter key
+    And I set the field "Type..." in the "Filter 1" "fieldset" to "SID"
     And I click on "Apply filters" "button"
-    And I should see "Nothing to display"
+
+    Then I should see "Nothing to display"
+
+    # Match:
+    #   Keyword Any ["TID"].
+
+    # Set the Keyword to "Any" ["TID"]
     And I click on "SID" "autocomplete_selection"
-    And I set the field "Type..." to "TID"
-    And I press the enter key
+    And I set the field "Type..." in the "Filter 1" "fieldset" to "TID"
     And I click on "Apply filters" "button"
-    And I should see "Teacher 1" in the "participants" "table"
-    And I should not see "Student 1" in the "participants" "table"
+
+    Then I should see "Patricia Pea" in the "participants" "table"
+    But I should not see "Student 1" in the "participants" "table"
+
+    # Match:
+    #   Keyword Any ["CITY"].
+
+    # Set the Keyword to "Any" ["CITY"]
     And I click on "TID" "autocomplete_selection"
-    And I set the field "Type..." to "CITY"
-    And I press the enter key
+    And I set the field "Type..." in the "Filter 1" "fieldset" to "CITY"
     And I click on "Apply filters" "button"
-    And I should see "Teacher 1" in the "participants" "table"
-    And I should not see "Student 1" in the "participants" "table"
-    # Check no match.
+
+    Then I should see "Patricia Pea" in the "participants" "table"
+    But I should not see "Student 1" in the "participants" "table"
+
+    # No data matches regardless of capabilities.
+    # Match:
+    #   Keyword Any ["NOTHING"].
+
+    # Set the Keyword to "Any" ["NOTHING"]
     And I click on "CITY" "autocomplete_selection"
-    And I set the field "Type..." to "NOTHING"
-    And I press the enter key
+    And I set the field "Type..." in the "Filter 1" "fieldset" to "NOTHING"
     And I click on "Apply filters" "button"
-    And I should see "Nothing to display"
+
+    Then I should see "Nothing to display"
 
   @javascript
   Scenario: Individual filters can be removed, which will automatically refresh the participants list
-    Given I log in as "teacher1"
+    # Match All:
+    #   Roles All ["Student"]; and
+    #   Keyword Any ["@example.com"].
+
+    # Set the Roles to "All" ["Student"].
+    Given I log in as "patricia"
     And I am on "Course 1" course homepage
     And I navigate to course participants
     And I set the field "Match" in the "Filter 1" "fieldset" to "All"
     And I set the field "type" in the "Filter 1" "fieldset" to "Roles"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset"
-    And I click on "Student" "list_item"
+    And I set the field "Type or select..." in the "Filter 1" "fieldset" to "Student"
+
+    # Set the Keyword to "Any" ["@example.com"]
     And I click on "Add condition" "button"
-    # Set filterset to match all.
-    And I set the field "Match" to "All"
     And I set the field "Match" in the "Filter 2" "fieldset" to "Any"
     And I set the field "type" in the "Filter 2" "fieldset" to "Keyword"
-    And I set the field "Type..." to "@example"
-    And I press the enter key
+    And I set the field "Type..." in the "Filter 2" "fieldset" to "@example"
+
+    # Set filterset to match all.
+    And I set the field "Match" to "All"
     And I click on "Apply filters" "button"
-    And I should see "Student 1" in the "participants" "table"
+
+    Then I should see "Student 1" in the "participants" "table"
     And I should see "Student 2" in the "participants" "table"
     And I should see "Student 3" in the "participants" "table"
-    And I should not see "Student 4" in the "participants" "table"
-    And I should not see "Teacher 1" in the "participants" "table"
+    But I should not see "Student 4" in the "participants" "table"
+    And I should not see "Patricia Pea" in the "participants" "table"
+
+    # Match:
+    #   Keyword Any ["@example.com"].
     When I click on "Remove filter row" "button" in the "Filter 1" "fieldset"
     Then I should see "Student 1" in the "participants" "table"
     And I should see "Student 2" in the "participants" "table"
     And I should see "Student 3" in the "participants" "table"
-    And I should see "Teacher 1" in the "participants" "table"
-    And I should not see "Student 4" in the "participants" "table"
+    And I should see "Patricia Pea" in the "participants" "table"
+    But I should not see "Student 4" in the "participants" "table"
 
   @javascript
   Scenario: All filters can be cleared at once
-    Given I log in as "teacher1"
+    # Match All:
+    #   Roles All ["Student"]; and
+    #   Keyword Any ["@example.com"].
+
+    # Set the Roles to "All" ["Student"].
+    Given I log in as "patricia"
     And I am on "Course 1" course homepage
     And I navigate to course participants
-    And I set the field "Match" in the "Filter 1" "fieldset" to "All"
+    When I set the field "Match" in the "Filter 1" "fieldset" to "All"
     And I set the field "type" in the "Filter 1" "fieldset" to "Roles"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset"
-    And I click on "Student" "list_item"
+    And I set the field "Type or select..." in the "Filter 1" "fieldset" to "Student"
+
+    # Set the Keyword to "All" ["@example.com"].
     And I click on "Add condition" "button"
-    # Set filterset to match all.
-    And I set the field "Match" to "All"
     And I set the field "Match" in the "Filter 2" "fieldset" to "Any"
     And I set the field "type" in the "Filter 2" "fieldset" to "Keyword"
-    And I set the field "Type..." to "@example"
-    And I press the enter key
+    And I set the field "Type..." in the "Filter 2" "fieldset" to "@example"
+
+    # Set filterset to match all.
+    And I set the field "Match" to "All"
     And I click on "Apply filters" "button"
-    And I should see "Student 1" in the "participants" "table"
+
+    Then I should see "Student 1" in the "participants" "table"
     And I should see "Student 2" in the "participants" "table"
     And I should see "Student 3" in the "participants" "table"
-    And I should not see "Student 4" in the "participants" "table"
-    And I should not see "Teacher 1" in the "participants" "table"
+    But I should not see "Student 4" in the "participants" "table"
+    And I should not see "Patricia Pea" in the "participants" "table"
+
+    # Match Any.
     When I click on "Clear filters" "button"
     Then I should see "Student 1" in the "participants" "table"
     And I should see "Student 2" in the "participants" "table"
     And I should see "Student 3" in the "participants" "table"
     And I should see "Student 4" in the "participants" "table"
-    And I should see "Teacher 1" in the "participants" "table"
+    And I should see "Patricia Pea" in the "participants" "table"
 
   @javascript
   Scenario: Filterset match type is reset when reducing to a single filter
-    Given I log in as "teacher1"
+    # Match None:
+    #   Keyword Any ["@example.com"]; and
+    #   Roles All ["Teacher"].
+    Given I log in as "patricia"
     And I am on "Course 1" course homepage
     And I navigate to course participants
-    And I set the field "Match" in the "Filter 1" "fieldset" to "Any"
+
+    # Set the Keyword to "Any" ["@example.com"]
+    When I set the field "Match" in the "Filter 1" "fieldset" to "Any"
     And I set the field "type" in the "Filter 1" "fieldset" to "Keyword"
     And I set the field "Type..." to "@example.com"
-    And I press the enter key
+
+    # Set the Roles to "All" ["Student"].
     And I click on "Add condition" "button"
-    # Set filterset to match none.
-    And I set the field "Match" to "None"
     And I set the field "Match" in the "Filter 2" "fieldset" to "All"
     And I set the field "type" in the "Filter 2" "fieldset" to "Roles"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 2" "fieldset"
-    And I click on "Student" "list_item"
+    And I set the field "Type or select..." in the "Filter 2" "fieldset" to "Student"
+
     # Match none of student role and @example.com keyword.
+    And I set the field "Match" to "None"
     And I click on "Apply filters" "button"
-    And I should see "Teacher 1" in the "participants" "table"
-    And I should not see "Student 1" in the "participants" "table"
+
+    Then I should see "Patricia Pea" in the "participants" "table"
+    But I should not see "Student 1" in the "participants" "table"
     And I should not see "Student 2" in the "participants" "table"
     And I should not see "Student 3" in the "participants" "table"
     And I should not see "Student 4" in the "participants" "table"
+
+    # Match:
+    #   Keyword Any ["@example.com"].
+    # When removing the pen-ultimate filter, the filterset match type is removed too.
     When I click on "Remove filter row" "button" in the "Filter 2" "fieldset"
-    # Filterset match type and role filter are removed, leaving keyword filter only.
     Then I should see "Student 1" in the "participants" "table"
     And I should see "Student 2" in the "participants" "table"
     And I should see "Student 3" in the "participants" "table"
-    And I should not see "Student 4" in the "participants" "table"
-    And I should not see "Teacher 1" in the "participants" "table"
-    And I click on "Add condition" "button"
-    # Re-add a second filter and ensure the default (All) filterset match type is set.
+    But I should not see "Student 4" in the "participants" "table"
+    And I should not see "Patricia Pea" in the "participants" "table"
+
+    # Match Any:
+    #   Keyword Any ["@example.com"]; and
+    #   Role All ["Student"].
+    # The default filterset match (Any) should apply.
+    # Set the Roles to "All" ["Student"].
+    When I click on "Add condition" "button"
     And I set the field "Match" in the "Filter 2" "fieldset" to "All"
     And I set the field "type" in the "Filter 2" "fieldset" to "Role"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 2" "fieldset"
-    And I click on "Student" "list_item"
+    And I set the field "Type or select..." in the "Filter 2" "fieldset" to "Student"
     And I click on "Apply filters" "button"
-    And I should see "Student 1" in the "participants" "table"
+
+    Then I should see "Student 1" in the "participants" "table"
     And I should see "Student 2" in the "participants" "table"
     And I should see "Student 3" in the "participants" "table"
     And I should not see "Student 4" in the "participants" "table"
-    And I should not see "Teacher 1" in the "participants" "table"
+    But I should not see "Patricia Pea" in the "participants" "table"
 
   @javascript
   Scenario: Filter users by first initial
-    Given I log in as "teacher1"
+    # Match:
+    #   No filters; and
+    # First initial "T".
+    Given I log in as "patricia"
     And I am on "Course 2" course homepage
     And I navigate to course participants
     And I should see "Student 1" in the "participants" "table"
     And I should see "Student 2" in the "participants" "table"
     And I should see "Student 3" in the "participants" "table"
     And I should see "Trendy Learnson" in the "participants" "table"
-    And I should see "Teacher 1" in the "participants" "table"
+    And I should see "Patricia Pea" in the "participants" "table"
     When I click on "T" "link" in the ".firstinitial" "css_element"
     Then I should see "Trendy Learnson" in the "participants" "table"
-    And I should see "Teacher 1" in the "participants" "table"
+    But I should not see "Patricia Pea" in the "participants" "table"
     And I should not see "Student 1" in the "participants" "table"
     And I should not see "Student 2" in the "participants" "table"
     And I should not see "Student 3" in the "participants" "table"
 
   @javascript
   Scenario: Filter users by last initial
-    Given I log in as "teacher1"
+    # Match:
+    #   No filters; and
+    # Last initial "L".
+    Given I log in as "patricia"
     And I am on "Course 2" course homepage
     And I navigate to course participants
     And I should see "Student 1" in the "participants" "table"
     And I should see "Student 2" in the "participants" "table"
     And I should see "Student 3" in the "participants" "table"
     And I should see "Trendy Learnson" in the "participants" "table"
-    And I should see "Teacher 1" in the "participants" "table"
+    And I should see "Patricia Pea" in the "participants" "table"
     When I click on "L" "link" in the ".lastinitial" "css_element"
     Then I should see "Trendy Learnson" in the "participants" "table"
-    And I should not see "Student 1" in the "participants" "table"
+    But I should not see "Student 1" in the "participants" "table"
     And I should not see "Student 2" in the "participants" "table"
     And I should not see "Student 3" in the "participants" "table"
-    And I should not see "Teacher 1" in the "participants" "table"
+    And I should not see "Patricia Pea" in the "participants" "table"
 
   @javascript
   Scenario: Filter users by first and last initials
-    Given I log in as "teacher1"
+    # Match:
+    #   No filters; and
+    # First initial "T"; and
+    # Last initial "L".
+    Given I log in as "patricia"
     And I am on "Course 2" course homepage
     And I navigate to course participants
     And I should see "Student 1" in the "participants" "table"
     And I should see "Student 2" in the "participants" "table"
     And I should see "Student 3" in the "participants" "table"
     And I should see "Trendy Learnson" in the "participants" "table"
-    And I should see "Teacher 1" in the "participants" "table"
+    And I should see "Patricia Pea" in the "participants" "table"
     When I click on "T" "link" in the ".firstinitial" "css_element"
     And I click on "L" "link" in the ".lastinitial" "css_element"
     Then I should see "Trendy Learnson" in the "participants" "table"
-    And I should not see "Student 1" in the "participants" "table"
+    But I should not see "Student 1" in the "participants" "table"
     And I should not see "Student 2" in the "participants" "table"
     And I should not see "Student 3" in the "participants" "table"
-    And I should not see "Teacher 1" in the "participants" "table"
+    And I should not see "Patricia Pea" in the "participants" "table"
 
   @javascript
   Scenario: Initials filtering is always applied in addition to any other filtering
-    Given I log in as "teacher1"
+    # Match:
+    #   Roles All ["Teacher"]; and
+    # First initial "T".
+    Given I log in as "patricia"
     And I am on "Course 2" course homepage
     And I navigate to course participants
     And I should see "Student 1" in the "participants" "table"
     And I should see "Student 2" in the "participants" "table"
     And I should see "Student 3" in the "participants" "table"
     And I should see "Trendy Learnson" in the "participants" "table"
-    And I should see "Teacher 1" in the "participants" "table"
+    And I should see "Patricia Pea" in the "participants" "table"
+
+    # Set the Role to "Any" ["Student"].
     When I set the field "Match" in the "Filter 1" "fieldset" to "Any"
     And I set the field "type" in the "Filter 1" "fieldset" to "Role"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset"
-    And I click on "Student" "list_item"
+    And I set the field "Type or select..." in the "Filter 1" "fieldset" to "Student"
     And I click on "Apply filters" "button"
-    When I click on "T" "link" in the ".firstinitial" "css_element"
+
+    # Last initial "T".
+    And I click on "T" "link" in the ".firstinitial" "css_element"
     Then I should see "Trendy Learnson" in the "participants" "table"
-    And I should not see "Student 1" in the "participants" "table"
+    But I should not see "Student 1" in the "participants" "table"
     And I should not see "Student 2" in the "participants" "table"
     And I should not see "Student 3" in the "participants" "table"
-    And I should not see "Teacher 1" in the "participants" "table"
+    And I should not see "Patricia Pea" in the "participants" "table"
index 51c35c0..1ebbcb3 100644 (file)
@@ -76,8 +76,7 @@ Feature: Course participants can be filtered to display all the users
     And I navigate to course participants
     And I set the field "Match" in the "Filter 1" "fieldset" to "All"
     And I set the field "type" in the "Filter 1" "fieldset" to "Roles"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset"
-    And I click on "Student" "list_item"
+    And I set the field "Type or select..." in the "Filter 1" "fieldset" to "Student"
     When I click on "Apply filters" "button"
     Then I should see "24 participants found"
     And I should see "Show all 24"
@@ -94,15 +93,14 @@ Feature: Course participants can be filtered to display all the users
     And I navigate to course participants
     And I set the field "Match" in the "Filter 1" "fieldset" to "All"
     And I set the field "type" in the "Filter 1" "fieldset" to "Roles"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset"
-    And I click on "Student" "list_item"
+    And I set the field "Type or select..." in the "Filter 1" "fieldset" to "Student"
     When I click on "Apply filters" "button"
-    Then I should see "24 participants found"
-    And I should see "Show all 24"
+    Then I should see "23 participants found"
+    And I should see "Show all 23"
     And I should not see "Show 20 per page"
-    And I click on "Show all 24" "link"
+    And I click on "Show all 23" "link"
     And I should see "Show 20 per page"
-    And I should not see "Show all 24"
+    And I should not see "Show all 23"
 
   @javascript
   Scenario: Apply one value for more than one filter and show all matching users
@@ -113,12 +111,10 @@ Feature: Course participants can be filtered to display all the users
     And I set the field "Match" to "All"
     And I set the field "Match" in the "Filter 1" "fieldset" to "Any"
     And I set the field "type" in the "Filter 1" "fieldset" to "Roles"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset"
-    And I click on "Student" "list_item"
+    And I set the field "Type or select..." in the "Filter 1" "fieldset" to "Student"
     And I set the field "Match" in the "Filter 2" "fieldset" to "Any"
     And I set the field "type" in the "Filter 2" "fieldset" to "Status"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 2" "fieldset"
-    And I click on "Active" "list_item"
+    And I set the field "Type or select..." in the "Filter 2" "fieldset" to "Active"
     When I click on "Apply filters" "button"
     And I click on "Show all 23" "link"
     Then I should see "23 participants found"
index f056317..8e0d7a0 100644 (file)
@@ -79,6 +79,6 @@ Feature: Users' names are displayed across the site according to the user policy
     And I am on "Course 1" course homepage
     When I navigate to course participants
     And I press "Enrol users"
-    And I set the field "Select users" to "three@example.com"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Select users" "form_row"
+    And I click on "Select users" "field"
+    And I type "three@example.com"
     Then I should see "Sarah, Sev, Siobhan, Desforges"
index f15572e..11f2ddc 100644 (file)
@@ -60,8 +60,7 @@ Feature: View course participants groups
     And I should see "Student 1x"
     And I should see "Student 2x"
     And I set the field "type" in the "Filter 1" "fieldset" to "Groups"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset"
-    And I click on "Group B" "list_item"
+    And I set the field "Type or select..." in the "Filter 1" "fieldset" to "Group B"
     And I click on "Apply filters" "button"
     And I should see "Student 3x"
     And I should see "Student 4x"
index 809dece..846a31c 100644 (file)
@@ -29,9 +29,9 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2021052500.48;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2021052500.49;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.
-$release  = '4.0dev (Build: 20201217)'; // Human-friendly version name
+$release  = '4.0dev (Build: 20201224)'; // Human-friendly version name
 $branch   = '400';                      // This version's branch.
 $maturity = MATURITY_ALPHA;             // This version's maturity level.