Merge branch 'MDL-68612-master-integration' of git://github.com/mickhawkins/moodle
authorJun Pataleta <jun@moodle.com>
Wed, 3 Jun 2020 07:39:36 +0000 (15:39 +0800)
committerJun Pataleta <jun@moodle.com>
Wed, 3 Jun 2020 07:39:36 +0000 (15:39 +0800)
88 files changed:
admin/index.php
admin/renderer.php
admin/templates/setting.mustache
blocks/tests/externallib_test.php
config-dist.php
contentbank/classes/output/bankcontent.php
contentbank/index.php
contentbank/templates/bankcontent.mustache
contentbank/templates/bankcontent/toolbar_dropdown.mustache
contentbank/tests/behat/edit_content.feature
course/amd/build/activitychooser.min.js
course/amd/build/activitychooser.min.js.map
course/amd/src/activitychooser.js
course/format/upgrade.txt
filter/displayh5p/filter.php
grade/edit/tree/index.php
grade/edit/tree/lib.php
grade/report/grader/lib.php
grade/report/lib.php
grade/tests/behat/behat_grade.php
grade/tests/behat/grade_item_duplication.feature [new file with mode: 0644]
h5p/amd/build/editor_display.min.js
h5p/amd/build/editor_display.min.js.map
h5p/amd/src/editor_display.js
h5p/classes/framework.php
h5p/classes/helper.php
install/lang/hat/admin.php [new file with mode: 0644]
install/lang/mwl/langconfig.php [new file with mode: 0644]
lang/en/grades.php
lib/accesslib.php
lib/adminlib.php
lib/behat/classes/partial_named_selector.php
lib/behat/core_behat_file_helper.php
lib/behat/form_field/behat_form_field.php
lib/behat/form_field/behat_form_filemanager.php
lib/db/services.php
lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button-debug.js
lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button-min.js
lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button.js
lib/editor/atto/plugins/media/yui/src/button/js/button.js
lib/form/templates/element-advcheckbox-inline.mustache
lib/form/templates/element-checkbox-inline.mustache
lib/form/templates/element-filemanager.mustache
lib/form/templates/element-filepicker.mustache
lib/form/templates/element-group-inline.mustache
lib/form/templates/element-group.mustache
lib/form/templates/element-password-inline.mustache
lib/form/templates/element-password.mustache
lib/form/templates/element-template.mustache
lib/form/templates/element-text-inline.mustache
lib/form/templates/element-text.mustache
lib/form/templates/element-url.mustache
lib/grade/grade_item.php
lib/grade/tests/grade_item_test.php
lib/outputcomponents.php
lib/questionlib.php
lib/table/amd/build/local/dynamic/repository.min.js
lib/table/amd/build/local/dynamic/repository.min.js.map
lib/table/amd/src/local/dynamic/repository.js
lib/table/classes/external/dynamic/get.php [moved from lib/table/classes/external/dynamic/fetch.php with 99% similarity]
lib/table/tests/external/dynamic/get_test.php [moved from lib/table/tests/external/dynamic/fetch_test.php with 94% similarity]
lib/templates/campaign_content.mustache [new file with mode: 0644]
lib/templates/filemanager_fileselect.mustache
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-debug.js
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-min.js
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue.js
lib/yui/src/notification/js/dialogue.js
message/output/popup/templates/notification_popover.mustache
message/templates/message_drawer_view_settings_body_content.mustache
message/templates/message_index.mustache
mod/h5pactivity/classes/external/h5pactivity_summary_exporter.php
mod/quiz/styles.css
privacy/classes/local/request/moodle_content_writer.php
question/type/ddimageortext/amd/build/question.min.js
question/type/ddimageortext/amd/build/question.min.js.map
question/type/ddimageortext/amd/src/question.js
repository/filepicker.js
repository/tests/behat/cancel_add_file.feature
repository/tests/behat/select_file.feature
repository/upload/tests/behat/behat_repository_upload.php
theme/boost/scss/moodle/contentbank.scss
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/question.scss
theme/boost/style/moodle.css
theme/classic/scss/classic/post.scss
theme/classic/style/moodle.css
theme/classic/tests/behat/behat_theme_classic_behat_repository_upload.php
version.php

