Merge branch 'MDL-68953-master' of git://github.com/lameze/moodle
authorVíctor Déniz Falcón <victor@moodle.com>
Tue, 9 Jun 2020 12:18:46 +0000 (13:18 +0100)
committerVíctor Déniz Falcón <victor@moodle.com>
Tue, 9 Jun 2020 12:18:46 +0000 (13:18 +0100)
45 files changed:
admin/tool/moodlenet/lang/en/tool_moodlenet.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
enrol/manual/amd/build/quickenrolment.min.js
enrol/manual/amd/build/quickenrolment.min.js.map
enrol/manual/amd/src/quickenrolment.js
h5p/classes/editor.php
h5p/classes/helper.php
lang/en/admin.php
lang/en/contentbank.php
lib/classes/plugin_manager.php
lib/table/amd/build/dynamic.min.js
lib/table/amd/build/dynamic.min.js.map
lib/table/amd/src/dynamic.js
lib/templates/filemanager_selectlayout.mustache
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/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/styles.css
repository/contentbank/tests/behat/file_update.feature [new file with mode: 0644]
theme/boost/scss/moodle/core.scss
theme/boost/style/moodle.css
theme/classic/scss/classic/post.scss
theme/classic/style/moodle.css
user/amd/build/participantsfilter.min.js
user/amd/build/participantsfilter.min.js.map
user/amd/build/status_field.min.js
user/amd/build/status_field.min.js.map
user/amd/src/participantsfilter.js
user/amd/src/status_field.js
version.php

index 1ba4e5b..d6f6ac1 100644 (file)
@@ -36,9 +36,9 @@ $string['defaultmoodlenet_desc'] = "The URL to either Moodle HQ's MoodleNet inst
 $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['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'] = 'Enabling the integration allows users with the capability to create and manage activities to
+browse MoodleNet from the activity chooser and import MoodleNet resources into their course. It also allows users with the
+capability to restore backups, to push backup files from MoodleNet into Moodle.';
 $string['errorduringdownload'] = 'An error occurred while downloading the file: {$a}';
 $string['forminfo'] = "It will be automatically saved on your moodle profile.";
 $string['footermessage'] = "Or browse for content on";
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 ee9d82b..a451f2b 100644 (file)
Binary files a/enrol/manual/amd/build/quickenrolment.min.js and b/enrol/manual/amd/build/quickenrolment.min.js differ
index a413c22..7561852 100644 (file)
Binary files a/enrol/manual/amd/build/quickenrolment.min.js.map and b/enrol/manual/amd/build/quickenrolment.min.js.map differ
index 087e4c4..ee67d3c 100644 (file)
@@ -165,9 +165,15 @@ const submitFormAjax = (dynamicTable, modal) => {
             throw new Error(response.error);
         }
 
-        DynamicTable.refreshTableContent(dynamicTable);
-        return Str.get_string('totalenrolledusers', 'enrol', response.count);
+        return response.count;
     })
