Merge branch 'MDL-68977-master' of git://github.com/andrewnicols/moodle
authorJake Dallimore <jake@moodle.com>
Wed, 10 Jun 2020 07:36:44 +0000 (15:36 +0800)
committerJake Dallimore <jake@moodle.com>
Wed, 10 Jun 2020 07:36:44 +0000 (15:36 +0800)
62 files changed:
admin/tool/moodlenet/lang/en/tool_moodlenet.php
admin/tool/moodlenet/settings.php
admin/tool/task/classes/check/cronrunning.php
contentbank/amd/build/sort.min.js
contentbank/amd/build/sort.min.js.map
contentbank/amd/src/sort.js
contentbank/classes/contenttype.php
contentbank/classes/output/viewcontent.php
contentbank/edit.php
contentbank/tests/behat/edit_content.feature
course/templates/activitychooser.mustache
course/tests/caching_content_item_readonly_repository_test.php
h5p/classes/editor.php
h5p/classes/helper.php
h5p/classes/local/library/handler.php
h5p/h5plib/v124/lang/en/h5plib_v124.php
lang/en/admin.php
lang/en/contentbank.php
lang/en/files.php
lang/en/h5p.php
lang/en/install.php
lang/en/moodle.php
lib/classes/plugin_manager.php
lib/classes/userfeedback.php
lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button-debug.js
lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button-min.js
lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button.js
lib/editor/atto/plugins/image/yui/src/button/js/button.js
lib/editor/tinymce/lang/en/editor_tinymce.php
lib/outputrenderers.php
lib/templates/campaign_content.mustache
lib/templates/filemanager_selectlayout.mustache
lib/tests/behat/userfeedback.feature [new file with mode: 0644]
mod/h5pactivity/classes/external/get_h5pactivity_access_information.php
mod/h5pactivity/classes/external/h5pactivity_summary_exporter.php
mod/h5pactivity/classes/local/report/participants.php
mod/h5pactivity/lang/en/h5pactivity.php
mod/h5pactivity/lib.php
mod/h5pactivity/templates/local/result/answer.mustache [moved from mod/h5pactivity/templates/result/answer.mustache with 97% similarity]
mod/h5pactivity/templates/local/result/header.mustache [moved from mod/h5pactivity/templates/result/header.mustache with 96% similarity]
mod/h5pactivity/templates/local/result/options.mustache [moved from mod/h5pactivity/templates/result/options.mustache with 94% similarity]
mod/h5pactivity/templates/result.mustache
mod/h5pactivity/tests/external/get_h5pactivities_by_courses_test.php
mod/h5pactivity/tests/external/get_h5pactivity_access_information_test.php
question/type/calculatedmulti/styles.css
question/type/multichoice/amd/build/clearchoice.min.js
question/type/multichoice/amd/build/clearchoice.min.js.map
question/type/multichoice/amd/src/clearchoice.js
question/type/multichoice/renderer.php
question/type/multichoice/styles.css
question/type/multichoice/tests/behat/clearanswers.feature
question/type/multichoice/tests/walkthrough_test.php
repository/contentbank/tests/behat/file_update.feature [new file with mode: 0644]
repository/flickr_public/lang/en/repository_flickr_public.php
repository/merlot/lang/en/repository_merlot.php
theme/boost/scss/moodle/core.scss
theme/boost/style/moodle.css
theme/classic/scss/classic/post.scss
theme/classic/style/moodle.css
user/classes/table/participants_search.php
user/tests/behat/filter_participants.feature
version.php

index 1ba4e5b..fbe02fa 100644 (file)
@@ -31,16 +31,15 @@ $string['aria:footermessage'] = "Browse for content on MoodleNet";
 $string['browsecontentmoodlenet'] = "Or browse for content on MoodleNet";
 $string['clearsearch'] = "Clear search";
 $string['connectandbrowse'] = "Connect to and browse:";
-$string['defaultmoodlenet'] = "Default MoodleNet URL";
+$string['defaultmoodlenet'] = 'MoodleNet URL';
 $string['defaultmoodlenet_desc'] = "The URL to either Moodle HQ's MoodleNet instance, or your preferred instance.";
 $string['defaultmoodlenetname'] = "MoodleNet instance name";
-$string['defaultmoodlenetname_desc'] = 'The name of either Moodle HQ\'s MoodleNet instance or your preferred MoodleNet instance to browse on.';
+$string['defaultmoodlenetnamevalue'] = 'MoodleNet Home';
+$string['defaultmoodlenetname_desc'] = 'The name of the MoodleNet instance available via the activity chooser.';
 $string['enablemoodlenet'] = 'Enable MoodleNet integration';
-$string['enablemoodlenet_desc'] = 'Enabling the integration allows users with the \'xx\' capability to browse MoodleNet from the
-activity chooser and import MoodleNet resources into their course. It also allows users to push backups from MoodleNet into Moodle.
-';
+$string['enablemoodlenet_desc'] = 'If enabled, a user with the capability to create and manage activities can browse MoodleNet via the activity chooser and import MoodleNet resources into their course. In addition, a user with the capability to restore backups can select a backup file on MoodleNet and restore it into Moodle.';
 $string['errorduringdownload'] = 'An error occurred while downloading the file: {$a}';
-$string['forminfo'] = "It will be automatically saved on your moodle profile.";
+$string['forminfo'] = 'Your MoodleNet profile will be automatically saved in your profile on this site.';
 $string['footermessage'] = "Or browse for content on";
 $string['instancedescription'] = "MoodleNet is an open social media platform for educators, with a focus on the collaborative curation of collections of open resources. ";
 $string['instanceplaceholder'] = '@yourprofile@moodle.net';
@@ -48,15 +47,14 @@ $string['inputhelp'] = 'Or if you have a MoodleNet account already, enter your M
 $string['invalidmoodlenetprofile'] = '$userprofile is not correctly formatted';
 $string['importconfirm'] = 'You are about to import the content "{$a->resourcename} ({$a->resourcetype})" into the course "{$a->coursename}". Are you sure you want to continue?';
 $string['importconfirmnocourse'] = 'You are about to import the content "{$a->resourcename} ({$a->resourcetype})" into your site. Are you sure you want to continue?';
-$string['importformatselectguidingtext'] = 'In which format would you like this content "{$a->name} ({$a->type})" to be added to your course?';
+$string['importformatselectguidingtext'] = 'In which format would you like the content "{$a->name} ({$a->type})" to be added to your course?';
 $string['importformatselectheader'] = 'Choose the content display format';
 $string['missinginvalidpostdata'] = 'The resource information from MoodleNet is either missing, or is in an incorrect format.
 If this happens repeatedly, please contact the site administrator.';
 $string['mnetprofile'] = 'MoodleNet profile';
-$string['mnetprofiledesc'] = '<p>Enter in your MoodleNet profile details here to be redirected to your profile while visiting MoodleNet.</p>';
+$string['mnetprofiledesc'] = '<p>Enter your MoodleNet profile details here to be redirected to your profile while visiting MoodleNet.</p>';
 $string['moodlenetsettings'] = 'MoodleNet settings';
-$string['moodlenetnotenabled'] = 'The MoodleNet integration must be enabled before resource imports can be processed.
-To enable this feature, see the \'enablemoodlenet\' setting.';
+$string['moodlenetnotenabled'] = 'The MoodleNet integration must be enabled in Site administration / MoodleNet before resource imports can be processed.';
 $string['notification'] = 'You are about to import the content "{$a->name} ({$a->type})" into your site. Select the course in which it should be added, or <a href="{$a->cancellink}">cancel</a>.';
 $string['searchcourses'] = "Search courses";
 $string['selectpagetitle'] = 'Select page';