index ee3b2e3..5660318 100644 (file)
@@ -899,6 +899,9 @@ if (empty($CFG->disabledevlibdirscheck) && (is_dir($CFG->dirroot.'/vendor') || i
 // Check if the site is being foced onto ssl.
 $overridetossl = !empty($CFG->overridetossl);
 
+// Check if moodle campaign content setting is enabled or not.
+$showcampaigncontent = !isset($CFG->showcampaigncontent) || $CFG->showcampaigncontent;
+
 admin_externalpage_setup('adminnotifications');
 
 $output = $PAGE->get_renderer('core', 'admin');
@@ -906,4 +909,5 @@ $output = $PAGE->get_renderer('core', 'admin');
 echo $output->admin_notifications_page($maturity, $insecuredataroot, $errorsdisplayed, $cronoverdue, $dbproblems,
                                        $maintenancemode, $availableupdates, $availableupdatesfetch, $buggyiconvnomb,
                                        $registered, $cachewarnings, $eventshandlers, $themedesignermode, $devlibdir,
-                                       $mobileconfigured, $overridetossl, $invalidforgottenpasswordurl, $croninfrequent);
+                                       $mobileconfigured, $overridetossl, $invalidforgottenpasswordurl, $croninfrequent,
+                                       $showcampaigncontent);
index 45f46a5..cda9ed6 100644 (file)
@@ -282,6 +282,7 @@ class core_admin_renderer extends plugin_renderer_base {
      * @param bool $overridetossl Whether or not ssl is being forced.
      * @param bool $invalidforgottenpasswordurl Whether the forgotten password URL does not link to a valid URL.
      * @param bool $croninfrequent If true, warn that cron hasn't run in the past few minutes
+     * @param bool $showcampaigncontent Whether the campaign content should be visible or not.
      *
      * @return string HTML to output.
      */
@@ -289,7 +290,9 @@ class core_admin_renderer extends plugin_renderer_base {
             $cronoverdue, $dbproblems, $maintenancemode, $availableupdates, $availableupdatesfetch,
             $buggyiconvnomb, $registered, array $cachewarnings = array(), $eventshandlers = 0,
             $themedesignermode = false, $devlibdir = false, $mobileconfigured = false,
-            $overridetossl = false, $invalidforgottenpasswordurl = false, $croninfrequent = false) {
+            $overridetossl = false, $invalidforgottenpasswordurl = false, $croninfrequent = false,
+            $showcampaigncontent = false) {
+
         global $CFG;
         $output = '';
 
@@ -312,6 +315,7 @@ class core_admin_renderer extends plugin_renderer_base {
         $output .= $this->registration_warning($registered);
         $output .= $this->mobile_configuration_warning($mobileconfigured);
         $output .= $this->forgotten_password_url_warning($invalidforgottenpasswordurl);
+        $output .= $this->campaign_content($showcampaigncontent);
 
         //////////////////////////////////////////////////////////////////////////////////////////////////
         ////  IT IS ILLEGAL AND A VIOLATION OF THE GPL TO HIDE, REMOVE OR MODIFY THIS COPYRIGHT NOTICE ///
@@ -878,6 +882,20 @@ class core_admin_renderer extends plugin_renderer_base {
         return $output;
     }
 
+    /**
+     * Display campaign content.
+     *
+     * @param bool $showcampaigncontent Whether the campaign content should be visible or not.
+     * @return string the campaign content raw html.
+     */
+    protected function campaign_content(bool $showcampaigncontent): string {
+        if (!$showcampaigncontent) {
+            return '';
+        }
+
+        return $this->render_from_template('core/campaign_content', ['lang' => current_language()]);
+    }
+
     /**
      * Display a warning about the forgotten password URL not linking to a valid URL.
      *
index a3a3a22..498aa2c 100644 (file)
 }}
 <div class="form-item row" id="{{id}}">
     <div class="form-label col-sm-3 text-sm-right">
-        <label {{#labelfor}}for="{{labelfor}}"{{/labelfor}}>
-            {{{title}}}
-            {{#override}}
-                <div class="alert alert-info">{{override}}</div>
-            {{/override}}
-            {{#warning}}
-                <div class="alert alert-warning">{{warning}}</div>
-            {{/warning}}
-        </label>
+        {{#customcontrol}}
+            <p {{#labelfor}}id="{{labelfor}}_label"{{/labelfor}}>
+                {{{title}}}
+                {{#override}}
+                    <div class="alert alert-info">{{override}}</div>
+                {{/override}}
+                {{#warning}}
+                    <div class="alert alert-warning">{{warning}}</div>
+                {{/warning}}
+            </p>
+        {{/customcontrol}}
+        {{^customcontrol}}
+            <label {{#labelfor}}for="{{labelfor}}"{{/labelfor}}>
+                {{{title}}}
+                {{#override}}
+                    <div class="alert alert-info">{{override}}</div>
+                {{/override}}
+                {{#warning}}
+                    <div class="alert alert-warning">{{warning}}</div>
+                {{/warning}}
+            </label>
+        {{/customcontrol}}
         <span class="form-shortname d-block small text-muted">{{{name}}}</span>
     </div>
     <div class="form-setting col-sm-9">
         {{#dependenton}}<div class="form-dependenton mb-4 text-muted">{{{.}}}</div>{{/dependenton}}
     </div>
 </div>
+{{#customcontrol}}
+    {{#js}}
+        require(['jquery'], function($) {
+            $('#{{id}}_label').css('cursor', 'default');
+            $('#{{id}}_label').click(function() {
+                $('#{{id}}')
+                    .find('button, a, input:not([type="hidden"]), select, textarea, [tabindex]')
+                    .filter(':not([disabled]):not([tabindex="0"]):not([tabindex="-1"])')
+                    .first().focus();
+            });
+        });
+    {{/js}}
+{{/customcontrol}}
index bd815e6..ee1b61b 100644 (file)
@@ -267,9 +267,11 @@ class core_block_externallib_testcase extends externallib_advanced_testcase {
             $returnedblocks[] = $block['name'];
             // Check the configuration returned for this default block.
             if ($block['name'] == 'recentlyaccessedcourses') {
-                $this->assertEquals('displaycategories', $block['configs'][0]['name']);
-                $this->assertEquals(json_encode('0'), $block['configs'][0]['value']);
-                $this->assertEquals('plugin', $block['configs'][0]['type']);
+                // Convert config to associative array to avoid DB sorting randomness.
+                $config = array_column($block['configs'], null, 'name');
+                $this->assertArrayHasKey('displaycategories', $config);
+                $this->assertEquals(json_encode('0'), $config['displaycategories']['value']);
+                $this->assertEquals('plugin', $config['displaycategories']['type']);
             }
         }
         // Remove lp block.
index ccc760a..995e0df 100644 (file)
@@ -1055,6 +1055,15 @@ $CFG->admin = 'admin';
 //      $CFG->alternative_file_system_class = '\\local_myfilestorage\\file_system';
 //
 //=========================================================================
+// 15. CAMPAIGN CONTENT
+//=========================================================================
+//
+// We have added a campaign content to the notifications page, in case you want to hide that from your site you just
+// need to set showcampaigncontent setting to false.
+//
+//      $CFG->showcampaigncontent = true;
+//
+//=========================================================================
 // ALL DONE!  To continue installation, visit your main page with a browser
 //=========================================================================
 
index b851222..93b512b 100644 (file)
@@ -101,7 +101,7 @@ class bankcontent implements renderable, templatable {
         // The tools are displayed in the action bar on the index page.
         foreach ($this->toolbar as $tool) {
             // Customize the output of a tool, like dropdowns.
-            $method = 'export_tool_'.$tool['name'];
+            $method = 'export_tool_'.$tool['action'];
             if (method_exists($this, $method)) {
                 $this->$method($tool);
             }
index e4bb6d6..5608a9a 100644 (file)
@@ -70,7 +70,12 @@ if (has_capability('moodle/contentbank:useeditor', $context)) {
     if (!empty($editabletypes)) {
         // Editor base URL.
         $editbaseurl = new moodle_url('/contentbank/edit.php', ['contextid' => $contextid]);
-        $toolbar[] = ['name' => get_string('add'), 'link' => $editbaseurl, 'dropdown' => true, 'contenttypes' => $editabletypes];
+        $toolbar[] = [
+            'name' => get_string('add'),
+            'link' => $editbaseurl, 'dropdown' => true,
+            'contenttypes' => $editabletypes,
+            'action' => 'add'
+        ];
     }
 }
 
@@ -80,7 +85,12 @@ if (has_capability('moodle/contentbank:upload', $context)) {
     $accepted = $cb->get_supported_extensions_as_string($context);
     if (!empty($accepted)) {
         $importurl = new moodle_url('/contentbank/upload.php', ['contextid' => $contextid]);
-        $toolbar[] = array('name' => get_string('upload', 'contentbank'), 'link' => $importurl, 'icon' => 'i/upload');
+        $toolbar[] = [
+            'name' => get_string('upload', 'contentbank'),
+            'link' => $importurl,
+            'icon' => 'i/upload',
+            'action' => 'upload'
+        ];
     }
 }
 
index 020b905..294ac41 100644 (file)
                         <div class="cb-thumbnail" role="img" aria-label="{{{ name }}}"
                         style="background-image: url('{{{ icon }}}');">
                         </div>
-                        <a href="{{{ link }}}" class="cb-link stretched-link">
+                        <a href="{{{ link }}}" class="cb-link stretched-link" title="{{{ name }}}">
                             <span class="cb-name word-break-all clamp-2" data-region="cb-content-name">
                                 {{{ name }}}
                             </span>
index 7a2fbf5..f49dbd6 100644 (file)
                 {{/typeeditorparams}}
                 {{#typeeditorparams}}
                     <a class="dropdown-item icon-size-4" href="{{{ baseurl }}}&{{{ typeeditorparams }}}">
-                        <img alt="" class="icon" src="{{{ typeicon }}}"> {{ typename }}
+                        {{#typeicon}}
+                            <img alt="" class="icon" src="{{{ typeicon }}}">
+                        {{/typeicon}}
+                        {{^typeicon}}
+                            {{#pix}} b/h5p_library, core {{/pix}}
+                        {{/typeicon}} {{ typename }}
                     </a>
                 {{/typeeditorparams}}
             {{/types}}
index 713768c..aef6eab 100644 (file)
@@ -97,3 +97,21 @@ Feature: Content bank use editor feature
       | moodle/contentbank:useeditor     | Prohibit   | editingteacher | System       |           |
     And I reload the page
     Then "[data-action=Add-content]" "css_element" should not exist
+
+  Scenario: Users can edit content and save changes
+    Given the following "contentbank content" exist:
+      | contextlevel | reference | contenttype     | user  | contentname             | filepath                                    |
+      | System       |           | contenttype_h5p | admin | filltheblanks.h5p       | /h5p/tests/fixtures/filltheblanks.h5p       |
+    And I click on "Site pages" "list_item" in the "Navigation" "block"
+    And I click on "Content bank" "link" in the "Navigation" "block"
+    And I click on "filltheblanks.h5p" "link"
+    And I click on "Edit" "link"
+    And I switch to "h5p-editor-iframe" class iframe
+    And the field "Title" matches value "Geography"
+    And I set the field "Title" to "New title"
+    And I switch to the main frame
+    When I click on "Save" "button"
+    And I should see "filltheblanks.h5p" in the "h1" "css_element"
+    And I click on "Edit" "link"
+    And I switch to "h5p-editor-iframe" class iframe
+    Then the field "Title" matches value "New title"
index da936c1..82e4295 100644 (file)
Binary files a/course/amd/build/activitychooser.min.js and b/course/amd/build/activitychooser.min.js differ
index 83b317b..288328b 100644 (file)
Binary files a/course/amd/build/activitychooser.min.js.map and b/course/amd/build/activitychooser.min.js.map differ
index 0aa10d5..3704632 100644 (file)
@@ -91,13 +91,23 @@ const registerListenerEvents = (courseId, chooserConfig) => {
     events.forEach((event) => {
         document.addEventListener(event, async(e) => {
             if (e.target.closest(selectors.elements.sectionmodchooser)) {
+                let caller;
                 // We need to know who called this.
                 // Standard courses use the ID in the main section info.
                 const sectionDiv = e.target.closest(selectors.elements.section);
                 // Front page courses need some special handling.
                 const button = e.target.closest(selectors.elements.sectionmodchooser);
+
                 // If we don't have a section ID use the fallback ID.
-                const caller = sectionDiv || button;
+                // We always want the sectionDiv caller first as it keeps track of section ID's after DnD changes.
+                // The button attribute is always just a fallback for us as the section div is not always available.
+                // A YUI change could be done maybe to only update the button attribute but we are going for minimal change here.
+                if (sectionDiv !== null && sectionDiv.hasAttribute('data-sectionid')) {
+                    // We check for attributes just in case of outdated contrib course formats.
+                    caller = sectionDiv;
+                } else {
+                    caller = button;
+                }
 
                 // We want to show the modal instantly but loading whilst waiting for our data.
                 let bodyPromiseResolver;
index 382287d..ebb0e4a 100644 (file)
@@ -15,6 +15,9 @@ Overview of this plugin type at http://docs.moodle.org/dev/Course_formats
   preference is set, or when the theme sets $THEME->enablecourseajaxtheme to false. Formats which directly access
   the '.section_add_menus' element or its children should be updated accordingly.
 
+* section_header() now needs to include 'data-sectionid' => $section->section in the .section li to ensure correct section
+  selection for the Activity Chooser.
+
 === 3.8 ===
 
 * The following functions have been finally deprecated and can not be used anymore:
index 8d85c1c..f693b3c 100644 (file)
@@ -65,7 +65,7 @@ class filter_displayh5p extends moodle_text_filter {
         $allowedsources = get_config('filter_displayh5p', 'allowedsources');
         $allowedsources = array_filter(array_map('trim', explode("\n", $allowedsources)));
 
-        $localsource = '('.preg_quote($CFG->wwwroot).'/[^ &\#"\'<]*\.h5p([?][^ "\'<]*)?[^ \#"\'<]*)';
+        $localsource = '('.preg_quote($CFG->wwwroot, '~').'/[^ &\#"\'<]*\.h5p([?][^ "\'<]*)?[^ \#"\'<]*)';
         $allowedsources[] = $localsource;
 
         $params = array(
index aa58497..152cd65 100644 (file)
@@ -86,6 +86,17 @@ if ($action == 'moveselect') {
 $grade_edit_tree = new grade_edit_tree($gtree, $movingeid, $gpr);
 
 switch ($action) {
+    case 'duplicate':
+        if ($eid and confirm_sesskey()) {
+            if (!$el = $gtree->locate_element($eid)) {
+                print_error('invalidelementid', '', $returnurl);
+            }
+
+            $object->duplicate();
+            redirect($returnurl);
+        }
+        break;
+
     case 'delete':
         if ($eid && confirm_sesskey()) {
             if (!$grade_edit_tree->element_deletable($element)) {
index 8eac5fe..91744d8 100644 (file)
@@ -149,6 +149,18 @@ class grade_edit_tree {
                 $actionsmenu->add($icon);
             }
 
+            if ($this->element_duplicatable($element)) {
+                $duplicateparams = array();
+                $duplicateparams['id'] = $COURSE->id;
+                $duplicateparams['action'] = 'duplicate';
+                $duplicateparams['eid'] = $eid;
+                $duplicateparams['sesskey'] = sesskey();
+                $aurl = new moodle_url('index.php', $duplicateparams);
+                $duplicateicon = new pix_icon('t/copy', get_string('duplicate'));
+                $icon = new action_menu_link_secondary($aurl, $duplicateicon, get_string('duplicate'));
+                $actionsmenu->add($icon);
+            }
+
             $aurl = new moodle_url('index.php', array('id' => $COURSE->id, 'action' => 'moveselect', 'eid' => $eid, 'sesskey' => sesskey()));
             $moveaction .= $OUTPUT->action_icon($aurl, new pix_icon('t/move', get_string('move')));
         }
@@ -460,6 +472,24 @@ class grade_edit_tree {
         return false;
     }
 
+    /**
+     * Given an element of the grade tree, returns whether it is duplicatable or not (only manual grade items are duplicatable)
+     *
+     * @param array $element
+     * @return bool
+     */
+    public function element_duplicatable($element) {
+        if ($element['type'] != 'item') {
+            return false;
+        }
+
+        $gradeitem = $element['object'];
+        if ($gradeitem->itemtype != 'mod') {
+            return true;
+        }
+        return false;
+    }
+
     /**
      * Given the grade tree and an array of element ids (e.g. c15, i42), and expecting the 'moveafter' URL param,
      * moves the selected items to the requested location. Then redirects the user to the given $returnurl
index 5227baf..40a9b11 100644 (file)
@@ -725,13 +725,15 @@ class grade_report_grader extends grade_report {
             $usercell->scope = 'row';
 
             if ($showuserimage) {
-                $usercell->text = $OUTPUT->user_picture($user, array('visibletoscreenreaders' => false));
+                $usercell->text = $OUTPUT->user_picture($user, ['link' => false, 'visibletoscreenreaders' => false]);
             }
 
             $fullname = fullname($user, $viewfullnames);
-            $usercell->text .= html_writer::link(new moodle_url('/user/view.php', array('id' => $user->id, 'course' => $this->course->id)), $fullname, array(
-                'class' => 'username',
-            ));
+            $usercell->text = html_writer::link(
+                    new moodle_url('/user/view.php', ['id' => $user->id, 'course' => $this->course->id]),
+                    $usercell->text . $fullname,
+                    ['class' => 'username']
+            );
 
             if (!empty($user->suspendedenrolment)) {
                 $usercell->attributes['class'] .= ' usersuspended';
@@ -753,13 +755,18 @@ class grade_report_grader extends grade_report {
                 $a = new stdClass();
                 $a->user = $fullname;
                 $strgradesforuser = get_string('gradesforuser', 'grades', $a);
-                $url = new moodle_url('/grade/report/'.$CFG->grade_profilereport.'/index.php', array('userid' => $user->id, 'id' => $this->course->id));
-                $userreportcell->text .= $OUTPUT->action_icon($url, new pix_icon('t/grades', $strgradesforuser));
+                $url = new moodle_url('/grade/report/'.$CFG->grade_profilereport.'/index.php',
+                        ['userid' => $user->id, 'id' => $this->course->id]);
+                $userreportcell->text .= $OUTPUT->action_icon($url, new pix_icon('t/grades', ''), null,
+                        ['title' => $strgradesforuser, 'aria-label' => $strgradesforuser]);
             }
 
             if ($canseesingleview) {
-                $url = new moodle_url('/grade/report/singleview/index.php', array('id' => $this->course->id, 'itemid' => $user->id, 'item' => 'user'));
-                $singleview = $OUTPUT->action_icon($url, new pix_icon('t/editstring', get_string('singleview', 'grades', $fullname)));
+                $strsingleview = get_string('singleview', 'grades', $fullname);
+                $url = new moodle_url('/grade/report/singleview/index.php',
+                        ['id' => $this->course->id, 'itemid' => $user->id, 'item' => 'user']);
+                $singleview = $OUTPUT->action_icon($url, new pix_icon('t/editstring', ''), null,
+                        ['title' => $strsingleview, 'aria-label' => $strsingleview]);
                 $userreportcell->text .= $singleview;
             }
 
@@ -913,13 +920,16 @@ class grade_report_grader extends grade_report {
                         if (has_all_capabilities(array('gradereport/singleview:view', 'moodle/grade:viewall',
                             'moodle/grade:edit'), $this->context)) {
 
+                            $strsingleview = get_string('singleview', 'grades', $element['object']->get_name());
                             $url = new moodle_url('/grade/report/singleview/index.php', array(
                                 'id' => $this->course->id,
                                 'item' => 'grade',
                                 'itemid' => $element['object']->id));
                             $singleview = $OUTPUT->action_icon(
-                                $url,
-                                new pix_icon('t/editstring', get_string('singleview', 'grades', $element['object']->get_name()))
+                                    $url,
+                                    new pix_icon('t/editstring', ''),
+                                    null,
+                                    ['title' => $strsingleview, 'aria-label' => $strsingleview]
                             );
                         }
                     }
@@ -1269,7 +1279,8 @@ class grade_report_grader extends grade_report {
         $fulltable = new html_table();
         $fulltable->attributes['class'] = 'gradereport-grader-table';
         $fulltable->id = 'user-grades';
-        $fulltable->summary = get_string('summarygrader', 'gradereport_grader');
+        $fulltable->caption = get_string('summarygrader', 'gradereport_grader');
+        $fulltable->captionhide = true;
 
         // Extract rows from each side (left and right) and collate them into one row each
         foreach ($leftrows as $key => $row) {
@@ -1637,24 +1648,32 @@ class grade_report_grader extends grade_report {
 
             if (in_array($element['object']->id, $this->collapsed['aggregatesonly'])) {
                 $url->param('action', 'switch_plus');
-                $icon = $OUTPUT->action_icon($url, new pix_icon('t/switch_plus', $strswitchplus), null, null);
+                $icon = $OUTPUT->action_icon($url, new pix_icon('t/switch_plus', ''), null,
+                        ['title' => $strswitchplus, 'aria-label' => $strswitchplus]);
                 $showing = get_string('showingaggregatesonly', 'grades');
             } else if (in_array($element['object']->id, $this->collapsed['gradesonly'])) {
                 $url->param('action', 'switch_whole');
-                $icon = $OUTPUT->action_icon($url, new pix_icon('t/switch_whole', $strswitchwhole), null, null);
+                $icon = $OUTPUT->action_icon($url, new pix_icon('t/switch_whole', ''), null,
+                        ['title' => $strswitchwhole, 'aria-label' => $strswitchwhole]);
                 $showing = get_string('showinggradesonly', 'grades');
             } else {
                 $url->param('action', 'switch_minus');
-                $icon = $OUTPUT->action_icon($url, new pix_icon('t/switch_minus', $strswitchminus), null, null);
+                $icon = $OUTPUT->action_icon($url, new pix_icon('t/switch_minus', ''), null,
+                        ['title' => $strswitchminus, 'aria-label' => $strswitchminus]);
                 $showing = get_string('showingfullmode', 'grades');
             }
         }
 
         $name = $element['object']->get_name();
-        $courseheaderid = 'courseheader_' . clean_param($name, PARAM_ALPHANUMEXT);
-        $courseheader = html_writer::tag('span', $name, array('id' => $courseheaderid,
-                'title' => $name, 'class' => 'gradeitemheader'));
-        $courseheader .= html_writer::label($showing, $courseheaderid, false, array('class' => 'accesshide'));
+        $describedbyid = uniqid();
+        $courseheader = html_writer::tag('span', $name, [
+            'title' => $name,
+            'class' => 'gradeitemheader',
+            'aria-describedby' => $describedbyid
+        ]);
+        $courseheader .= html_writer::div($showing, 'sr-only', [
+            'id' => $describedbyid
+        ]);
         $courseheader .= $icon;
 
         return $courseheader;
index 70be5da..8df6324 100644 (file)
@@ -417,8 +417,8 @@ abstract class grade_report {
         $matrix = array('up' => 'desc', 'down' => 'asc', 'move' => 'desc');
         $strsort = $this->get_lang_string('sort' . $matrix[$direction]);
 
-        $arrow = $OUTPUT->pix_icon($pix[$direction], $strsort, '', array('class' => 'sorticon'));
-        return html_writer::link($sortlink, $arrow, array('title'=>$strsort));
+        $arrow = $OUTPUT->pix_icon($pix[$direction], '', '', ['class' => 'sorticon']);
+        return html_writer::link($sortlink, $arrow, ['title' => $strsort, 'aria-label' => $strsort]);
     }
 
     /**
index b17c4cd..b5e6dde 100644 (file)
@@ -117,6 +117,29 @@ class behat_grade extends behat_base {
             "//tr[descendant::*[text() = " . $this->escape($gradeitem) . "]]", 'xpath_element'));
     }
 
+    /**
+     * Duplicates a grade item or category.
+     *
+     * Teacher must be on the grade setup page.
+     *
+     * @Given /^I duplicate the grade item "(?P<grade_item_string>(?:[^"]|\\")*)"$/
+     * @param string $gradeitem
+     */
+    public function i_duplicate_the_grade_item($gradeitem) {
+
+        $gradeitem = behat_context_helper::escape($gradeitem);
+
+        if ($this->running_javascript()) {
+            $xpath = "//tr[contains(.,$gradeitem)]//*[contains(@class,'moodle-actionmenu')]//a[contains(@class,'toggle-display')]";
+            if ($this->getSession()->getPage()->findAll('xpath', $xpath)) {
+                $this->execute("behat_general::i_click_on", array($this->escape($xpath), "xpath_element"));
+            }
+        }
+
+        $this->execute("behat_general::i_click_on_in_the", array(get_string('duplicate'), 'link',
+            "//tr[descendant::*[text() = " . $this->escape($gradeitem) . "]]", 'xpath_element'));
+    }
+
     /**
      * Sets a calculated manual grade item. Needs a table with item name - idnumber relation.
      * The step requires you to be in the 'Gradebook setup' page.
diff --git a/grade/tests/behat/grade_item_duplication.feature b/grade/tests/behat/grade_item_duplication.feature
new file mode 100644 (file)
index 0000000..8bbcdad
--- /dev/null
@@ -0,0 +1,45 @@
+@core @core_grades
+Feature: We can duplicate grade items that already exist.
+  In order to quickly create grade items that have similar settings.
+  As a teacher
+  I need to duplicate an existing grade item and check that its values are properly duplicated.
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname | format |
+      | Course 1 | C1 | topics |
+    And the following "grade categories" exist:
+      | fullname  | course |
+      | Category1 | C1     |
+    And the following "activities" exist:
+      | activity | course | idnumber | name        | gradecategory |
+      | assign   | C1     | a1       | Assignment1 | Category1     |
+    And the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+    And the following "grade items" exist:
+      | itemname | course | category  | idnumber | gradetype | grademax | grademin | gradepass | display | decimals | hidden | weightoverride |
+      | Item1    | C1     | Category1 | 001      | Value     | 80.00    | 5.00     | 40.00     | 1       | 1        | 0      | 1              |
+
+  Scenario: Ensure the duplicated grade item settings match the original grade item
+    Given I log in as "teacher1"
+    And I am on "Course 1" course homepage
+    And I navigate to "Setup > Gradebook setup" in the course gradebook
+    And I should not see "Duplicate   Category1"
+    And I should not see "Duplicate   Assignment1"
+    When I duplicate the grade item "Item1"
+    Then I should see "Item1 (copy)"
+    And I follow "Edit   Item1 (copy)"
+    And the field "Item name" matches value "Item1 (copy)"
+    And the field "ID number" matches value ""
+    And the field "Grade type" matches value "Value"
+    And the field "Maximum grade" matches value "80.00"
+    And the field "Minimum grade" matches value "5.00"
+    And the field "Grade to pass" matches value "40.00"
+    And the field "Grade display type" matches value "Real"
+    And the field "Overall decimal places" matches value "1"
+    And the field "Hidden" matches value "0"
+    And the field "Weight adjusted" matches value "1"
index 8d7d442..a33bad7 100644 (file)
Binary files a/h5p/amd/build/editor_display.min.js and b/h5p/amd/build/editor_display.min.js differ
index 9560735..09d896b 100644 (file)
Binary files a/h5p/amd/build/editor_display.min.js.map and b/h5p/amd/build/editor_display.min.js.map differ
index 8b89e4f..e27dcff 100644 (file)
@@ -59,4 +59,5 @@ export const init = (elementId) => {
         inputname,
         cancelSubmitCallback
     );
+    document.querySelector('#' + elementId + ' iframe').setAttribute('name', 'h5p-editor');
 };
index 4524f0a..2e4a2ca 100644 (file)
@@ -796,7 +796,12 @@ class framework implements \H5PFrameworkInterface {
         }
 
         $content['disable'] = $content['disable'] ?? null;
-
+        // Add title to 'params' to use in the editor.
+        if (!empty($content['title'])) {
+            $params = json_decode($content['params']);
+            $params->title = $content['title'];
+            $content['params'] = json_encode($params);
+        }
         $data = [
             'jsoncontent' => $content['params'],
             'displayoptions' => $content['disable'],
@@ -1206,6 +1211,10 @@ class framework implements \H5PFrameworkInterface {
         if (empty($params->metadata)) {
             $params->metadata = new \stdClass();
         }
+        // Add title to metadata.
+        if (!empty($params->title) && empty($params->metadata->title)) {
+            $params->metadata->title = $params->title;
+        }
         $content['metadata'] = $params->metadata;
         $content['params'] = json_encode($params->params ?? $params);
 
index 8e85ffc..721187e 100644 (file)
@@ -75,6 +75,10 @@ class helper {
             ];
             $options = ['disable' => self::get_display_options($core, $config)];
 
+            // Add the 'title' if exists from 'h5p.json' data to keep it for the editor.
+            if (!empty($h5pvalidator->h5pC->mainJsonData['title'])) {
+                $content['title'] = $h5pvalidator->h5pC->mainJsonData['title'];
+            }
             $h5pstorage->savePackage($content, null, $skipcontent, $options);
 
             return $h5pstorage->contentId;
diff --git a/install/lang/hat/admin.php b/install/lang/hat/admin.php
new file mode 100644 (file)
index 0000000..66d75af
--- /dev/null
@@ -0,0 +1,35 @@
+<?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Automatically generated strings for Moodle installer
+ *
+ * Do not edit this file manually! It contains just a subset of strings
+ * needed during the very first steps of installation. This file was
+ * generated automatically by export-installer.php (which is part of AMOS
+ * {@link http://docs.moodle.org/dev/Languages/AMOS}) using the
+ * list of strings defined in /install/stringnames.txt.
+ *
+ * @package   installer
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['clianswerno'] = 'n';
+$string['cliansweryes'] = 'w';
+$string['cliyesnoprompt'] = 'Tape w (pou wi) oswa n (pou non)';
diff --git a/install/lang/mwl/langconfig.php b/install/lang/mwl/langconfig.php
new file mode 100644 (file)
index 0000000..f5455b1
--- /dev/null
@@ -0,0 +1,33 @@
+<?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Automatically generated strings for Moodle installer
+ *
+ * Do not edit this file manually! It contains just a subset of strings
+ * needed during the very first steps of installation. This file was
+ * generated automatically by export-installer.php (which is part of AMOS
+ * {@link http://docs.moodle.org/dev/Languages/AMOS}) using the
+ * list of strings defined in /install/stringnames.txt.
+ *
+ * @package   installer
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['thislanguage'] = 'Mirand├ęs';
index f717e4f..11d5b11 100644 (file)
@@ -163,6 +163,7 @@ $string['dropped'] = 'Dropped';
 $string['droplowestvalues'] = 'Drop {$a} lowest values';
 $string['dropxlowest'] = 'Drop X lowest';
 $string['dropxlowestwarning'] = 'Note: If you use drop x lowest the grading assumes that all items in the category have the same point value. If point values differ results will be unpredictable';
+$string['duplicatedgradeitem'] = '{$a} (copy)';
 $string['duplicatescale'] = 'Duplicate scale';
 $string['edit'] = 'Edit';
 $string['editcalculation'] = 'Edit calculation';
index eaefd6b..7e63155 100644 (file)
@@ -2234,7 +2234,7 @@ function reset_role_capabilities($roleid) {
  * the database.
  *
  * @access private
- * @param string $component examples: 'moodle', 'mod/forum', 'block/quiz_results'
+ * @param string $component examples: 'moodle', 'mod_forum', 'block_quiz_results'
  * @return boolean true if success, exception in case of any problems
  */
 function update_capabilities($component = 'moodle') {
index 1541db2..fd87ec4 100644 (file)
@@ -1686,6 +1686,8 @@ abstract class admin_setting {
     private $forceltr = null;
     /** @var array list of other settings that may cause this setting to be hidden */
     private $dependenton = [];
+    /** @var bool Whether this setting uses a custom form control */
+    protected $customcontrol = false;
 
     /**
      * Constructor
@@ -2081,6 +2083,16 @@ abstract class admin_setting {
     public function get_dependent_on() {
         return $this->dependenton;
     }
+
+    /**
+     * Whether this setting uses a custom form control.
+     * This function is especially useful to decide if we should render a label element for this setting or not.
+     *
+     * @return bool
+     */
+    public function has_custom_form_control(): bool {
+        return $this->customcontrol;
+    }
 }
 
 /**
@@ -8925,6 +8937,7 @@ function format_admin_setting($setting, $title='', $form='', $description='', $l
     $context->description = highlight($query, markdown_to_html($description));
     $context->element = $form;
     $context->forceltr = $setting->get_force_ltr();
+    $context->customcontrol = $setting->has_custom_form_control();
 
     return $OUTPUT->render_from_template('core_admin/setting', $context);
 }
@@ -10384,6 +10397,7 @@ class admin_setting_configstoredfile extends admin_setting {
         $this->filearea = $filearea;
         $this->itemid   = $itemid;
         $this->options  = (array)$options;
+        $this->customcontrol = true;
     }
 
     /**
index a1825a6..1b2d43e 100644 (file)
@@ -221,7 +221,9 @@ XPATH
 .//*[contains(., %locator%) and not(.//*[contains(., %locator%)])]
 XPATH
         , 'form_row' => <<<XPATH
-.//*[self::label or self::div[contains(concat(' ', @class, ' '), ' fstaticlabel ')]][contains(., %locator%)]/ancestor::*[contains(concat(' ', @class, ' '), ' fitem ')]
+.//*[contains(concat(' ', @class, ' '), ' col-form-label ')]
+    [normalize-space(.)= %locator%]
+    /ancestor::*[contains(concat(' ', @class, ' '), ' fitem ')]
 XPATH
         , 'autocomplete_selection' => <<<XPATH
 .//div[contains(concat(' ', normalize-space(@class), ' '), concat(' ', 'form-autocomplete-selection', ' '))]/span[@role='listitem'][contains(normalize-space(.), %locator%)]
@@ -253,7 +255,7 @@ XPATH
         ,
             'filemanager' => <<<XPATH
 .//*[@data-fieldtype = 'filemanager' or @data-fieldtype = 'filepicker']
-    /descendant::input[@id = //label[contains(normalize-space(string(.)), %locator%)]/@for]
+    /descendant::input[@id = substring-before(//p[contains(normalize-space(string(.)), %locator%)]/@id, '_label')]
 XPATH
         ,
              'passwordunmask' => <<<XPATH
index 957a357..b4064ef 100644 (file)
@@ -74,7 +74,7 @@ trait core_behat_file_helper {
             $filepickerelement = behat_context_helper::escape($filepickerelement);
             $filepickercontainer = $this->find(
                     'xpath',
-                    "//input[./@id = //label[normalize-space(.)=$filepickerelement]/@for]" .
+                    "//input[./@id = substring-before(//p[normalize-space(.)=$filepickerelement]/@id, '_label')]" .
                     "//ancestor::*[@data-fieldtype = 'filemanager' or @data-fieldtype = 'filepicker']",
                     $exception
             );
index bb011e0..ff85a2f 100644 (file)
@@ -251,7 +251,7 @@ class behat_form_field {
         // Defaults to label.
         if ($locatortype == 'label' || $locatortype == false) {
 
-            $labelnode = $this->session->getPage()->find('xpath', '//label[@for="' . $fieldid . '"]');
+            $labelnode = $this->session->getPage()->find('xpath', "//label[@for='$fieldid']|//p[@id='{$fieldid}_label']");
 
             // Exception only if $locatortype was specified.
             if (!$labelnode && $locatortype == 'label') {
index 7d70949..1fa9ff6 100644 (file)
@@ -68,7 +68,7 @@ class behat_form_filemanager extends behat_form_field {
         $fieldlabel = $this->get_field_locator();
 
         // Get the name of the current directory elements.
-        $xpath = "//label[contains(., '" . $fieldlabel . "')]" .
+        $xpath = "//p[normalize-space(.)='$fieldlabel']" .
             "/ancestor::div[contains(concat(' ', normalize-space(@class), ' '), ' fitem ')]" .
             "/descendant::div[@data-fieldtype = 'filemanager']" .
             "/descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' fp-filename ')]";
index e8c1350..1a9ae7f 100644 (file)
@@ -2764,10 +2764,10 @@ $functions = array(
         'capabilities'  => '',
         'services'      => [MOODLE_OFFICIAL_MOBILE_SERVICE],
     ],
-    'core_table_dynamic_fetch' => [
-        'classname' => 'core_table\external\dynamic\fetch',
+    'core_table_get_dynamic_table_content' => [
+        'classname' => 'core_table\external\dynamic\get',
         'methodname' => 'execute',
-        'description' => 'Fetch a dynamic table view raw html',
+        'description' => 'Get the dynamic table content raw html',
         'type' => 'read',
         'ajax' => true,
         'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE],
index 3368bfc..5ffa220 100644 (file)
Binary files a/lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button-debug.js and b/lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button-debug.js differ
index 53089ce..652046d 100644 (file)
Binary files a/lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button-min.js and b/lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button-min.js differ
index 3368bfc..5ffa220 100644 (file)
Binary files a/lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button.js and b/lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button.js differ
index c242b70..39e20cb 100644 (file)
@@ -712,7 +712,9 @@ Y.namespace('M.atto_media').Button = Y.Base.create('button', Y.M.editor_atto.Edi
      */
     _getMediumProperties: function(medium) {
         var boolAttr = function(elem, attr) {
-            return elem.getAttribute(attr) ? true : false;
+            // As explained in MDL-64175, some OS (like Ubuntu), are removing the value for these attributes.
+            // So in order to check if attr="true", we need to check if the attribute exists and if the value is empty or true.
+            return (elem.hasAttribute(attr) && (elem.getAttribute(attr) || elem.getAttribute(attr) === ''));
         };
 
         var tracks = {
index 0584acc..a47a25c 100644 (file)
@@ -13,7 +13,6 @@
         value="{{element.selectedvalue}}"
     {{/element.selectedvalue}}
     {{#element.checked}}checked{{/element.checked}}
-    size="{{element.size}}"
     {{#error}}
         autofocus aria-describedby="{{element.iderror}}"
     {{/error}}
index 994ebc5..fb90de4 100644 (file)
@@ -13,7 +13,6 @@
         value="1"
     {{/element.value}}
     {{#element.checked}}checked{{/element.checked}}
-    size="{{element.size}}"
     {{#error}}
         autofocus aria-describedby="{{element.iderror}}"
     {{/error}}
index 6f02ca0..f2607eb 100644 (file)
@@ -1,5 +1,26 @@
 {{< core_form/element-template }}
+    {{$label}}
+        {{^element.hiddenlabel}}
+            <p id="{{element.id}}_label" class="col-form-label d-inline" aria-hidden="true">
+                {{{label}}}
+            </p>
+        {{/element.hiddenlabel}}
+    {{/label}}
     {{$element}}
-        {{{element.html}}}
+        <fieldset class="w-100 m-0 p-0 border-0" id="{{element.id}}_fieldset">
+            <legend class="sr-only">{{label}}</legend>
+            {{{element.html}}}
+        </fieldset>
     {{/element}}
 {{/ core_form/element-template }}
+{{#js}}
+(function() {
+    var label = document.getElementById('{{element.id}}_label');
+    if (label) {
+        label.style.cursor = 'default';
+        label.addEventListener('click', function() {
+            document.querySelectorAll('#{{element.id}}_fieldset div.fp-toolbar a')[0].focus();
+        });
+    }
+})();
+{{/js}}
index 6f02ca0..579cf20 100644 (file)
@@ -1,5 +1,26 @@
 {{< core_form/element-template }}
+    {{$label}}
+        {{^element.hiddenlabel}}
+            <p id="{{element.id}}_label" class="col-form-label d-inline" aria-hidden="true">
+                {{{label}}}
+            </p>
+        {{/element.hiddenlabel}}
+    {{/label}}
     {{$element}}
-        {{{element.html}}}
+        <fieldset class="w-100 m-0 p-0 border-0" id="{{element.id}}_fieldset">
+            <legend class="sr-only">{{label}}</legend>
+            {{{element.html}}}
+        </fieldset>
     {{/element}}
 {{/ core_form/element-template }}
+{{#js}}
+(function() {
+    var label = document.getElementById('{{element.id}}_label');
+    if (label) {
+        label.style.cursor = 'default';
+        label.addEventListener('click', function() {
+            document.querySelectorAll('#{{element.id}}_fieldset .fp-btn-choose')[0].focus();
+        });
+    }
+})();
+{{/js}}
index 2d3022e..bdddf75 100644 (file)
@@ -1,6 +1,6 @@
 {{< core_form/element-template-inline }}
     {{$element}}
-    <div class="d-flex flex-wrap">
+    <div class="d-flex flex-wrap align-items-center">
         {{#element.elements}}
             {{{separator}}}
             {{{html}}}
index 4664a09..d5cca77 100644 (file)
@@ -9,7 +9,7 @@
     {{$element}}
         <fieldset class="w-100 m-0 p-0 border-0">
             <legend class="sr-only">{{label}}</legend>
-            <div class="d-flex flex-wrap">
+            <div class="d-flex flex-wrap align-items-center">
             {{#element.elements}}
                 {{{separator}}}
                 {{{html}}}
 require(['jquery'], function($) {
     $('#{{element.id}}_label').css('cursor', 'default');
     $('#{{element.id}}_label').click(function() {
-        $('#{{element.id}}').find('button, a, input, select, textarea, [tabindex]:not([tabindex="-1"])').filter(':enabled').first().focus();
+        $('#{{element.id}}')
+            .find('button, a, input:not([type="hidden"]), select, textarea, [tabindex]')
+            .filter(':not([disabled]):not([tabindex="0"]):not([tabindex="-1"])')
+            .first().focus();
     });
 });
 {{/js}}
index 59da069..f832dec 100644 (file)
@@ -41,7 +41,7 @@
                 name="{{element.name}}"
                 id="{{element.id}}"
                 value="{{element.value}}"
-                size="{{element.size}}"
+                {{#element.size}}size="{{element.size}}"{{/element.size}}
                 {{#error}}
                     autofocus aria-describedby="{{element.iderror}}"
                 {{/error}} {{{element.attributes}}}>
index d6e80f8..9a7758d 100644 (file)
@@ -41,7 +41,7 @@
                 name="{{element.name}}"
                 id="{{element.id}}"
                 value="{{element.value}}"
-                size="{{element.size}}"
+                {{#element.size}}size="{{element.size}}"{{/element.size}}
                 {{#error}}
                     autofocus aria-describedby="{{element.iderror}}"
                 {{/error}} {{{element.attributes}}}>
index bbf538d..d9528fa 100644 (file)
@@ -49,7 +49,7 @@
             {{#advanced}}<abbr class="initialism text-info" title="{{#str}}advanced{{/str}}">!</abbr>{{/advanced}}
             {{{helpbutton}}}
         </span>
-        {{$ label }}
+        {{# label}}{{$ label }}
             {{^element.staticlabel}}
                 <label class="col-form-label d-inline {{#element.hiddenlabel}}sr-only{{/element.hiddenlabel}}" for="{{element.id}}">
                     {{{label}}}
@@ -60,7 +60,7 @@
                     {{{label}}}
                 </span>
             {{/element.staticlabel}}
-        {{/ label }}
+        {{/ label }}{{/ label}}
     </div>
     <div class="col-md-9 form-inline felement" data-fieldtype="{{element.type}}">
         {{$ element }}
index b6bb3e9..81f7a5a 100644 (file)
@@ -8,7 +8,7 @@
             readonly {{#element.hardfrozen}}disabled{{/element.hardfrozen}}
     {{/element.frozen}}
             value="{{element.value}}"
-            size="{{element.size}}"
+            {{#element.size}}size="{{element.size}}"{{/element.size}}
             {{#error}}
                 autofocus aria-describedby="{{element.iderror}}"
             {{/error}}
index 087201a..7e972a3 100644 (file)
@@ -8,7 +8,7 @@
         {{/element.frozen}}
                 id="{{element.id}}"
                 value="{{element.value}}"
-                size="{{element.size}}"
+                {{#element.size}}size="{{element.size}}"{{/element.size}}
                 {{#error}}
                     autofocus aria-describedby="{{element.iderror}}"
                 {{/error}}
index 5529d4a..e161c8f 100644 (file)
@@ -8,7 +8,7 @@
                 name="{{element.name}}"
                 id="{{element.id}}"
                 value="{{element.value}}"
-                size="{{element.size}}"
+                {{#element.size}}size="{{element.size}}"{{/element.size}}
                 {{#error}}
                     autofocus aria-describedby="{{element.iderror}}"
                 {{/error}}
index dd4a16a..1f21f76 100644 (file)
@@ -449,6 +449,41 @@ class grade_item extends grade_object {
         return true;
     }
 
+    /**
+     * Duplicate grade item.
+     *
+     * @return grade_item The duplicate grade item
+     */
+    public function duplicate() {
+        // Convert current object to array.
+        $copy = (array) $this;
+
+        if (empty($copy["id"])) {
+            throw new moodle_exception('invalidgradeitemid');
+        }
+
+        // Remove fields that will be either unique or automatically filled.
+        $removekeys = array();
+        $removekeys[] = 'id';
+        $removekeys[] = 'idnumber';
+        $removekeys[] = 'timecreated';
+        $removekeys[] = 'sortorder';
+        foreach ($removekeys as $key) {
+            unset($copy[$key]);
+        }
+
+        // Addendum to name.
+        $copy["itemname"] = get_string('duplicatedgradeitem', 'grades', $copy["itemname"]);
+
+        // Create new grade item.
+        $gradeitem = new grade_item($copy);
+
+        // Insert grade item into database.
+        $gradeitem->insert();
+
+        return $gradeitem;
+    }
+
     /**
      * In addition to perform parent::insert(), calls force_regrading() method too.
      *
index a420ceb..5c0b46b 100644 (file)
@@ -1050,4 +1050,78 @@ class core_grade_item_testcase extends grade_base_testcase {
         $this->assertEquals($gradeitem->itemmodule, $event->other['itemmodule']);
         $this->assertEquals('updatedname', $event->other['itemname']);
     }
+
+
+    /**
+     * Test grade item duplication expecting success.
+     */
+    public function test_grade_duplicate_grade_item_success() {
+        $cat = new grade_category();
+        $cat->courseid = $this->courseid;
+        $cat->fullname = 'Grade category';
+        $cat->insert();
+
+        // Method exists.
+        $gi = new grade_item();
+        $this->assertTrue(method_exists($gi, 'duplicate'));
+
+        // Grade item is inserted and valid for duplication.
+        $gi->courseid = $this->courseid;
+        $gi->categoryid = $cat->id;
+        $gi->itemtype = 'manual';
+        $gi->itemname = 'Grade Item 1';
+        $gi->idnumber = '1000';
+        $gi->insert();
+        $gi2 = $gi->duplicate();
+
+        $this->assertEquals($gi->courseid, $gi2->courseid);
+        $this->assertEquals($gi->categoryid, $gi2->categoryid);
+        $this->assertEquals($gi->itemtype, $gi2->itemtype);
+        $this->assertEquals($gi->gradetype, $gi2->gradetype);
+        $this->assertEquals($gi->grademax, $gi2->grademax);
+        $this->assertEquals($gi->grademin, $gi2->grademin);
+        $this->assertEquals($gi->gradepass, $gi2->gradepass);
+        $this->assertEquals($gi->display, $gi2->display);
+        $this->assertEquals($gi->decimals, $gi2->decimals);
+        $this->assertEquals($gi->hidden, $gi2->hidden);
+        $this->assertEquals($gi->weightoverride, $gi2->weightoverride);
+
+        $this->assertNotEquals($gi->id, $gi2->id);
+        $this->assertNotEquals($gi->idnumber, $gi2->idnumber);
+        $this->assertNotEquals($gi->sortorder, $gi2->sortorder);
+        $this->assertNotEquals($gi->itemname, $gi2->itemname);
+    }
+
+    /**
+     * Test grade item duplication exception expected with incomplete grade item.
+     */
+    public function test_grade_duplicate_grade_item_incomplete() {
+        // Grade item is not valid because it is empty.
+        $gi = new grade_item();
+        $gi->courseid = $this->courseid;
+        $this->expectException("moodle_exception");
+        $gi2 = $gi->duplicate();
+    }
+
+    /**
+     * Test grade item duplication exception expected because item must be in db.
+     */
+    public function test_grade_duplicate_grade_item_not_in_db() {
+        $cat = new grade_category();
+        $cat->courseid = $this->courseid;
+        $cat->fullname = 'Grade category';
+        $cat->insert();
+
+        // Grade item is valid for insertion but is not inserted into db.
+        // Duplicate method throws an exception.
+        $gi = new grade_item();
+        $gi->courseid = $this->courseid;
+        $gi->categoryid = $cat->id;
+        $gi->itemtype = 'manual';
+        $gi->itemname = 'Grade Item 1';
+        $gi->idnumber = '1000';
+
+        $this->expectException("moodle_exception");
+        $gi2 = $gi->duplicate();
+    }
 }
index 6c81e34..e08b3d3 100644 (file)
@@ -2765,6 +2765,12 @@ class html_table {
 
     /**
      * @var string Description of the contents for screen readers.
+     *
+     * The "summary" attribute on the "table" element is not supported in HTML5.
+     * Consider describing the structure of the table in a "caption" element or in a "figure" element containing the table;
+     * or, simplify the structure of the table so that no description is needed.
+     *
+     * @deprecated since Moodle 3.9.
      */
     public $summary;
 
index 859e424..470b3be 100644 (file)
@@ -560,7 +560,7 @@ function question_move_question_tags_to_new_context(array $questions, context $n
     $questionstagobjects = core_tag_tag::get_items_tags('core_question', 'question', $questionids);
 
     foreach ($questions as $question) {
-        $tagobjects = $questionstagobjects[$question->id];
+        $tagobjects = $questionstagobjects[$question->id] ?? [];
 
         foreach ($tagobjects as $tagobject) {
             $tagid = $tagobject->taginstanceid;
index ff5ca26..e07cb96 100644 (file)
Binary files a/lib/table/amd/build/local/dynamic/repository.min.js and b/lib/table/amd/build/local/dynamic/repository.min.js differ
index a8cb000..4cc7125 100644 (file)
Binary files a/lib/table/amd/build/local/dynamic/repository.min.js.map and b/lib/table/amd/build/local/dynamic/repository.min.js.map differ
index 3e43ac9..e548e97 100644 (file)
@@ -54,7 +54,7 @@ export const fetch = (component, handler, uniqueid, {
         hiddenColumns = {}
     } = {}, resetPreferences = false) => {
     return fetchMany([{
-        methodname: `core_table_dynamic_fetch`,
+        methodname: `core_table_get_dynamic_table_content`,
         args: {
             component,
             handler,
similarity index 99%
rename from lib/table/classes/external/dynamic/fetch.php
rename to lib/table/classes/external/dynamic/get.php
index ad2fcda..f21e463 100644 (file)
@@ -31,7 +31,6 @@ use external_multiple_structure;
 use external_single_structure;
 use external_value;
 use external_warnings;
-use moodle_url;
 
 /**
  * Core table external functions.
@@ -41,7 +40,7 @@ use moodle_url;
  * @copyright  2020 Simey Lameze <simey@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class fetch extends external_api {
+class get extends external_api {
 
     /**
      * Describes the parameters for fetching the table html.
@@ -140,7 +139,7 @@ class fetch extends external_api {
     }
 
     /**
-     * External function to fetch a table view.
+     * External function to get the table view content.
      *
      * @param string $component The component.
      * @param string $handler Dynamic table class name.
@@ -15,7 +15,7 @@
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * Unit tests for core_table\external\fetch;
+ * Unit tests for core_table\external\dynamic\get;
  *
  * @package   core_table
  * @category  test
@@ -31,14 +31,14 @@ use core_table\local\filter\filter;
 use advanced_testcase;
 
 /**
- * Unit tests for core_table\external\fetch;
+ * Unit tests for core_table\external\dynamic\get;
  *
  * @package   core_table
  * @category  test
  * @copyright  2020 Simey Lameze <simey@moodle.com>
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class fetch_test extends advanced_testcase {
+class get_test extends advanced_testcase {
 
     /**
      * Setup before class.
@@ -55,7 +55,7 @@ class fetch_test extends advanced_testcase {
         $this->resetAfterTest();
 
         $this->expectException(\invalid_parameter_exception::class);
-        fetch::execute(
+        get::execute(
             "core-user",
             "participants",
             "",
@@ -79,7 +79,7 @@ class fetch_test extends advanced_testcase {
         $this->resetAfterTest();
 
         $this->expectException(\UnexpectedValueException::class);
-        fetch::execute(
+        get::execute(
             "core_users",
             "participants",
             "",
@@ -106,7 +106,7 @@ class fetch_test extends advanced_testcase {
         $this->expectExceptionMessage("Table handler class {$handler} not found. Please make sure that your table handler class is under the \\core_user\\table namespace.");
 
         // Tests that invalid users_participants_table class gets an exception.
-        fetch::execute(
+        get::execute(
             "core_user",
             "users_participants_table",
             "",
@@ -143,7 +143,7 @@ class fetch_test extends advanced_testcase {
         $this->expectExceptionMessage("Invalid parameter value detected (filters => Invalid parameter value detected " .
         "(Missing required key in single structure: name): Missing required key in single structure: name");
 
-        fetch::execute(
+        get::execute(
             "core_user",
             "participants", "user-index-participants-{$course->id}",
             $this->get_sort_array(['firstname' => SORT_ASC]),
@@ -153,9 +153,9 @@ class fetch_test extends advanced_testcase {
     }
 
     /**
-     * Test execute fetch table.
+     * Test execute method.
      */
-    public function test_execute_fetch_table(): void {
+    public function test_table_get_execute(): void {
         $this->resetAfterTest();
 
         $course = $this->getDataGenerator()->create_course();
@@ -176,7 +176,7 @@ class fetch_test extends advanced_testcase {
             ]
         ];
 
-        $participantstable = fetch::execute(
+        $participantstable = get::execute(
             "core_user",
             "participants",
             "user-index-participants-{$course->id}",
diff --git a/lib/templates/campaign_content.mustache b/lib/templates/campaign_content.mustache
new file mode 100644 (file)
index 0000000..dbba382
--- /dev/null
@@ -0,0 +1,50 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template core/campaign_content
+
+    Moodle campaign content template.
+
+    The purpose of this template is to render an iframe that contains campaign content.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * lang User's language.
+
+    Example context (json):
+    { "lang": "en"}
+}}
+<div class="alert alert-secondary alert-block fade in alert-dismissible">
+    <button type="button" class="close" data-dismiss="alert">&times;</button>
+    <iframe id="campaign-content" class="w-100 border-0"></iframe>
+</div>
+{{#js}}
+(function() {
+    var iframe = document.getElementById('campaign-content');
+    iframe.src = 'https://campaign.moodle.org/current/lms/{{lang}}/';
+    window.addEventListener('message', function (event) {
+        if (event.origin === 'https://campaign.moodle.org') {
+            iframe.style.height = event.data + 'px';
+        }
+    });
+})();
+{{/js}}
index 09084b6..c827c9a 100644 (file)
                     </div>
                 </div>
                 <div class="fp-original form-group row mx-0">
-                    <label class="form-control-label col-4 px-0">{{#str}}original, repository{{/str}}</label>
+                    <div class="form-control-label col-4 px-0">{{#str}}original, repository{{/str}}</div>
                     <div class="col-8 form-inline">
                         <span class="fp-originloading">{{#pix}}i/loading_small{{/pix}} {{#str}}loading, repository{{/str}}</span><span class="fp-value"></span>
                     </div>
                 </div>
                 <div class="fp-reflist form-group row mx-0">
-                    <label class="form-control-label col-4 px-0">{{#str}}referenceslist, repository{{/str}}</label>
+                    <div class="form-control-label col-4 px-0">{{#str}}referenceslist, repository{{/str}}</div>
                     <div class="col-8 form-inline">
                         <p class="fp-refcount"></p>
                         <span class="fp-reflistloading">{{#pix}}i/loading_small{{/pix}} {{#str}}loading, repository{{/str}}</span>
index b7844a0..6551623 100644 (file)
Binary files a/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-debug.js and b/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-debug.js differ
index 596a348..56cfc98 100644 (file)
Binary files a/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-min.js and b/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-min.js differ
index 49a9120..e896593 100644 (file)
Binary files a/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue.js and b/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue.js differ
index 95a79e3..7c44355 100644 (file)
@@ -97,8 +97,10 @@ Y.extend(DIALOGUE, Y.Panel, {
         var bb;
 
         if (this.get('closeButton') !== false) {
-            // The buttons constructor does not allow custom attributes
-            this.get('buttons').header[0].setAttribute('title', this.get('closeButtonTitle'));
+            var title = this.get('closeButtonTitle');
+            // The buttons constructor does not allow custom attributes.
+            this.get('buttons').header[0].setAttribute('title', title);
+            this.get('buttons').header[0].setAttribute('aria-label', title);
         }
 
         // Initialise the element cache.
index d207c37..5c728cc 100644 (file)
            href="#"
            title="{{#str}} markallread {{/str}}"
            data-action="mark-all-read"
-           role="button">
-            <span class="normal-icon">{{#pix}} t/markasread, core, {{#str}} markallread {{/str}} {{/pix}}</span>
+           role="button"
+           aria-label="{{#str}} markallread {{/str}}">
+            <span class="normal-icon">{{#pix}} t/markasread, core {{/pix}}</span>
             {{> core/loading }}
         </a>
         <a href="{{{urls.preferences}}}"
-           title="{{#str}} notificationpreferences, message {{/str}}">
-            {{#pix}} i/settings, core, {{#str}} notificationpreferences, message {{/str}} {{/pix}}
+           title="{{#str}} notificationpreferences, message {{/str}}"
+           aria-label="{{#str}} notificationpreferences, message {{/str}}">
+            {{#pix}} i/settings, core {{/pix}}
         </a>
     {{/headeractions}}
 
index f359f34..4acd27d 100644 (file)
     <h3 class="h6 font-weight-bold">{{#str}} privacy, message {{/str}}</h3>
     <p>{{#str}} privacy_desc, message {{/str}}</p>
     <div data-preference="blocknoncontacts" class="mb-3">
-        {{#privacy}}
-            <div class="custom-control custom-radio mb-2">
-                <input
-                    type="radio"
-                    name="message_blocknoncontacts"
-                    class="custom-control-input"
-                    id="block-noncontacts-{{uniqid}}-{{value}}"
-                    value="{{value}}"
-                >
-                <label class="custom-control-label ml-2" for="block-noncontacts-{{uniqid}}-{{value}}">
-                    {{text}}
-                </label>
-            </div>
-        {{/privacy}}
+        <fieldset>
+            <legend class="sr-only">{{#str}} contactableprivacy, message {{/str}}</legend>
+            {{#privacy}}
+                <div class="custom-control custom-radio mb-2">
+                    <input
+                        type="radio"
+                        name="message_blocknoncontacts"
+                        class="custom-control-input"
+                        id="block-noncontacts-{{uniqid}}-{{value}}"
+                        value="{{value}}"
+                    >
+                    <label class="custom-control-label ml-2" for="block-noncontacts-{{uniqid}}-{{value}}">
+                        {{text}}
+                    </label>
+                </div>
+            {{/privacy}}
+        </fieldset>
     </div>
 
     <div class="hidden" data-region="notification-preference-container">
index 65c44fc..f47b39c 100644 (file)
@@ -15,7 +15,7 @@
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
-    @template core_message/message_drawer
+    @template core_message/message_index
 
     This template will render the message drawer.
 
index 0088715..26145ad 100644 (file)
@@ -201,7 +201,7 @@ class h5pactivity_summary_exporter extends exporter {
 
         $values['introfiles'] = external_util::get_area_files($context->id, 'mod_h5pactivity', 'intro', false, false);
 
-        $values['package'] = external_util::get_area_files($context->id, 'mod_h5pactivity', 'package', false, false);
+        $values['package'] = external_util::get_area_files($context->id, 'mod_h5pactivity', 'package', false, true);
 
         // Only if this H5P activity has been deployed, return the exported file.
         $fileh5p = api::get_export_info_from_context_id($context->id, $factory, 'mod_h5pactivity', 'package');
index 20a3811..ebfaa04 100644 (file)
@@ -889,15 +889,13 @@ table.quizreviewsummary td.cell {
 }
 
 #page-mod-quiz-edit ul.slots li.section li.activity .activityinstance {
-    display: block;
+    display: flex;
+    flex: 1 1 auto;
     min-height: 1.7em;
-    position: absolute;
-    top: 0;
-    left: 5em;
-    width: 100%;
 }
 
 #page-mod-quiz-edit ul.slots li.section li.activity .mod-indent-outer {
+    display: flex;
     padding-left: 22px;
 }
 
@@ -914,7 +912,6 @@ table.quizreviewsummary td.cell {
     white-space: nowrap;
     text-overflow: ellipsis;
     overflow: hidden;
-    width: 70%;
     display: inline-block;
     height: 20px;
 }
@@ -927,6 +924,9 @@ table.quizreviewsummary td.cell {
 #page-mod-quiz-edit ul.slots li.activity div.activityinstance .questionname {
     font-weight: bold;
     color: #555;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
 }
 
 #page-mod-quiz-edit ul.slots li.activity div.activityinstance .questiontext {
@@ -941,6 +941,10 @@ table.quizreviewsummary td.cell {
 
 #page-mod-quiz-edit ul.slots li.activity div.activityinstance .mod_quiz_random_qbank_link {
     font-size: 0.8em;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    margin-left: 0.25rem;
 }
 
 #page-mod-quiz-edit ul.slots .activityinstance img.activityicon {
@@ -950,6 +954,7 @@ table.quizreviewsummary td.cell {
 }
 
 #page-mod-quiz-edit .section .activity .actions {
+    position: inherit;
     white-space: nowrap;
     background: #e6e6e6;
     padding: 0.1em 0;
@@ -1219,8 +1224,10 @@ table#categoryquestions {
     #page-mod-quiz-edit ul.slots li.section li.activity .activityinstance {
         top: -30px;
         left: 0;
-    }
-    #page-mod-quiz-edit ul.slots .activityinstance span.instancename {
+        padding-right: 0;
+        overflow: hidden;
+        align-items: center;
+        position: absolute;
         width: 100%;
     }
 }
index fd0340d..ba9d231 100644 (file)
@@ -286,7 +286,6 @@ class moodle_content_writer implements content_writer {
         // This weird code is to look for a subcontext that contains a number and append an '_' to the front.
         // This is because there seems to be some weird problem with array_merge_recursive used in finalise_content().
         $subcontext = array_map(function($data) {
-            $data = clean_param($data, PARAM_PATH);
             if (stripos($data, DIRECTORY_SEPARATOR) !== false) {
                 $newpath = explode(DIRECTORY_SEPARATOR, $data);
                 $newpath = array_map(function($value) {
@@ -295,11 +294,18 @@ class moodle_content_writer implements content_writer {
                     }
                     return $value;
                 }, $newpath);
-                return implode(DIRECTORY_SEPARATOR, $newpath);
+                $data = implode(DIRECTORY_SEPARATOR, $newpath);
             } else if (is_numeric($data)) {
                 $data = '_' . $data;
             }
-            return $data;
+            // Because clean_param() normalises separators to forward-slashes
+            // and because there is code DIRECTORY_SEPARATOR dependent after
+            // this array_map(), we ensure we get the original separator.
+            // Note that maybe we could leave the clean_param() alone, but
+            // surely that means that the DIRECTORY_SEPARATOR dependent
+            // code is not needed at all. So better keep existing behavior
+            // until this is revisited.
+            return str_replace('/', DIRECTORY_SEPARATOR, clean_param($data, PARAM_PATH));
         }, $subcontext);
 
         // Combine the context path, and the subcontext data.
index 00c2afd..9e1953f 100644 (file)
Binary files a/question/type/ddimageortext/amd/build/question.min.js and b/question/type/ddimageortext/amd/build/question.min.js differ
index 675e499..1a2e53b 100644 (file)
Binary files a/question/type/ddimageortext/amd/build/question.min.js.map and b/question/type/ddimageortext/amd/build/question.min.js.map differ
index 9c7fd7b..248c56f 100644 (file)
@@ -508,6 +508,7 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys
                 break;
 
             case keys.escape:
+                questionManager.isKeyboardNavigation = false;
                 break;
 
             default:
index e7c4c4b..ad9644f 100644 (file)
@@ -260,13 +260,18 @@ YUI.add('moodle-core_filepicker', function(Y) {
                 // manually call dynload for parent elements in the tree so we can load other siblings
                 if (options.dynload) {
                     var root = scope.treeview.getRoot();
+                    // Whether search results are currently displayed in the active repository in the filepicker.
+                    // We do not want to load siblings of parent elements when displaying search tree results.
+                    var isSearchResult = typeof options.callbackcontext.active_repo !== 'undefined' &&
+                        options.callbackcontext.active_repo.issearchresult;
                     while (root && root.children && root.children.length) {
                         root = root.children[0];
                         if (root.path == mytreeel.path) {
                             root.origpath = options.filepath;
                             root.origlist = fileslist;
+                        } else if (!root.isLeaf && root.expanded && !isSearchResult) {
+                            Y.bind(options.treeview_dynload, options.callbackcontext)(root, null);
                         }
-                        // Removed bind as of MDL-62415 as it overwrites the search tree results
                     }
                 }
             } else {
index 2742d57..76c4359 100644 (file)
@@ -19,7 +19,7 @@ Feature: A selected file can be cancelled
       | Name | Folder name |
       | Description | Folder description |
     And I upload "lib/tests/fixtures/upload_users.csv" file to "Files" filemanager
-    And I click on "//label[contains(., 'Files')]/ancestor::div[contains(concat(' ', @class, ' '), ' fitem ')]//*[contains(@title, 'Add...')]" "xpath_element"
+    And I click on "Add..." "button" in the "Files" "form_row"
     And I click on "Recent files" "link" in the ".fp-repo-area" "css_element"
     And I click on "//a[contains(concat(' ', normalize-space(@class), ' '), ' fp-file ')][normalize-space(.)='empty.txt']" "xpath_element"
     And I click on ".moodle-dialogue-focused .fp-select .fp-select-cancel" "css_element"
index e7cd4eb..43bce52 100644 (file)
@@ -21,7 +21,7 @@ Feature: Select file feature
     And I click on "Save and display" "button"
     And I follow "Dashboard" in the user menu
     And I follow "Manage private files"
-    And I click on "//label[contains(., 'Files')]/ancestor::div[contains(concat(' ', @class, ' '), ' fitem ')]//*[contains(@title, 'Add...')]" "xpath_element"
+    And I click on "Add..." "button" in the "Files" "form_row"
     And I click on "Recent files" "link" in the ".fp-repo-area" "css_element"
     And I click on "Display folder with file icons" "link" in the ".file-picker" "css_element"
     And I click on "//a[contains(concat(' ', normalize-space(@class), ' '), ' fp-file ')][normalize-space(.)='empty.txt']" "xpath_element"
@@ -42,7 +42,7 @@ Feature: Select file feature
     And I click on "Save and display" "button"
     And I follow "Dashboard" in the user menu
     And I follow "Manage private files"
-    And I click on "//label[contains(., 'Files')]/ancestor::div[contains(concat(' ', @class, ' '), ' fitem ')]//*[contains(@title, 'Add...')]" "xpath_element"
+    And I click on "Add..." "button" in the "Files" "form_row"
     And I click on "Recent files" "link" in the ".fp-repo-area" "css_element"
     And I click on "Display folder with file details" "link" in the ".file-picker" "css_element"
     And I click on "//div[contains(concat(' ', normalize-space(@class), ' '), ' file-picker ')]/descendant::span[normalize-space(.)='empty.txt']/ancestor::a" "xpath_element"
@@ -63,7 +63,7 @@ Feature: Select file feature
     And I click on "Save and display" "button"
     And I follow "Dashboard" in the user menu
     And I follow "Manage private files"
-    And I click on "//label[contains(., 'Files')]/ancestor::div[contains(concat(' ', @class, ' '), ' fitem ')]//*[contains(@title, 'Add...')]" "xpath_element"
+    And I click on "Add..." "button" in the "Files" "form_row"
     And I click on "Recent files" "link" in the ".fp-repo-area" "css_element"
     And I click on "Display folder as file tree" "link" in the ".file-picker" "css_element"
     And I click on "//div[contains(concat(' ', normalize-space(@class), ' '), ' file-picker ')]/descendant::span[normalize-space(.)='empty.txt']/ancestor::a" "xpath_element"
index 74d6abf..52fa09b 100644 (file)
@@ -209,10 +209,8 @@ class behat_repository_upload extends behat_base {
             $filepickerelement = behat_context_helper::escape($filepickerelement);
             $filepickercontainer = $this->find(
                     'xpath',
-                    "//input[./@id = //label[normalize-space(.)=$filepickerelement]/@for]" .
-                    "//ancestor::div[contains(concat(' ', normalize-space(@class), ' '), ' felement ')] |" .
-                    "//input[./@id = //label[normalize-space(.)=$filepickerelement]/@for]" .
-                    "//ancestor::div[contains(concat(' ', normalize-space(@class), ' '), ' form-setting ')]",
+                    "//input[./@id = substring-before(//p[normalize-space(.)=$filepickerelement]/@id, '_label')]" .
+                    "//ancestor::*[@data-fieldtype = 'filemanager' or @data-fieldtype = 'filepicker']",
                     $exception
             );
         }
index 0eea802..a7bfc64 100644 (file)
     }
 }
 
-.cb-toolbar .dropdown-scrollable {
+.cb-toolbar-container .dropdown-scrollable {
     max-height: 190px;
     overflow-y: auto;
-}
\ No newline at end of file
+}
index 2bfa166..cc1ed11 100644 (file)
@@ -48,13 +48,7 @@ $gototop-bottom-position: 50px !default;
     opacity: 0;
     transition: opacity .7s ease 0s, visibility .1s ease .8s;
     display: block;
-    position: fixed; /* IE compatibility hack */
-    @supports (position: sticky) {
-        position: sticky;
-    }
-    @supports (-ms-ime-align:auto) {
-        position: fixed; /* Edge compatibility hack */
-    }
+    position: fixed;
     bottom: $gototop-bottom-position;
     right: 0;
     a {
index 38ac391..4ed5c9a 100644 (file)
@@ -424,6 +424,7 @@ body.jsenabled .questionflag input[type=checkbox] {
 #page-mod-quiz-edit ul.slots .activityinstance {
     > a {
         display: flex;
+        max-width: 100%;
         align-items: center;
         text-indent: 0;
         padding-left: 0;
index 0a03d63..e7e4ed0 100644 (file)
@@ -9663,16 +9663,8 @@ input[disabled] {
   transition: opacity .7s ease 0s, visibility .1s ease .8s;
   display: block;
   position: fixed;
-  /* IE compatibility hack */
   bottom: 50px;
   right: 0; }
-  @supports (position: sticky) {
-    #goto-top-link {
-      position: sticky; } }
-  @supports (-ms-ime-align: auto) {
-    #goto-top-link {
-      position: fixed;
-      /* Edge compatibility hack */ } }
   #goto-top-link a {
     position: absolute;
     right: 0;
@@ -13056,7 +13048,7 @@ table.calendartable caption {
 .content-bank-container.view-list .cb-btnsort.dir-desc .desc {
   display: block; }
 
-.cb-toolbar .dropdown-scrollable {
+.cb-toolbar-container .dropdown-scrollable {
   max-height: 190px;
   overflow-y: auto; }
 
@@ -15678,6 +15670,7 @@ body.jsenabled .questionflag input[type=checkbox] {
 
 #page-mod-quiz-edit ul.slots .activityinstance > a {
   display: flex;
+  max-width: 100%;
   align-items: center;
   text-indent: 0;
   padding-left: 0; }
index a4ad1f8..33a0f58 100644 (file)
     .blockcolumn,
     .region-main {
         flex: 0 0 100%;
+        max-width: 100%;
         padding: 0 1rem;
         margin-bottom: 1rem;
     }
     }
 }
 
-.block_myoverview,
-.block_recentlyaccesseditems {
-    .dashboard-card-deck {
-        .dashboard-card {
-            width: calc(33.33% - #{$card-gutter});
+@include media-breakpoint-up(sm) {
+    .block_myoverview,
+    .block_recentlyaccesseditems {
+        .dashboard-card-deck {
+            .dashboard-card {
+                width: calc(33.33% - #{$card-gutter});
+            }
         }
     }
 }
index 9212c96..97ff4c0 100644 (file)
@@ -9868,16 +9868,8 @@ input[disabled] {
   transition: opacity .7s ease 0s, visibility .1s ease .8s;
   display: block;
   position: fixed;
-  /* IE compatibility hack */
   bottom: 50px;
   right: 0; }
-  @supports (position: sticky) {
-    #goto-top-link {
-      position: sticky; } }
-  @supports (-ms-ime-align: auto) {
-    #goto-top-link {
-      position: fixed;
-      /* Edge compatibility hack */ } }
   #goto-top-link a {
     position: absolute;
     right: 0;
@@ -13271,7 +13263,7 @@ table.calendartable caption {
 .content-bank-container.view-list .cb-btnsort.dir-desc .desc {
   display: block; }
 
-.cb-toolbar .dropdown-scrollable {
+.cb-toolbar-container .dropdown-scrollable {
   max-height: 190px;
   overflow-y: auto; }
 
@@ -15901,6 +15893,7 @@ body.jsenabled .questionflag input[type=checkbox] {
 
 #page-mod-quiz-edit ul.slots .activityinstance > a {
   display: flex;
+  max-width: 100%;
   align-items: center;
   text-indent: 0;
   padding-left: 0; }
@@ -19404,6 +19397,7 @@ body {
   .blockcolumn,
   .region-main {
     flex: 0 0 100%;
+    max-width: 100%;
     padding: 0 1rem;
     margin-bottom: 1rem; } }
 
@@ -19596,9 +19590,10 @@ body {
     padding: 0 1rem 0 !important;
     /* stylelint-disable-line declaration-no-important */ } }
 
-.block_myoverview .dashboard-card-deck .dashboard-card,
-.block_recentlyaccesseditems .dashboard-card-deck .dashboard-card {
-  width: calc(33.33% - 0.5rem); }
+@media (min-width: 576px) {
+  .block_myoverview .dashboard-card-deck .dashboard-card,
+  .block_recentlyaccesseditems .dashboard-card-deck .dashboard-card {
+    width: calc(33.33% - 0.5rem); } }
 
 @media (min-width: 768px) {
   .blockcolumn .dashboard-card-deck {
index 369b91e..365548f 100644 (file)
@@ -59,11 +59,11 @@ class behat_theme_classic_behat_repository_upload extends behat_repository_uploa
                 $exception
             );
         } else {
-            // Gets the ffilemanager node specified by the locator which contains the filepicker container.
+            // Gets the filemanager node specified by the locator which contains the filepicker container.
             $filepickerelement = behat_context_helper::escape($filepickerelement);
             $filepickercontainer = $this->find(
                 'xpath',
-                "//input[./@id = //label[normalize-space(.)=$filepickerelement]/@for]" .
+                "//input[./@id = substring-before(//p[normalize-space(.)=$filepickerelement]/@id, '_label')]" .
                     "//ancestor::div[contains(concat(' ', normalize-space(@class), ' '), ' felement ')]",
                 $exception
             );
index d1a9b53..f506718 100644 (file)
@@ -29,9 +29,9 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2020052700.01;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2020060200.01;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.
-$release  = '3.9dev+ (Build: 20200527)'; // Human-friendly version name
+$release  = '3.9dev+ (Build: 20200602)'; // Human-friendly version name
 $branch   = '39';                       // This version's branch.
 $maturity = MATURITY_ALPHA;             // This version's maturity level.