+    .then(count => {
+        return Promise.all([
+            Str.get_string('totalenrolledusers', 'enrol', count),
+            DynamicTable.refreshTableContent(dynamicTable),
+        ]);
+    })
+    .then(([notificationBody]) => notificationBody)
     .then(notificationBody => Toast.add(notificationBody))
     .catch(error => {
         Notification.addNotification({
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 a04da00..872d80f 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> script 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.';
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 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 3bf3339..0060edc 100644 (file)
Binary files a/lib/table/amd/build/dynamic.min.js and b/lib/table/amd/build/dynamic.min.js differ
index f1e7466..3cbcaea 100644 (file)
Binary files a/lib/table/amd/build/dynamic.min.js.map and b/lib/table/amd/build/dynamic.min.js.map differ
index 1448991..b812a1f 100644 (file)
@@ -26,6 +26,7 @@ import Events from './local/dynamic/events';
 import Pending from 'core/pending';
 import {addIconToContainer} from 'core/loadingicon';
 import {fetch as fetchTableData} from 'core_table/local/dynamic/repository';
+import Notification from 'core/notification';
 
 let watching = false;
 
@@ -248,7 +249,7 @@ export const getPageNumber = tableRoot => getTableData(tableRoot).tablePageNumbe
  * @returns {Promise}
  */
 export const setPageSize = (tableRoot, pageSize, refreshContent = true) =>
-    updateTable(tableRoot, {pageSize, pageNumber: 0}, refreshContent);
+    updateTable(tableRoot, {pageSize, pageNumber: 1}, refreshContent);
 
 /**
  * Get the current page size.
@@ -353,49 +354,50 @@ export const init = () => {
         if (sortableLink) {
             e.preventDefault();
 
-            setSortOrder(tableRoot, sortableLink.dataset.sortby, sortableLink.dataset.sortorder);
+            setSortOrder(tableRoot, sortableLink.dataset.sortby, sortableLink.dataset.sortorder)
+            .catch(Notification.exception);
         }
 
         const firstInitialLink = e.target.closest(Selectors.initialsBar.links.firstInitial);
         if (firstInitialLink !== null) {
             e.preventDefault();
 
-            setFirstInitial(tableRoot, firstInitialLink.dataset.initial);
+            setFirstInitial(tableRoot, firstInitialLink.dataset.initial).catch(Notification.exception);
         }
 
         const lastInitialLink = e.target.closest(Selectors.initialsBar.links.lastInitial);
         if (lastInitialLink !== null) {
             e.preventDefault();
 
-            setLastInitial(tableRoot, lastInitialLink.dataset.initial);
+            setLastInitial(tableRoot, lastInitialLink.dataset.initial).catch(Notification.exception);
         }
 
         const pageItem = e.target.closest(Selectors.paginationBar.links.pageItem);
         if (pageItem) {
             e.preventDefault();
 
-            setPageNumber(tableRoot, pageItem.dataset.pageNumber);
+            setPageNumber(tableRoot, pageItem.dataset.pageNumber).catch(Notification.exception);
         }
 
         const hide = e.target.closest(Selectors.table.links.hide);
         if (hide) {
             e.preventDefault();
 
-            hideColumn(tableRoot, hide.dataset.column);
+            hideColumn(tableRoot, hide.dataset.column).catch(Notification.exception);
         }
 
         const show = e.target.closest(Selectors.table.links.show);
         if (show) {
             e.preventDefault();
 
-            showColumn(tableRoot, show.dataset.column);
+            showColumn(tableRoot, show.dataset.column).catch(Notification.exception);
         }
 
         const resetTablePreferencesLink = e.target.closest('.resettable a');
         if (resetTablePreferencesLink) {
             e.preventDefault();
 
-            resetTablePreferences(tableRoot);
+            resetTablePreferences(tableRoot).catch(Notification.exception);
         }
     });
 };
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">
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);
@@ -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 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. */
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 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 1176f2b..60a726d 100644 (file)
Binary files a/user/amd/build/participantsfilter.min.js and b/user/amd/build/participantsfilter.min.js differ
index b4c9f89..7965370 100644 (file)
Binary files a/user/amd/build/participantsfilter.min.js.map and b/user/amd/build/participantsfilter.min.js.map differ
index 5c1b1bb..f0f6af8 100644 (file)
Binary files a/user/amd/build/status_field.min.js and b/user/amd/build/status_field.min.js differ
index 2c77fb2..52641a3 100644 (file)
Binary files a/user/amd/build/status_field.min.js.map and b/user/amd/build/status_field.min.js.map differ
index 718e5ab..4e31a15 100644 (file)
@@ -371,7 +371,8 @@ export const init = participantsRegionId => {
                 filters: Object.values(activeFilters).map(filter => filter.filterValue),
                 jointype: filterSet.querySelector(Selectors.filterset.fields.join).value,
             }
-        );
+        )
+        .catch(Notification.exception);
     };
 
     /**
index 1563120..e5637f6 100644 (file)
@@ -280,7 +280,8 @@ const submitEditFormAjax = (clickedLink, getBody, modal, userEnrolmentId, userDa
         return data;
     })
     .then(() => {
-        DynamicTable.refreshTableContent(getDynamicTableFromLink(clickedLink));
+        DynamicTable.refreshTableContent(getDynamicTableFromLink(clickedLink))
+        .catch(Notification.exception);
 
         return Str.get_string('enrolmentupdatedforuser', 'core_enrol', userData);
     })
@@ -321,7 +322,8 @@ const submitUnenrolFormAjax = (clickedLink, modal, args, userData) => {
         return data;
     })
     .then(() => {
-        DynamicTable.refreshTableContent(getDynamicTableFromLink(clickedLink));
+        DynamicTable.refreshTableContent(getDynamicTableFromLink(clickedLink))
+        .catch(Notification.exception);
 
         return Str.get_string('unenrolleduser', 'core_enrol', userData);
     })
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.