Merge branch 'MDL-68963-master' of git://github.com/bmbrands/moodle
authorVíctor Déniz Falcón <victor@moodle.com>
Mon, 8 Jun 2020 21:03:39 +0000 (22:03 +0100)
committerVíctor Déniz Falcón <victor@moodle.com>
Mon, 8 Jun 2020 21:03:39 +0000 (22:03 +0100)
30 files changed:
admin/tool/moodlenet/lang/en/tool_moodlenet.php
contentbank/classes/contenttype.php
contentbank/classes/output/viewcontent.php
contentbank/edit.php
contentbank/tests/behat/edit_content.feature
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/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/tests/external/get_h5pactivities_by_courses_test.php
mod/h5pactivity/tests/external/get_h5pactivity_access_information_test.php
repository/contentbank/tests/behat/file_update.feature [new file with mode: 0644]
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

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 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 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 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);
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
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 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 9dcb372..7be6184 100644 (file)
@@ -19435,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;
@@ -19495,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;
@@ -19555,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;
@@ -19620,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);
     })