index 4b6fb4f..ec1cc55 100644 (file)
@@ -37,7 +37,7 @@ if ($hassiteconfig) {
 
     $temp = new admin_setting_configtext('tool_moodlenet/defaultmoodlenetname',
         get_string('defaultmoodlenetname', 'tool_moodlenet'), new lang_string('defaultmoodlenetname_desc', 'tool_moodlenet'),
-        'Moodle HQ MoodleNet');
+        new lang_string('defaultmoodlenetnamevalue', 'tool_moodlenet'));
     $settings->add($temp);
 
     $temp = new admin_setting_configtext('tool_moodlenet/defaultmoodlenet', get_string('defaultmoodlenet', 'tool_moodlenet'),
index 46aa690..2efcee1 100644 (file)
@@ -81,9 +81,17 @@ class cronrunning extends check {
             }
 
             if (empty($lastcron)) {
-                $summary = get_string('cronwarningnever', 'admin', [
-                    'expected' => $formatexpected,
-                ]);
+                if (empty($CFG->cronclionly)) {
+                    $url = new \moodle_url('/admin/cron.php');
+                    $summary = get_string('cronwarningneverweb', 'admin', [
+                        'url' => $url->out(),
+                        'expected' => $formatexpected,
+                    ]);
+                } else {
+                    $summary = get_string('cronwarningnever', 'admin', [
+                        'expected' => $formatexpected,
+                    ]);
+                }
             } else if (empty($CFG->cronclionly)) {
                 $url = new \moodle_url('/admin/cron.php');
                 $summary = get_string('cronwarning', 'admin', [
index bd655b7..d8222ca 100644 (file)
Binary files a/contentbank/amd/build/sort.min.js and b/contentbank/amd/build/sort.min.js differ
index 663a438..dae9e77 100644 (file)
Binary files a/contentbank/amd/build/sort.min.js.map and b/contentbank/amd/build/sort.min.js.map differ
index 24f4f79..94c1a24 100644 (file)
@@ -35,8 +35,8 @@ import Notification from 'core/notification';
  */
 export const init = () => {
     const contentBank = document.querySelector(selectors.regions.contentbank);
-    Prefetch.prefetchStrings('contentbank', ['sortbyx', 'sortbyxreverse', 'contentname',
-        'lastmodified', 'size', 'type']);
+    Prefetch.prefetchStrings('contentbank', ['contentname', 'lastmodified', 'size', 'type']);
+    Prefetch.prefetchStrings('moodle', ['sortbyx', 'sortbyxreverse']);
     registerListenerEvents(contentBank);
 };
 
index ba9442b..6b6a140 100644 (file)
@@ -330,10 +330,13 @@ abstract class contenttype {
 
     /**
      * Returns whether or not the user has permission to use the editor.
+     * This function will be called with the content to be edited as parameter,
+     * or null when is checking permission to create a new content using the editor.
      *
+     * @param  content $content The content to be edited or null when creating a new content.
      * @return bool     True if the user can edit content. False otherwise.
      */
-    final public function can_edit(): bool {
+    final public function can_edit(?content $content = null): bool {
         if (!$this->is_feature_supported(self::CAN_EDIT)) {
             return false;
         }
@@ -342,19 +345,24 @@ abstract class contenttype {
             return false;
         }
 
+        if (!is_null($content) && !$this->can_manage($content)) {
+            return false;
+        }
+
         $classname = 'contenttype/'.$this->get_plugin_name();
 
         $editioncap = $classname.':useeditor';
         $hascapabilities = has_all_capabilities(['moodle/contentbank:useeditor', $editioncap], $this->context);
-        return $hascapabilities && $this->is_edit_allowed();
+        return $hascapabilities && $this->is_edit_allowed($content);
     }
 
     /**
      * Returns plugin allows edition.
      *
+     * @param  content $content The content to be edited.
      * @return bool     True if plugin allows edition. False otherwise.
      */
-    protected function is_edit_allowed(): bool {
+    protected function is_edit_allowed(?content $content): bool {
         // Plugins can overwrite this function to add any check they need.
         return true;
     }
index efb403e..48871a6 100644 (file)
@@ -75,7 +75,7 @@ class viewcontent implements renderable, templatable {
         $data->contenthtml = $contenthtml;
 
         // Check if the user can edit this content type.
-        if ($this->contenttype->can_edit()) {
+        if ($this->contenttype->can_edit($this->content)) {
             $data->usercanedit = true;
             $urlparams = [
                 'contextid' => $this->content->get_contextid(),
index 832f1c2..cdddcd4 100644 (file)
@@ -45,6 +45,7 @@ if (!empty($id)) {
 } else {
     $contenttypename = "contenttype_$pluginname";
     $heading = get_string('addinganew', 'moodle', get_string('description', $contenttypename));
+    $content = null;
 }
 
 // Check plugin is enabled.
@@ -61,9 +62,9 @@ if (class_exists($contenttypeclass)) {
     print_error('unsupported', 'core_contentbank', $returnurl);
 }
 
-// Checks the user can edit this content type.
-if (!$contenttype->can_edit()) {
-    print_error('contenttypenoedit', 'core_contentbank', $returnurl, $contenttype->get_plugin_name());
+// Checks the user can edit this content and content type.
+if (!$contenttype->can_edit($content)) {
+    print_error('contenttypenoedit', 'core_contentbank', $returnurl);
 }
 
 $values = [
index aef6eab..29108c7 100644 (file)
@@ -115,3 +115,47 @@ Feature: Content bank use editor feature
     And I click on "Edit" "link"
     And I switch to "h5p-editor-iframe" class iframe
     Then the field "Title" matches value "New title"
+
+  Scenario: Teachers can edit their own content in the content bank
+    Given the following "users" exist:
+      | username | firstname | lastname | email                |
+      | teacher1 | Teacher   | 1        | teacher1@example.com |
+    And the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1        | 0        |
+    And the following "course enrolments" exist:
+      | user     | course | role           |
+      | teacher1 | C1     | editingteacher |
+    And the following "contentbank content" exist:
+      | contextlevel | reference | contenttype     | user     | contentname       | filepath                              |
+      | Course       | C1        | contenttype_h5p | admin    | filltheblanks.h5p | /h5p/tests/fixtures/filltheblanks.h5p |
+      | Course       | C1        | contenttype_h5p | teacher1 | ipsums.h5p        | /h5p/tests/fixtures/ipsums.h5p        |
+    When I log out
+    And I log in as "teacher1"
+    And I am on "Course 1" course homepage
+    And I expand "Site pages" node
+    And I click on "Content bank" "link"
+    And I follow "ipsums.h5p"
+    Then "Edit" "link" should exist in the "region-main" "region"
+
+  Scenario: Teachers can't edit content created by other users in the content bank
+    Given the following "users" exist:
+      | username | firstname | lastname | email                |
+      | teacher1 | Teacher   | 1        | teacher1@example.com |
+    And the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1        | 0        |
+    And the following "course enrolments" exist:
+      | user     | course | role           |
+      | teacher1 | C1     | editingteacher |
+    And the following "contentbank content" exist:
+      | contextlevel | reference | contenttype     | user     | contentname       | filepath                              |
+      | Course       | C1        | contenttype_h5p | admin    | filltheblanks.h5p | /h5p/tests/fixtures/filltheblanks.h5p |
+      | Course       | C1        | contenttype_h5p | teacher1 | ipsums.h5p        | /h5p/tests/fixtures/ipsums.h5p        |
+    When I log out
+    And I log in as "teacher1"
+    And I am on "Course 1" course homepage
+    And I expand "Site pages" node
+    And I click on "Content bank" "link"
+    And I follow "filltheblanks.h5p"
+    Then "Edit" "link" should not exist in the "region-main" "region"
index bf6ba5e..b143d52 100644 (file)
@@ -41,7 +41,7 @@
                     {{>core_course/local/activitychooser/search}}
                 </div>
                 <div data-region="chooser-container" class="chooser-container">
-                    <div class="nav nav-tabs z-index-1" id="activities-{{uniqid}}" role="tablist">
+                    <div class="nav nav-tabs flex-shrink-0 z-index-1" id="activities-{{uniqid}}" role="tablist">
                         <a class="nav-item nav-link {{#favouritesFirst}}active{{/favouritesFirst}} {{^favourites}}d-none{{/favourites}}"
                            id="starred-tab-{{uniqid}}"
                            data-toggle="tab"
index 6ad37f1..aacdf54 100644 (file)
@@ -51,15 +51,15 @@ class caching_content_item_readonly_repository_testcase extends \advanced_testca
         // Get the content items using both the live and the caching repos.
         $items = $cir->find_all_for_course($course, $user);
         $cacheditems = $ccir->find_all_for_course($course, $user);
-        $itemsfiltered = array_filter($items, function($item) {
-            return $item->get_component_name() == 'mod_assign';
-        });
-        $cacheditemsfiltered = array_filter($cacheditems, function($item) {
-            return $item->get_component_name() == 'mod_assign';
-        });
+        $itemsfiltered = array_values(array_filter($items, function($item) {
+            return $item->get_component_name() == 'mod_book';
+        }));
+        $cacheditemsfiltered = array_values(array_filter($cacheditems, function($item) {
+            return $item->get_component_name() == 'mod_book';
+        }));
 
-        // Verify the assign module is in both result sets.
-        $module = $DB->get_record('modules', ['name' => 'assign']);
+        // Verify the book module is in both result sets.
+        $module = $DB->get_record('modules', ['name' => 'book']);
         $this->assertEquals($module->name, $itemsfiltered[0]->get_name());
         $this->assertEquals($module->name, $cacheditemsfiltered[0]->get_name());
 
@@ -67,12 +67,12 @@ class caching_content_item_readonly_repository_testcase extends \advanced_testca
         $DB->set_field("modules", "visible", "0", ["id" => $module->id]);
         $items = $cir->find_all_for_course($course, $user);
         $cacheditems = $ccir->find_all_for_course($course, $user);
-        $itemsfiltered = array_filter($items, function($item) {
-            return $item->get_component_name() == 'mod_assign';
-        });
-        $cacheditemsfiltered = array_filter($cacheditems, function($item) {
-            return $item->get_component_name() == 'mod_assign';
-        });
+        $itemsfiltered = array_values(array_filter($items, function($item) {
+            return $item->get_component_name() == 'mod_book';
+        }));
+        $cacheditemsfiltered = array_values(array_filter($cacheditems, function($item) {
+            return $item->get_component_name() == 'mod_book';
+        }));
 
         // The caching repo should return the same list, while the live repo will return the updated list.
         $this->assertEquals($module->name, $cacheditemsfiltered[0]->get_name());
index 8faa32f..a4b4af7 100644 (file)
@@ -228,10 +228,6 @@ class editor {
             throw new coding_exception('Missing H5P library.');
         }
 
-        if ($content->h5plibrary != $this->library) {
-            throw new coding_exception("Wrong H5P library.");
-        }
-
         $content->params = $content->h5pparams;
 
         if (!empty($this->oldcontent)) {
@@ -310,15 +306,16 @@ class editor {
         if ($file) {
             $fields['contenthash'] = $file->get_contenthash();
 
-            // Delete old file if any.
-            if (!empty($this->oldfile)) {
-                $this->oldfile->delete();
-            }
-            // Create new file.
+            // Create or update H5P file.
             if (empty($this->filearea['filename'])) {
                 $this->filearea['filename'] = $contentarray['slug'] . '.h5p';
             }
-            $newfile = $fs->create_file_from_storedfile($this->filearea, $file);
+            if (!empty($this->oldfile)) {
+                $this->oldfile->replace_file_with($file);
+                $newfile = $this->oldfile;
+            } else {
+                $newfile = $fs->create_file_from_storedfile($this->filearea, $file);
+            }
             if (empty($this->oldcontent)) {
                 $pathnamehash = $newfile->get_pathnamehash();
             } else {
index 721187e..2cf6598 100644 (file)
@@ -359,7 +359,7 @@ class helper {
             'crossorigin' => null,
             'libraryConfig' => $core->h5pF->getLibraryConfig(),
             'pluginCacheBuster' => self::get_cache_buster(),
-            'libraryUrl' => autoloader::get_h5p_core_library_url('core/js')
+            'libraryUrl' => autoloader::get_h5p_core_library_url('js')->out(),
         );
 
         return $settings;
index c9c5389..6108969 100644 (file)
@@ -134,6 +134,10 @@ abstract class handler {
         $value = null;
         $h5pversion = static::get_h5p_version();
         $component = 'h5plib_v' . $h5pversion;
+        // Composed code languages, such as 'Spanish, Mexican' are different in H5P and Moodle:
+        // - In H5P, they use '-' to separate language from the country. For instance: es-mx.
+        // - However, in Moodle, they have '_' instead of '-'. For instance: es_mx.
+        $language = str_replace('-', '_', $language);
         if (get_string_manager()->string_exists($identifier, $component)) {
             $defaultmoodlelang = 'en';
             // In Moodle, all the English strings always will exist because they have to be declared in order to let users
index b8f4222..35e5ee3 100644 (file)
@@ -39,7 +39,7 @@ $string['editor:cancellabel'] = 'Cancel';
 $string['editor:changefile'] = 'Change file';
 $string['editor:changelanguage'] = 'Change language to :language?';
 $string['editor:changelibrary'] = 'Change content type?';
-$string['editor:changelogdescription'] = 'Some licenses require that changes made to the original work, or derivatives are logged and displayed. You may log your changes here for licensing reasons or just to allow yourself and others to keep track of the changes made to this content.';
+$string['editor:changelogdescription'] = 'Some licences require that changes made to the original work, or derivatives are logged and displayed. You may log your changes here for licensing reasons or just to allow yourself and others to keep track of the changes made to this content.';
 $string['editor:close'] = 'Close';
 $string['editor:commonfields'] = 'Text overrides and translations';
 $string['editor:commonfieldsdescription'] = 'Here you can edit settings or translate texts used in this content.';
@@ -61,7 +61,7 @@ $string['editor:contenttypeinstallbuttonlabel'] = 'Install';
 $string['editor:contenttypeinstallerror'] = ':contentType could not be installed. Contact your administrator.';
 $string['editor:contenttypeinstallsuccess'] = ':contentType successfully installed!';
 $string['editor:contenttypeinstallingbuttonlabel'] = 'Installing';
-$string['editor:contenttypelicensepaneltitle'] = 'License';
+$string['editor:contenttypelicensepaneltitle'] = 'Licence';
 $string['editor:contenttypenotinstalled'] = 'Content type not installed';
 $string['editor:contenttypenotinstalleddesc'] = 'You do not have permission to install content types.';
 $string['editor:contenttypeowner'] = 'By :owner';
index a04da00..736516d 100644 (file)
@@ -433,6 +433,7 @@ $string['cronremotepassword'] = 'Cron password for remote access';
 $string['cronwarning'] = 'The <a href="{$a->url}">admin/cron.php script</a> has not been run for {$a->actual} and should run every {$a->expected}.';
 $string['cronwarningcli'] = 'The <code>admin/cli/cron.php</code> script has not been run for {$a->actual} and should run every {$a->expected}.';
 $string['cronwarningnever'] = 'The <code>admin/cli/cron.php</code> script has never been run and should run every {$a->expected}.';
+$string['cronwarningneverweb'] = 'The <a href="{$a->url}">admin/cron.php script</a> has never been run and should run every {$a->expected}.';
 $string['ctyperequired'] = 'The ctype PHP extension is now required by Moodle, in order to improve site performance and to offer multilingual compatibility.';
 $string['curlsecurityallowedport'] = 'cURL allowed ports list';
 $string['curlsecurityallowedportsyntax'] = 'List of port numbers that cURL can connect to. Valid entries are integer numbers only. Put each entry on a new line. If left empty, then all ports are allowed. If set, in almost all cases, both 443 and 80 should be specified for cURL to connect to standard HTTPS and HTTP ports.';
@@ -544,8 +545,8 @@ $string['enablerssfeeds'] = 'Enable RSS feeds';
 $string['enablesearchareas'] = 'Enable search areas';
 $string['enablestats'] = 'Enable statistics';
 $string['enabletrusttext'] = 'Enable trusted content';
-$string['enableuserfeedback'] = 'Enable feedback about Moodle';
-$string['enableuserfeedback_desc'] = 'If enabled, a \'Give feedback\' link is displayed in a Dashboard alert and in the footer for users to give feedback about the Moodle LMS to Moodle HQ. The Dashboard alert also has a \'Remind me later\' option.';
+$string['enableuserfeedback'] = 'Enable feedback about this software';
+$string['enableuserfeedback_desc'] = 'If enabled, a \'Give feedback about this software\' link is displayed in a Dashboard alert and in the footer for users to give feedback about Moodle to Moodle HQ. The Dashboard alert also has a \'Remind me later\' option.';
 $string['enablewebservices'] = 'Enable web services';
 $string['enablewsdocumentation'] = 'Web services documentation';
 $string['enrolinstancedefaults'] = 'Enrolment instance defaults';
index 6e9176d..7c63e1e 100644 (file)
@@ -32,7 +32,7 @@ $string['contentnotrenamed'] = 'An error was encountered while trying to rename
 $string['contentrenamed'] = 'The content has been renamed.';
 $string['contentsmoved'] = 'Content bank contents moved to {$a}.';
 $string['contenttypenoaccess'] = 'You cannot view this {$a} instance.';
-$string['contenttypenoedit'] = 'You cannot edit contents of the {$a} content type.';
+$string['contenttypenoedit'] = 'You can not edit this content';
 $string['eventcontentcreated'] = 'Content created';
 $string['eventcontentdeleted'] = 'Content deleted';
 $string['eventcontentupdated'] = 'Content updated';
index b2e305b..8702327 100644 (file)
@@ -31,7 +31,7 @@ $string['privacy:metadata:files:contenthash'] = 'A hash of the file\'s content';
 $string['privacy:metadata:files:filename'] = 'The name of the file in its file area';
 $string['privacy:metadata:files:filepath'] = 'The path to the file in its file area';
 $string['privacy:metadata:files:filesize'] = 'The size of the file';
-$string['privacy:metadata:files:license'] = 'The license of the file\'s content';
+$string['privacy:metadata:files:license'] = 'The licence of the file\'s content';
 $string['privacy:metadata:files:mimetype'] = 'The MIME type of the file';
 $string['privacy:metadata:files:source'] = 'The source of the file';
 $string['privacy:metadata:files:timecreated'] = 'The time when the file was created';
index d640425..f2d73f2 100644 (file)
@@ -30,7 +30,7 @@ $string['addedandupdatedsp'] = 'Added {$a->%new} new H5P library and updated {$a
 $string['addedandupdatedss'] = 'Added {$a->%new} new H5P library and updated {$a->%old} old one.';
 $string['addednewlibraries'] = 'Added {$a->%new} new H5P libraries.';
 $string['addednewlibrary'] = 'Added {$a->%new} new H5P library.';
-$string['additionallicenseinfo'] = 'Any additional information about the license';
+$string['additionallicenseinfo'] = 'Any additional information about the licence';
 $string['atto_h5p'] = 'Insert H5P button';
 $string['atto_h5p_description'] = 'The Insert H5P button in the Atto editor enables users to insert H5P content by either entering a URL or embed code, or by uploading an H5P file.';
 $string['author'] = 'Author';
@@ -122,7 +122,7 @@ $string['invalidsemanticstype'] = 'H5P internal error: unknown content type "{$a
 $string['invalidstring'] = 'Provided string is not valid according to regexp in semantics. (value: "{$a->%value}", regexp: "{$a->%regexp}")';
 $string['librarydirectoryerror'] = 'Library directory name must match machineName or machineName-majorVersion.minorVersion (from library.json). (Directory: {$a->%directoryName} , machineName: {$a->%machineName}, majorVersion: {$a->%majorVersion}, minorVersion: {$a->%minorVersion})';
 $string['librariesmanagerdescription'] = '<p>H5P enables users to create interactive content by providing a range of content types.</p><p>To ensure that only trusted H5P content types are used on your site, you need to <i>either</i></p><ul><li>Upload H5P content types from h5p.org <i>or</i></li><li>Enable the scheduled task \'Download available H5P content types from h5p.org\'</li></ul><p>Note that users will only be able to use the H5P content types which are installed on your site.</p>';
-$string['license'] = 'License';
+$string['license'] = 'Licence';
 $string['licenseCC010'] = 'CC0 1.0 Universal (CC0 1.0) Public Domain Dedication';
 $string['licenseCC010U'] = 'CC0 1.0 Universal';
 $string['licenseCC10'] = '1.0 Generic';
@@ -135,8 +135,8 @@ $string['licenseV1'] = 'Version 1';
 $string['licenseV2'] = 'Version 2';
 $string['licenseV3'] = 'Version 3';
 $string['licensee'] = 'Licensee';
-$string['licenseextras'] = 'License extras';
-$string['licenseversion'] = 'License version';
+$string['licenseextras'] = 'Licence extras';
+$string['licenseversion'] = 'Licence version';
 $string['lockh5pdeploy'] = 'This H5P content cannot be accessed because it is being deployed. Please try again later.';
 $string['missingcontentfolder'] = 'A valid content folder is missing';
 $string['missingcoreversion'] = 'The system was unable to install the {$a->%component} component from the package, as it requires a newer version of the H5P plugin. This site is currently running version {$a->%current}, whereas the required version is {$a->%required} or higher. Please upgrade and then try again.';
index a077113..047c1cb 100644 (file)
@@ -46,7 +46,7 @@ $string['clialreadyconfigured'] = 'The configuration file config.php already exi
 $string['clialreadyinstalled'] = 'The configuration file config.php already exists. Please use admin/cli/install_database.php to upgrade Moodle for this site.';
 $string['cliinstallfinished'] = 'Installation completed successfully.';
 $string['cliinstallheader'] = 'Moodle {$a} command line installation program';
-$string['climustagreelicense'] = 'In non interactive mode you must agree to license by specifying --agree-license option';
+$string['climustagreelicense'] = 'In non-interactive mode you must agree to the licence by specifying --agree-license option';
 $string['cliskipdatabase'] = 'Skipping database installation.';
 $string['clitablesexist'] = 'Database tables already present; CLI installation cannot continue.';
 $string['compatibilitysettings'] = 'Checking your PHP settings ...';
index acf4969..7672db6 100644 (file)
@@ -225,8 +225,8 @@ $string['bycourseorder'] = 'By course order';
 $string['byname'] = 'by {$a}';
 $string['bypassed'] = 'Bypassed';
 $string['cachecontrols'] = 'Cache controls';
-$string['calltofeedback'] = 'Moodle HQ would like your feedback on the Moodle LMS.';
-$string['calltofeedback_give'] = 'Give feedback';
+$string['calltofeedback'] = 'The creators of this software would like your feedback.';
+$string['calltofeedback_give'] = 'Give feedback about this software';
 $string['calltofeedback_remind'] = 'Remind me later';
 $string['cancel'] = 'Cancel';
 $string['cancelled'] = 'Cancelled';
index 0d8d832..bb7f39d 100644 (file)
@@ -2000,7 +2000,7 @@ class core_plugin_manager {
                 'analytics', 'availabilityconditions', 'behat', 'capability', 'cohortroles', 'customlang',
                 'dataprivacy', 'dbtransfer', 'filetypes', 'generator', 'health', 'httpsreplace', 'innodb',
                 'installaddon', 'langimport', 'licensemanager', 'log', 'lp', 'lpimportcsv', 'lpmigrate', 'messageinbound',
-                'mobile', 'multilangupgrade', 'monitor', 'oauth2', 'phpunit', 'policy', 'profiling', 'recyclebin',
+                'mobile', 'moodlenet', 'multilangupgrade', 'monitor', 'oauth2', 'phpunit', 'policy', 'profiling', 'recyclebin',
                 'replace', 'spamcleaner', 'task', 'templatelibrary', 'uploadcourse', 'uploaduser', 'unsuproles',
                 'usertours', 'xmldb'
             ),
index d74f621..8128336 100644 (file)
@@ -96,7 +96,7 @@ class core_userfeedback {
     public static function should_display_reminder(): bool {
         global $CFG;
 
-        if ($CFG->enableuserfeedback && isloggedin() && !isguestuser()) {
+        if (static::can_give_feedback()) {
             $give = get_user_preferences('core_userfeedback_give');
             $remind = get_user_preferences('core_userfeedback_remind');
 
@@ -145,6 +145,17 @@ class core_userfeedback {
         return $url;
     }
 
+    /**
+     * Whether the current can give feedback.
+     *
+     * @return bool
+     */
+    public static function can_give_feedback(): bool {
+        global $CFG;
+
+        return $CFG->enableuserfeedback && isloggedin() && !isguestuser();
+    }
+
     /**
      * Returns the last major upgrade time
      *
index 75f44a5..b8c15f2 100644 (file)
Binary files a/lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button-debug.js and b/lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button-debug.js differ
index 939f2d7..a65bd18 100644 (file)
Binary files a/lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button-min.js and b/lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button-min.js differ
index 4bb3907..07a268a 100644 (file)
Binary files a/lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button.js and b/lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button.js differ
index 774d8b1..75f0ec4 100644 (file)
@@ -180,7 +180,7 @@ var CSS = {
                 // Add the image preview.
                 '<div class="mdl-align">' +
                 '<div class="{{CSS.IMAGEPREVIEWBOX}}">' +
-                    '<img src="#" class="{{CSS.IMAGEPREVIEW}}" alt="" style="display: none;"/>' +
+                    '<img class="{{CSS.IMAGEPREVIEW}}" alt="" style="display: none;"/>' +
                 '</div>' +
 
                 // Add the submit button and close the form.
index 8008f97..3298ea4 100644 (file)
@@ -118,7 +118,7 @@ $string['advanced_dlg:'] = '';
 $string['advanced_dlg:about_author'] = 'Author';
 $string['advanced_dlg:about_general'] = 'About';
 $string['advanced_dlg:about_help'] = 'Help';
-$string['advanced_dlg:about_license'] = 'License';
+$string['advanced_dlg:about_license'] = 'Licence';
 $string['advanced_dlg:about_loaded'] = 'Loaded plugins';
 $string['advanced_dlg:about_plugin'] = 'Plugin';
 $string['advanced_dlg:about_plugins'] = 'Plugins';
index 240d449..8802a9e 100644 (file)
@@ -835,7 +835,7 @@ class core_renderer extends renderer_base {
             }
         }
 
-        if (isloggedin() && !isguestuser()) {
+        if (core_userfeedback::can_give_feedback()) {
             $output .= html_writer::div(
                 $this->render_from_template('core/userfeedback_footer_link', ['url' => core_userfeedback::make_link()->out(false)])
             );
index dbba382..1e7848e 100644 (file)
@@ -33,8 +33,7 @@
     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>
+<div class="alert alert-secondary alert-block fade in">
     <iframe id="campaign-content" class="w-100 border-0"></iframe>
 </div>
 {{#js}}
index 974c5be..d97b92e 100644 (file)
@@ -29,7 +29,7 @@
     </div>
     <div class="container">
         <form>
-            <fieldset class="form-group row">
+            <fieldset class="form-group row flex-column">
                 <div class="form-check fp-linktype-2">
                     <label class="form-check-label">
                         <input class="form-check-input" type="radio">
diff --git a/lib/tests/behat/userfeedback.feature b/lib/tests/behat/userfeedback.feature
new file mode 100644 (file)
index 0000000..b090f80
--- /dev/null
@@ -0,0 +1,36 @@
+@core
+Feature: Gathering user feedback
+  In order to facilitate data collection from as broad a sample of Moodle users as possible
+  As Moodle HQ
+  We should add a link within Moodle to a permanent URL on which surveys will be placed
+
+  Scenario: Users should see a feedback link on footer when the feature is enabled
+    Given the following config values are set as admin:
+      | enableuserfeedback  | 1  |
+    When I log in as "admin"
+    Then I should see "Give feedback" in the "page-footer" "region"
+
+  Scenario: Users should not see a feedback link on footer when the feature is disabled
+    Given the following config values are set as admin:
+      | enableuserfeedback  | 0  |
+    When I log in as "admin"
+    Then I should not see "Give feedback" in the "page-footer" "region"
+
+  Scenario: Visitors should not see a feedback link on footer when they are not logged in
+    Given the following config values are set as admin:
+      | enableuserfeedback  | 1  |
+    When I am on site homepage
+    Then I should not see "Give feedback" in the "page-footer" "region"
+
+  @javascript
+  Scenario: Users should not see the notification after they click on the remind me later link
+    Given the following config values are set as admin:
+      | enableuserfeedback        | 1   |
+      | userfeedback_nextreminder | 2   |
+      | userfeedback_remindafter  | 90  |
+    When I log in as "admin"
+    And I follow "Dashboard" in the user menu
+    And I click on "Remind me later" "link"
+    And I reload the page
+    Then I should not see "Give feedback" in the "region-main" "region"
+    And I should not see "Remind me later" in the "region-main" "region"
index ff4ae4b..20e8012 100644 (file)
@@ -35,6 +35,7 @@ use external_value;
 use external_single_structure;
 use external_warnings;
 use context_module;
+use mod_h5pactivity\local\manager;
 
 /**
  * This is the external method for getting access information for a h5p activity.
@@ -81,10 +82,16 @@ class get_h5pactivity_access_information extends external_api {
 
         $result = [];
         // Return all the available capabilities.
+        $manager = manager::create_from_coursemodule($cm);
         $capabilities = load_capability_def('mod_h5pactivity');
         foreach ($capabilities as $capname => $capdata) {
             $field = 'can' . str_replace('mod/h5pactivity:', '', $capname);
-            $result[$field] = has_capability($capname, $context);
+            // For mod/h5pactivity:submit we need to check if tracking is enabled in the h5pactivity for the current user.
+            if ($field == 'cansubmit') {
+                $result[$field] = $manager->is_tracking_enabled();
+            } else {
+                $result[$field] = has_capability($capname, $context);
+            }
         }
 
         $result['warnings'] = [];
index 26145ad..493fa1f 100644 (file)
@@ -132,6 +132,9 @@ class h5pactivity_summary_exporter extends exporter {
             'coursemodule' => [
                 'type' => PARAM_INT
             ],
+            'context' => [
+                'type' => PARAM_INT
+            ],
             'introfiles' => [
                 'type' => external_files::get_properties_for_exporter(),
                 'multiple' => true
@@ -197,6 +200,7 @@ class h5pactivity_summary_exporter extends exporter {
 
         $values = [
             'coursemodule' => $context->instanceid,
+            'context' => $context->id,
         ];
 
         $values['introfiles'] = external_util::get_area_files($context->id, 'mod_h5pactivity', 'intro', false, false);
index 973b422..5e4765e 100644 (file)
@@ -85,7 +85,8 @@ class participants extends table_sql implements report {
         // Set query SQL.
         $capjoin = get_enrolled_with_capabilities_join($this->manager->get_context(), '', 'mod/h5pactivity:submit');
         $this->set_sql(
-            'u.*',
+            'DISTINCT u.id, u.picture, u.firstname, u.lastname, u.firstnamephonetic, u.lastnamephonetic,
+            u.middlename, u.alternatename, u.imagealt, u.email',
             "{user} u $capjoin->joins",
             $capjoin->wheres,
             $capjoin->params);
index 6d48fa0..97d4bf2 100644 (file)
@@ -53,14 +53,14 @@ $string['attempts_report'] = 'Attempts report';
 $string['attempts_none'] = 'This user has no attempts to display.';
 $string['choice'] = 'Choice';
 $string['completion'] = 'Completion';
-$string['contentbank'] = 'more information about the content bank';
-$string['contentbank_help'] = 'Within the content bank you can create and store contents using several authoring
-    tools which includes an integrated H5P packacge creator.';
+$string['contentbank'] = 'More information about the content bank';
+$string['contentbank_help'] = 'In the content bank you can create and store content using several authoring tools, including an integrated H5P creator.';
 $string['correct_answer'] = 'Correct answer';
 $string['deleteallattempts'] = 'Delete all H5P attempts';
 $string['displayexport'] = 'Allow download';
 $string['displayembed'] = 'Embed button';
 $string['displaycopyright'] = 'Copyright button';
+$string['dnduploadh5pactivity'] = 'Add an H5P activity';
 $string['duration'] = 'Duration';
 $string['enabletracking'] = 'Enable attempt tracking';
 $string['false'] = 'False';
index 36d0515..1b66faf 100644 (file)
@@ -487,3 +487,57 @@ function h5pactivity_set_mainfile(stdClass $data): void {
             0, ['subdirs' => 0, 'maxfiles' => 1]);
     }
 }
+
+/**
+ * Register the ability to handle drag and drop file uploads
+ * @return array containing details of the files / types the mod can handle
+ */
+function h5pactivity_dndupload_register(): array {
+    return [
+        'files' => [
+            [
+                'extension' => 'h5p',
+                'message' => get_string('dnduploadh5pactivity', 'h5pactivity')
+            ]
+        ]
+    ];
+}
+
+/**
+ * Handle a file that has been uploaded
+ * @param object $uploadinfo details of the file / content that has been uploaded
+ * @return int instance id of the newly created mod
+ */
+function h5pactivity_dndupload_handle($uploadinfo): int {
+    global $CFG;
+
+    $context = context_module::instance($uploadinfo->coursemodule);
+    file_save_draft_area_files($uploadinfo->draftitemid, $context->id, 'mod_h5pactivity', 'package', 0);
+    $fs = get_file_storage();
+    $files = $fs->get_area_files($context->id, 'mod_h5pactivity', 'package', 0, 'sortorder, itemid, filepath, filename', false);
+    $file = reset($files);
+
+    // Create a default h5pactivity object to pass to h5pactivity_add_instance()!
+    $h5p = get_config('h5pactivity');
+    $h5p->intro = '';
+    $h5p->introformat = FORMAT_HTML;
+    $h5p->course = $uploadinfo->course->id;
+    $h5p->coursemodule = $uploadinfo->coursemodule;
+    $h5p->grade = $CFG->gradepointdefault;
+
+    // Add some special handling for the H5P options checkboxes.
+    $factory = new \core_h5p\factory();
+    $core = $factory->get_core();
+    if (isset($uploadinfo->displayopt)) {
+        $config = (object) $uploadinfo->displayopt;
+    } else {
+        $config = \core_h5p\helper::decode_display_options($core);
+    }
+    $h5p->displayoptions = \core_h5p\helper::get_display_options($core, $config);
+
+    $h5p->cmidnumber = '';
+    $h5p->name = $uploadinfo->displayname;
+    $h5p->reference = $file->get_filename();
+
+    return h5pactivity_add_instance($h5p, null);
+}
@@ -15,7 +15,7 @@
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
-    @template mod_h5pactivity/result/answer
+    @template mod_h5pactivity/local/result/answer
 
     This template render all kind of answers/choice in a results table.
 
@@ -15,7 +15,7 @@
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
-    @template mod_h5pactivity/result/header
+    @template mod_h5pactivity/local/result/header
 
     This template will render a results header inside mod_h5pactivity results report.
 
@@ -15,7 +15,7 @@
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
-    @template mod_h5pactivity/result/options
+    @template mod_h5pactivity/local/result/options
 
     This template will render a choices table inside a H5P activity results report.
 
@@ -99,8 +99,8 @@
     {{#options}}
     <tr>
         <td>{{description}}</td>
-        <td>{{#correctanswer}}{{>mod_h5pactivity/result/answer}}{{/correctanswer}}</td>
-        <td>{{#useranswer}}{{>mod_h5pactivity/result/answer}}{{/useranswer}}</td>
+        <td>{{#correctanswer}}{{>mod_h5pactivity/local/result/answer}}{{/correctanswer}}</td>
+        <td>{{#useranswer}}{{>mod_h5pactivity/local/result/answer}}{{/useranswer}}</td>
     </tr>
     {{/options}}
     {{#score}}
index 4309275..4e5d219 100644 (file)
@@ -26,7 +26,7 @@
     Variables optional for this template:
     * hasoptions - If an option table must be present
     * optionslabel - The right label for available options on this result type
-    * options - An array of mod_h5pactivity/result/options compatible array
+    * options - An array of mod_h5pactivity/local/result/options compatible array
     * content - Extra content in HTML
     * track - Indicate if the result has displayable tracking
 
 
 <div class="container-fluid w-100 my-0 p-0">
     <div class="row w-100 py-3 px-1 m-0 p-md-3">
-        {{>mod_h5pactivity/result/header}}
+        {{>mod_h5pactivity/local/result/header}}
         {{{content}}}
         {{#hasoptions}}
-            {{>mod_h5pactivity/result/options}}
+            {{>mod_h5pactivity/local/result/options}}
         {{/hasoptions}}
         {{^track}}
             <div class="alert alert-warning w-100" role="alert">
index 62050d2..ac6014a 100644 (file)
@@ -65,16 +65,21 @@ class get_h5pactivities_by_courses_testcase extends externallib_advanced_testcas
             'introformat' => 1
         ];
         $activities[] = $this->getDataGenerator()->create_module('h5pactivity', $params);
-        // Add filename to make easier the asserts.
+        // Add filename and contextid to make easier the asserts.
         $activities[0]->filename = 'filltheblanks.h5p';
+        $context = context_module::instance($activities[0]->cmid);
+        $activities[0]->contextid = $context->id;
+
         $params = [
             'course' => $course1->id,
             'packagefilepath' => $CFG->dirroot.'/h5p/tests/fixtures/greeting-card-887.h5p',
             'introformat' => 1
         ];
         $activities[] = $this->getDataGenerator()->create_module('h5pactivity', $params);
-        // Add filename to make easier the asserts.
+        // Add filename and contextid to make easier the asserts.
         $activities[1]->filename = 'greeting-card-887.h5p';
+        $context = context_module::instance($activities[1]->cmid);
+        $activities[1]->contextid = $context->id;
 
         $course2 = $this->getDataGenerator()->create_course();
         $params = [
@@ -84,8 +89,9 @@ class get_h5pactivities_by_courses_testcase extends externallib_advanced_testcas
         ];
         $activities[] = $this->getDataGenerator()->create_module('h5pactivity', $params);
         $activities[2]->filename = 'guess-the-answer.h5p';
-
         $context = context_module::instance($activities[2]->cmid);
+        $activities[2]->contextid = $context->id;
+
         // Create a fake deploy H5P file.
         $generator = $this->getDataGenerator()->get_plugin_generator('core_h5p');
         $deployedfile = $generator->create_export_file($activities[2]->filename, $context->id, 'mod_h5pactivity', 'package');
@@ -176,6 +182,7 @@ class get_h5pactivities_by_courses_testcase extends externallib_advanced_testcas
             $this->assertEquals($activities[$i]->enabletracking, $result['h5pactivities'][$i]['enabletracking']);
             $this->assertEquals($activities[$i]->grademethod, $result['h5pactivities'][$i]['grademethod']);
             $this->assertEquals($activities[$i]->cmid, $result['h5pactivities'][$i]['coursemodule']);
+            $this->assertEquals($activities[$i]->contextid, $result['h5pactivities'][$i]['context']);
             $this->assertEquals($activities[$i]->filename, $result['h5pactivities'][$i]['package'][0]['filename']);
         }
     }
index fcef64d..00c587c 100644 (file)
@@ -46,42 +46,36 @@ class get_h5pactivity_access_information_testcase extends externallib_advanced_t
 
     /**
      * Test the behaviour of get_h5pactivity_access_information().
+     *
+     * @dataProvider get_h5pactivity_access_information_data
+     * @param string $role user role in course
+     * @param int $enabletracking if tracking is enabled
+     * @param array $enabledcaps capabilities enabled
      */
-    public function test_get_h5pactivity_access_information() {
+    public function test_get_h5pactivity_access_information(string $role, int $enabletracking, array $enabledcaps) {
         $this->resetAfterTest();
         $this->setAdminUser();
 
         $course = $this->getDataGenerator()->create_course();
-        $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]);
-        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
-        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
+        $activity = $this->getDataGenerator()->create_module('h5pactivity',
+            [
+                'course' => $course,
+                'enabletracking' => $enabletracking
+            ]
+        );
 
-        // Check the access information for a student.
-        $this->setUser($student);
-        $result = get_h5pactivity_access_information::execute($activity->id);
-        $result = external_api::clean_returnvalue(get_h5pactivity_access_information::execute_returns(), $result);
-        $this->assertCount(0, $result['warnings']);
-        unset($result['warnings']);
-
-        // Check default values for capabilities for student.
-        $enabledcaps = ['canview', 'cansubmit'];
-        foreach ($result as $capname => $capvalue) {
-            if (in_array($capname, $enabledcaps)) {
-                $this->assertTrue($capvalue);
-            } else {
-                $this->assertFalse($capvalue);
-            }
+        if ($role) {
+            $user = $this->getDataGenerator()->create_and_enrol($course, $role);
+            $this->setUser($user);
         }
 
-        // Check the access information for a teacher.
-        $this->setUser($teacher);
+        // Check the access information.
         $result = get_h5pactivity_access_information::execute($activity->id);
         $result = external_api::clean_returnvalue(get_h5pactivity_access_information::execute_returns(), $result);
         $this->assertCount(0, $result['warnings']);
         unset($result['warnings']);
 
-        // Check default values for capabilities for teacher.
-        $enabledcaps = ['canview', 'canaddinstance', 'canreviewattempts'];
+        // Check the values for capabilities.
         foreach ($result as $capname => $capvalue) {
             if (in_array($capname, $enabledcaps)) {
                 $this->assertTrue($capvalue);
@@ -89,9 +83,55 @@ class get_h5pactivity_access_information_testcase extends externallib_advanced_t
                 $this->assertFalse($capvalue);
             }
         }
+    }
+
+    /**
+     * Data provider for get_h5pactivity_access_information.
+     *
+     * @return array
+     */
+    public function get_h5pactivity_access_information_data(): array {
+        return [
+            'Admin, tracking enabled' => [
+                '', 1, ['canview', 'canreviewattempts', 'canaddinstance']
+            ],
+            'Admin, tracking disabled' => [
+                '', 0, ['canview', 'canreviewattempts', 'canaddinstance']
+            ],
+            'Student, tracking enabled' => [
+                'student', 1, ['canview', 'cansubmit']
+            ],
+            'Student, tracking disabled' => [
+                'student', 0, ['canview']
+            ],
+            'Teacher, tracking enabled' => [
+                'editingteacher', 1, [
+                    'canview',
+                    'canreviewattempts',
+                    'canaddinstance'
+                ]
+            ],
+            'Teacher, tracking disabled' => [
+                'editingteacher', 0, [
+                    'canview',
+                    'canreviewattempts',
+                    'canaddinstance'
+                ]
+            ],
+        ];
+    }
+
+    /**
+     * Test dml_missing_record_exception in get_h5pactivity_access_information.
+     */
+    public function test_dml_missing_record_exception() {
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $course = $this->getDataGenerator()->create_course();
 
         // Call the WS using an unexisting h5pactivityid.
         $this->expectException(dml_missing_record_exception::class);
-        $result = get_h5pactivity_access_information::execute($activity->id + 1);
+        $result = get_h5pactivity_access_information::execute(1);
     }
 }
\ No newline at end of file
index 8724019..297af38 100644 (file)
     display: none;
 }
 
+
 .que.calculatedmulti .answer div.r0,
 .que.calculatedmulti .answer div.r1 {
-    padding: 0.3em;
+    display: flex;
+    margin: 0.25rem 0;
+    align-items: flex-start;
+}
+
+.que.calculatedmulti .answer div.r0 input,
+.que.calculatedmulti .answer div.r1 input {
+    margin: 0.3rem 0.5rem;
+    width: 14px;
 }
index 61bd64b..1eb3da4 100644 (file)
Binary files a/question/type/multichoice/amd/build/clearchoice.min.js and b/question/type/multichoice/amd/build/clearchoice.min.js differ
index 75a3b58..20086fd 100644 (file)
Binary files a/question/type/multichoice/amd/build/clearchoice.min.js.map and b/question/type/multichoice/amd/build/clearchoice.min.js.map differ
index 850bbf7..5fb268c 100644 (file)
 define(['jquery', 'core/custom_interaction_events'], function($, CustomEvents) {
 
     var SELECTORS = {
-        ANSWER_RADIOS: '.answer input',
-        CLEARRESULTS_BUTTON: 'button[data-action="clearresults"]'
+        CHOICE_ELEMENT: '.answer input',
+        LINK: 'a',
+        RADIO: 'input[type="radio"]'
     };
 
-    var CSSHIDDEN = 'd-none';
+    /**
+     * Mark clear choice radio as enabled and checked.
+     *
+     * @param {Object} clearChoiceContainer The clear choice option container.
+     */
+    var checkClearChoiceRadio = function(clearChoiceContainer) {
+        clearChoiceContainer.find(SELECTORS.RADIO).prop('disabled', false).prop('checked', true);
+    };
+
+    /**
+     * Get the clear choice div container.
+     *
+     * @param {Object} root The question root element.
+     * @param {string} fieldPrefix The question outer div prefix.
+     * @returns {Object} The clear choice div container.
+     */
+    var getClearChoiceElement = function(root, fieldPrefix) {
+        return root.find('div[id="' + fieldPrefix + '"]');
+    };
+
+    /**
+     * Hide clear choice option.
+     *
+     * @param {Object} clearChoiceContainer The clear choice option container.
+     */
+    var hideClearChoiceOption = function(clearChoiceContainer) {
+        clearChoiceContainer.addClass('sr-only');
+        clearChoiceContainer.find(SELECTORS.LINK).attr('tabindex', -1);
+    };
+
+    /**
+     * Shows clear choice option.
+     *
+     * @param {Object} clearChoiceContainer The clear choice option container.
+     */
+    var showClearChoiceOption = function(clearChoiceContainer) {
+        clearChoiceContainer.removeClass('sr-only');
+        clearChoiceContainer.find(SELECTORS.LINK).attr('tabindex', 0);
+        clearChoiceContainer.find(SELECTORS.RADIO).prop('disabled', true);
+    };
 
     /**
      * Register event listeners for the clear choice module.
      *
      * @param {Object} root The question outer div prefix.
+     * @param {string} fieldPrefix The "Clear choice" div prefix.
      */
-    var registerEventListeners = function(root) {
+    var registerEventListeners = function(root, fieldPrefix) {
+        var clearChoiceContainer = getClearChoiceElement(root, fieldPrefix);
+
+        clearChoiceContainer.on(CustomEvents.events.activate, SELECTORS.LINK, function(e, data) {
 
-        var clearChoiceButton = root.find(SELECTORS.CLEARRESULTS_BUTTON);
+                // Mark the clear choice radio element as checked.
+                checkClearChoiceRadio(clearChoiceContainer);
+                // Now that the hidden radio has been checked, hide the clear choice option.
+                hideClearChoiceOption(clearChoiceContainer);
+
+                data.originalEvent.preventDefault();
+        });
 
-        root.on(CustomEvents.events.activate, SELECTORS.CLEARRESULTS_BUTTON, function(e, data) {
-            root.find(SELECTORS.ANSWER_RADIOS).each(function() {
-                $(this).prop('checked', false);
-            });
-            $(e.target).addClass(CSSHIDDEN);
-            data.originalEvent.preventDefault();
+        root.on(CustomEvents.events.activate, SELECTORS.CHOICE_ELEMENT, function() {
+            // If the event has been triggered by any other choice, show the clear choice option.
+            showClearChoiceOption(clearChoiceContainer);
         });
 
-        root.on(CustomEvents.events.activate, SELECTORS.ANSWER_RADIOS, function() {
-            clearChoiceButton.removeClass(CSSHIDDEN);
+        // If the clear choice radio receives focus from using the tab key, return the focus
+        // to the first answer option.
+        clearChoiceContainer.find(SELECTORS.RADIO).focus(function() {
+            var firstChoice = root.find(SELECTORS.CHOICE_ELEMENT).first();
+            firstChoice.focus();
         });
     };
 
@@ -56,10 +106,11 @@ define(['jquery', 'core/custom_interaction_events'], function($, CustomEvents) {
      * Initialise clear choice module.
 
      * @param {string} root The question outer div prefix.
+     * @param {string} fieldPrefix The "Clear choice" div prefix.
      */
-    var init = function(root) {
+    var init = function(root, fieldPrefix) {
         root = $('#' + root);
-        registerEventListeners(root);
+        registerEventListeners(root, fieldPrefix);
     };
 
     return {
index 7412d7a..966edca 100644 (file)
@@ -286,25 +286,38 @@ class qtype_multichoice_single_renderer extends qtype_multichoice_renderer_base
             }
         }
 
-        $questiondivid = $qa->get_outer_question_div_unique_id();
+        $clearchoiceid = $this->get_input_id($qa, -1);
+        $clearchoicefieldname = $qa->get_qt_field_name('clearchoice');
+        $clearchoiceradioattrs = [
+            'type' => $this->get_input_type(),
+            'name' => $qa->get_qt_field_name('answer'),
+            'id' => $clearchoiceid,
+            'value' => -1,
+            'class' => 'sr-only'
+        ];
 
+        $cssclass = 'qtype_multichoice_clearchoice';
         // When no choice selected during rendering, then hide the clear choice option.
-        $cssclass = '';
+        $linktabindex = 0;
         if (!$hascheckedchoice && $response == -1) {
-            $cssclass = 'd-none';
+            $cssclass .= ' sr-only';
+            $clearchoiceradioattrs['checked'] = 'checked';
+            $linktabindex = -1;
         }
+        // Adds an hidden radio that will be checked to give the impression the choice has been cleared.
+        $clearchoiceradio = html_writer::empty_tag('input', $clearchoiceradioattrs);
+        $clearchoiceradio .= html_writer::link('', get_string('clearchoice', 'qtype_multichoice'),
+            ['for' => $clearchoiceid, 'role' => 'button', 'tabindex' => $linktabindex,
+            'class' => 'btn btn-link ml-4 pl-1 mt-2']);
 
-        $clearchoicebutton = html_writer::tag('button', get_string('clearchoice', 'qtype_multichoice'), [
-            'class' => 'btn btn-link ml-3 ' . $cssclass,
-            'data-action' => 'clearresults',
-            'data-target' => '#' . $questiondivid
-        ]);
+        // Now wrap the radio and label inside a div.
+        $result = html_writer::tag('div', $clearchoiceradio, ['id' => $clearchoicefieldname, 'class' => $cssclass]);
 
         // Load required clearchoice AMD module.
         $this->page->requires->js_call_amd('qtype_multichoice/clearchoice', 'init',
-            [$questiondivid]);
+            [$qa->get_outer_question_div_unique_id(), $clearchoicefieldname]);
 
-        return $clearchoicebutton;
+        return $result;
     }
 
 }
index cef8048..df8ed86 100644 (file)
@@ -22,7 +22,8 @@
 
 .que.multichoice .answer div.r0 input,
 .que.multichoice .answer div.r1 input {
-    margin: 0.4rem 0.5rem;
+    margin: 0.3rem 0.5rem;
+    width: 14px;
 }
 
 /* Editing form. */
index 2facc00..3f78277 100644 (file)
@@ -19,7 +19,7 @@ Feature: Clear my answers
       | Course       | C1        | Test questions |
     And the following "questions" exist:
       | questioncategory | qtype       | name             | template    | questiontext    |
-      | Test questions   | multichoice | Multi-choice-001 | one_of_four | Question One  |
+      | Test questions   | multichoice | Multi-choice-001 | one_of_four | Question One    |
     And the following "activities" exist:
       | activity   | name   | intro              | course | idnumber | preferredbehaviour | canredoquestions |
       | quiz       | Quiz 1 | Quiz 1 description | C1     | quiz1    | immediatefeedback  | 1                |
@@ -40,3 +40,18 @@ Feature: Clear my answers
     Then I should not see "Clear my choice"
     And I click on "Check" "button" in the "Question One" "question"
     And I should see "Please select an answer" in the "Question One" "question"
+
+  @javascript
+  Scenario: Attempt a quiz and revisit a cleared answer.
+    When I log in as "student1"
+    And I am on "Course 1" course homepage
+    And I follow "Quiz 1"
+    And I press "Attempt quiz now"
+    And I should see "Question One"
+    And I click on "Four" "radio" in the "Question One" "question"
+    And I follow "Finish attempt ..."
+    And I click on "Return to attempt" "button"
+    And I click on "Clear my choice" "button" in the "Question One" "question"
+    And I follow "Finish attempt ..."
+    And I click on "Return to attempt" "button"
+    Then I should not see "Clear my choice"
index 95902b7..53ae95e 100644 (file)
@@ -126,6 +126,75 @@ class qtype_multichoice_walkthrough_test extends qbehaviour_walkthrough_test_bas
                 new question_pattern_expectation('/class="r1"/'));
     }
 
+    /**
+     * Test for clear choice option.
+     */
+    public function test_deferredfeedback_feedback_multichoice_clearchoice() {
+
+        // Create a multichoice, single question.
+        $mc = test_question_maker::make_a_multichoice_single_question();
+        $mc->shuffleanswers = false;
+
+        $clearchoice = -1;
+        $rightchoice = 0;
+        $wrongchoice = 2;
+
+        $this->start_attempt_at_question($mc, 'deferredfeedback', 3);
+
+        // Let's first submit the wrong choice (2).
+        $this->process_submission(array('answer' => $wrongchoice));  // Wrong choice (2).
+
+        $this->check_current_mark(null);
+        // Clear choice radio should not be checked.
+        $this->check_current_output(
+            $this->get_contains_mc_radio_expectation($rightchoice, true, false), // Not checked.
+            $this->get_contains_mc_radio_expectation($rightchoice + 1, true, false), // Not checked.
+            $this->get_contains_mc_radio_expectation($rightchoice + 2, true, true), // Wrong choice (2) checked.
+            $this->get_contains_mc_radio_expectation($clearchoice, true, false), // Not checked.
+            $this->get_does_not_contain_correctness_expectation(),
+            $this->get_does_not_contain_feedback_expectation()
+        );
+
+        // Now, let's clear our previous choice.
+        $this->process_submission(array('answer' => $clearchoice)); // Clear choice (-1).
+        $this->check_current_mark(null);
+
+        // This time, the clear choice radio should be the only one checked.
+        $this->check_current_output(
+            $this->get_contains_mc_radio_expectation($rightchoice, true, false), // Not checked.
+            $this->get_contains_mc_radio_expectation($rightchoice + 1, true, false), // Not checked.
+            $this->get_contains_mc_radio_expectation($rightchoice + 2, true, false), // Not checked.
+            $this->get_contains_mc_radio_expectation($clearchoice, true, true), // Clear choice radio checked.
+            $this->get_does_not_contain_correctness_expectation(),
+            $this->get_does_not_contain_feedback_expectation()
+        );
+
+        // Finally, let's submit the right choice.
+        $this->process_submission(array('answer' => $rightchoice)); // Right choice (0).
+        $this->check_current_state(question_state::$complete);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+            $this->get_contains_mc_radio_expectation($rightchoice, true, true),
+            $this->get_contains_mc_radio_expectation($rightchoice + 1, true, false),
+            $this->get_contains_mc_radio_expectation($rightchoice + 2, true, false),
+            $this->get_contains_mc_radio_expectation($clearchoice, true, false),
+            $this->get_does_not_contain_correctness_expectation(),
+            $this->get_does_not_contain_feedback_expectation()
+        );
+
+        // Finish the attempt.
+        $this->finish();
+
+        // Verify.
+        $this->check_current_state(question_state::$gradedright);
+        $this->check_current_mark(3);
+        $this->check_current_output(
+            $this->get_contains_mc_radio_expectation($rightchoice, false, true),
+            $this->get_contains_correct_expectation(),
+            new question_pattern_expectation('/class="r0 correct"/'),
+            new question_pattern_expectation('/class="r1"/'));
+    }
+
     public function test_deferredfeedback_feedback_multichoice_multi_showstandardunstruction_yes() {
 
         // Create a multichoice, multi question.
diff --git a/repository/contentbank/tests/behat/file_update.feature b/repository/contentbank/tests/behat/file_update.feature
new file mode 100644 (file)
index 0000000..96cb40a
--- /dev/null
@@ -0,0 +1,103 @@
+@repository @repository_contentbank @javascript @core_h5p
+Feature: Updating a file in the content bank after using in a course
+  In order to use file alias
+  As a user
+  Updated files must update references when is an alias
+
+  Background:
+    Given the following "categories" exist:
+      | name      | category | idnumber |
+      | Category1 | 0        | CAT1     |
+    And the following "courses" exist:
+      | fullname | shortname | category |
+      | Course1  | C1        | CAT1     |
+    And the following "contentbank content" exist:
+      | contextlevel | reference | contenttype     | user  | contentname | filepath                                  |
+      | Course       | C1        | contenttype_h5p | admin | package.h5p | /h5p/tests/fixtures/guess-the-answer.h5p  |
+    And the following "activities" exist:
+      | activity | name       | intro      | introformat | course | content  | contentformat | idnumber |
+      | page     | PageName1  | PageDesc1  | 1           | C1     | H5Ptest  | 1             | 1        |
+    And I log in as "admin"
+
+  Scenario: Referenced files updates alias as well
+    Given I am on "Course1" course homepage
+    And I follow "PageName1"
+    And I navigate to "Edit settings" in current page administration
+    And I click on "Insert H5P" "button" in the "#fitem_id_page" "css_element"
+    And I click on "Browse repositories..." "button" in the "Insert H5P" "dialogue"
+    And I select "Content bank" repository in file picker
+    And I click on "package.h5p" "file" in repository content area
+    And I click on "Create an alias/shortcut to the file" "radio"
+    And I click on "Select this file" "button"
+    And I click on "Insert H5P" "button" in the "Insert H5P" "dialogue"
+    And I wait until the page is ready
+    And I click on "Save and display" "button"
+    And I switch to "h5p-iframe" class iframe
+    And I switch to "h5p-iframe" class iframe
+    And I should see "Press here to reveal answer"
+    And I switch to the main frame
+    # Now edit the content in the content bank.
+    When I am on "Course1" course homepage with editing mode on
+    And I add the "Navigation" block if not present
+    And I expand "Site pages" node
+    And I click on "Content bank" "link"
+    And I click on "package.h5p" "link"
+    And I click on "Edit" "link"
+    And I wait until the page is ready
+    And I switch to "h5p-editor-iframe" class iframe
+    And I set the field "Title" to "Required title"
+    And I set the field "Descriptive solution label" to "This is a new text"
+    And I switch to the main frame
+    And I click on "Save" "button"
+    And I switch to "h5p-player" class iframe
+    And I switch to "h5p-iframe" class iframe
+    And I should see "This is a new text"
+    And I switch to the main frame
+    # Check the course page is updated.
+    Then I am on "Course1" course homepage
+    And I follow "PageName1"
+    And I switch to "h5p-iframe" class iframe
+    And I switch to "h5p-iframe" class iframe
+    And I should see "This is a new text"
+    And I switch to the main frame
+
+  Scenario: Copied files should not be updated if the original is edited
+    Given I am on "Course1" course homepage
+    And I follow "PageName1"
+    And I navigate to "Edit settings" in current page administration
+    And I click on "Insert H5P" "button" in the "#fitem_id_page" "css_element"
+    And I click on "Browse repositories..." "button" in the "Insert H5P" "dialogue"
+    And I select "Content bank" repository in file picker
+    And I click on "package.h5p" "file" in repository content area
+    And I click on "Select this file" "button"
+    And I click on "Insert H5P" "button" in the "Insert H5P" "dialogue"
+    And I wait until the page is ready
+    And I click on "Save and display" "button"
+    And I switch to "h5p-iframe" class iframe
+    And I switch to "h5p-iframe" class iframe
+    And I should see "Press here to reveal answer"
+    And I switch to the main frame
+    # Now edit the content in the content bank.
+    When I am on "Course1" course homepage with editing mode on
+    And I add the "Navigation" block if not present
+    And I expand "Site pages" node
+    And I click on "Content bank" "link"
+    And I click on "package.h5p" "link"
+    And I click on "Edit" "link"
+    And I wait until the page is ready
+    And I switch to "h5p-editor-iframe" class iframe
+    And I set the field "Title" to "Required title"
+    And I set the field "Descriptive solution label" to "This is a new text"
+    And I switch to the main frame
+    And I click on "Save" "button"
+    And I switch to "h5p-player" class iframe
+    And I switch to "h5p-iframe" class iframe
+    And I should see "This is a new text"
+    And I switch to the main frame
+    # Check the course page is not updated.
+    Then I am on "Course1" course homepage
+    And I follow "PageName1"
+    And I switch to "h5p-iframe" class iframe
+    And I switch to "h5p-iframe" class iframe
+    And I should see "Press here to reveal answer"
+    And I switch to the main frame
index 4c40775..618cbb1 100644 (file)
@@ -41,7 +41,7 @@ $string['flickr_public:view'] = 'Use Flickr public repository in file picker';
 $string['fulltext'] = 'Full text';
 $string['information'] = '<div>Get a <a href="http://www.flickr.com/services/api/keys/">Flickr API Key</a> for your Moodle site. </div>';
 $string['invalidemail'] = 'Invalid email account for flickr';
-$string['license'] = 'License';
+$string['license'] = 'Licence';
 $string['modification'] = 'I want to be able to modify the images';
 $string['notitle'] = 'notitle';
 $string['nullphotolist'] = 'There are no photos in this account';
index d2cb199..1a9f4e3 100644 (file)
@@ -24,7 +24,7 @@
  */
 
 $string['configplugin'] = 'Merlot.org configuration';
-$string['licensekey'] = 'License key';
+$string['licensekey'] = 'Licence key';
 $string['pluginname_help'] = 'Merlot.org';
 $string['pluginname'] = 'Merlot.org';
 $string['merlot:view'] = 'View the Merlot repository';
index afd70e8..ac58e81 100644 (file)
@@ -179,6 +179,17 @@ button.close {
     }
 }
 
+// Safari does not allow custom styling of checkboxes.
+.safari {
+    input[type="checkbox"],
+    input[type="radio"] {
+        &.focus,
+        &:focus {
+            outline: auto;
+        }
+    }
+}
+
 .usermenu,
 div.dropdown-item {
     a,
index 811f3c1..c9fc671 100644 (file)
@@ -9807,6 +9807,11 @@ a.dropdown-toggle:focus:hover,
 button.close:focus:hover {
   text-decoration: none; }
 
+.safari input[type="checkbox"].focus, .safari input[type="checkbox"]:focus,
+.safari input[type="radio"].focus,
+.safari input[type="radio"]:focus {
+  outline: auto; }
+
 .usermenu a,
 .usermenu a[role="button"],
 div.dropdown-item a,
index 33a0f58..c95eb24 100644 (file)
@@ -10,6 +10,7 @@
         .region-main {
             flex: 0 0 100%;
             padding: 0 1rem;
+            max-width: 100%;
         }
 
         &.blocks-pre {
 }
 
 @include media-breakpoint-up(sm) {
-    .block_myoverview,
-    .block_recentlyaccesseditems {
-        .dashboard-card-deck {
-            .dashboard-card {
-                width: calc(33.33% - #{$card-gutter});
-            }
+    .dashboard-card-deck .dashboard-card {
+        width: calc(50% - #{$card-gutter});
+    }
+}
+
+@include media-breakpoint-up(md) {
+    .dashboard-card-deck .dashboard-card {
+        width: calc(50% - #{$card-gutter});
+    }
+    .blocks-post,
+    .blocks-pre {
+        .dashboard-card-deck .dashboard-card {
+            width: calc(100% - #{$card-gutter});
+        }
+    }
+}
+
+@include media-breakpoint-up(lg) {
+    .dashboard-card-deck .dashboard-card {
+        width: calc(33.33% - #{$card-gutter});
+    }
+    .blocks-post,
+    .blocks-pre {
+        .dashboard-card-deck .dashboard-card {
+            width: calc(50% - #{$card-gutter});
+        }
+    }
+}
+
+@include media-breakpoint-up(xl) {
+    .dashboard-card-deck .dashboard-card {
+        width: calc(25% - #{$card-gutter});
+    }
+    .blocks-post,
+    .blocks-pre {
+        .dashboard-card-deck .dashboard-card {
+            width: calc(33.33% - #{$card-gutter});
         }
     }
 }
index 6404916..7be6184 100644 (file)
@@ -10012,6 +10012,11 @@ a.dropdown-toggle:focus:hover,
 button.close:focus:hover {
   text-decoration: none; }
 
+.safari input[type="checkbox"].focus, .safari input[type="checkbox"]:focus,
+.safari input[type="radio"].focus,
+.safari input[type="radio"]:focus {
+  outline: auto; }
+
 .usermenu a,
 .usermenu a[role="button"],
 div.dropdown-item a,
@@ -19430,7 +19435,8 @@ body {
     display: flex; }
     #page-content .region-main {
       flex: 0 0 100%;
-      padding: 0 1rem; }
+      padding: 0 1rem;
+      max-width: 100%; }
     #page-content.blocks-pre .columnleft {
       flex: 0 0 32%;
       order: -1;
@@ -19490,7 +19496,8 @@ body {
     display: flex; }
     #page-content .region-main {
       flex: 0 0 100%;
-      padding: 0 1rem; }
+      padding: 0 1rem;
+      max-width: 100%; }
     #page-content.blocks-pre .columnleft {
       flex: 0 0 25%;
       order: -1;
@@ -19550,7 +19557,8 @@ body {
     display: flex; }
     #page-content .region-main {
       flex: 0 0 100%;
-      padding: 0 1rem; }
+      padding: 0 1rem;
+      max-width: 100%; }
     #page-content.blocks-pre .columnleft {
       flex: 0 0 20%;
       order: -1;
@@ -19615,8 +19623,28 @@ body {
     /* stylelint-disable-line declaration-no-important */ } }
 
 @media (min-width: 576px) {
-  .block_myoverview .dashboard-card-deck .dashboard-card,
-  .block_recentlyaccesseditems .dashboard-card-deck .dashboard-card {
+  .dashboard-card-deck .dashboard-card {
+    width: calc(50% - 0.5rem); } }
+
+@media (min-width: 768px) {
+  .dashboard-card-deck .dashboard-card {
+    width: calc(50% - 0.5rem); }
+  .blocks-post .dashboard-card-deck .dashboard-card,
+  .blocks-pre .dashboard-card-deck .dashboard-card {
+    width: calc(100% - 0.5rem); } }
+
+@media (min-width: 992px) {
+  .dashboard-card-deck .dashboard-card {
+    width: calc(33.33% - 0.5rem); }
+  .blocks-post .dashboard-card-deck .dashboard-card,
+  .blocks-pre .dashboard-card-deck .dashboard-card {
+    width: calc(50% - 0.5rem); } }
+
+@media (min-width: 1200px) {
+  .dashboard-card-deck .dashboard-card {
+    width: calc(25% - 0.5rem); }
+  .blocks-post .dashboard-card-deck .dashboard-card,
+  .blocks-pre .dashboard-card-deck .dashboard-card {
     width: calc(33.33% - 0.5rem); } }
 
 @media (min-width: 768px) {
index 0d94483..1970c21 100644 (file)
@@ -265,9 +265,9 @@ class participants_search {
             }
         }
 
-        // Add any supplied additional WHERE clauses.
+        // Add any supplied additional forced WHERE clauses.
         if (!empty($additionalwhere)) {
-            $wheres[] = $additionalwhere;
+            $innerwhere .= " AND ({$additionalwhere})";
             $params = array_merge($params, $additionalparams);
         }
 
index 5e243dd..60475cd 100644 (file)
@@ -664,3 +664,25 @@ Feature: Course participants can be filtered
     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"
+
+  @javascript
+  Scenario: Initials filtering is always applied in addition to any other filtering
+    Given I log in as "teacher1"
+    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"
+    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 click on "Apply filters" "button"
+    When 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"
+    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"
index afa9555..386a576 100644 (file)
@@ -29,9 +29,9 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2020060700.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2020060900.00;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.
-$release  = '3.9beta (Build: 20200607)'; // Human-friendly version name
+$release  = '3.9rc1 (Build: 20200609)'; // Human-friendly version name
 $branch   = '39';                       // This version's branch.
-$maturity = MATURITY_BETA;             // This version's maturity level.
+$maturity = MATURITY_RC;             // This version's maturity level.