From a80dcdf641f1d8ca3f3168911337c28d91df1708 Mon Sep 17 00:00:00 2001 From: Sara Arjona Date: Wed, 9 Dec 2020 13:34:41 +0100 Subject: [PATCH 1/3] MDL-70438 contentbank: Add method get_uses to content --- contentbank/classes/content.php | 18 ++++++++++ contentbank/tests/content_test.php | 56 ++++++++++++++++++++++++++++++ contentbank/upgrade.txt | 5 +++ 3 files changed, 79 insertions(+) create mode 100644 contentbank/upgrade.txt diff --git a/contentbank/classes/content.php b/contentbank/classes/content.php index 227294bb60d..8048afefb5e 100644 --- a/contentbank/classes/content.php +++ b/contentbank/classes/content.php @@ -310,6 +310,24 @@ abstract class content { return null; } + /** + * Returns the places where the file associated to this content is used or an empty array if the content has no file. + * + * @return array of stored_file where current file content is used or empty array if it hasn't any file. + * @since 3.11 + */ + public function get_uses(): ?array { + $references = []; + + $file = $this->get_file(); + if ($file != null) { + $fs = get_file_storage(); + $references = $fs->get_references_by_storedfile($file); + } + + return $references; + } + /** * Returns the file url related to this content. * diff --git a/contentbank/tests/content_test.php b/contentbank/tests/content_test.php index 4c7ec2b8a95..7bc2e831f05 100644 --- a/contentbank/tests/content_test.php +++ b/contentbank/tests/content_test.php @@ -298,4 +298,60 @@ class core_contenttype_content_testcase extends \advanced_testcase { $this->assertInstanceOf(get_class($type), $contenttype); } + + /** + * Tests for 'get_uses' behaviour. + * + * @covers ::get_uses + */ + public function test_get_uses() { + $this->resetAfterTest(); + $this->setAdminUser(); + $context = context_system::instance(); + + // Add some content to the content bank. + $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank'); + $contents = $generator->generate_contentbank_data('contenttype_testable', 3, 0, $context); + $content1 = array_shift($contents); + + // Check content has no references for now. + $this->assertCount(0, $content1->get_uses()); + + // Add a link to the previous content. + $cbfile = $content1->get_file(); + $cbrecord = array( + 'contextid' => $cbfile->get_contextid(), + 'component' => $cbfile->get_component(), + 'filearea' => $cbfile->get_filearea(), + 'itemid' => $cbfile->get_itemid(), + 'filepath' => $cbfile->get_filepath(), + 'filename' => $cbfile->get_filename(), + ); + $fs = get_file_storage(); + $ref = $fs->pack_reference($cbrecord); + + $aliasrecord = new stdClass(); + $aliasrecord->contextid = $context->id; + $aliasrecord->component = 'core'; + $aliasrecord->filearea = 'phpunit'; + $aliasrecord->filepath = '/foo/'; + $aliasrecord->filename = 'one.txt'; + $aliasrecord->itemid = 0; + + $repos = \repository::get_instances(['type' => 'contentbank']); + $cbrepo = reset($repos); + $this->assertInstanceOf('repository', $cbrepo); + + $alias = $fs->create_file_from_reference($aliasrecord, $cbrepo->id, $ref); + + // Check content now has one reference (the previous alias). + $contentuses1 = $content1->get_uses(); + $this->assertCount(1, $contentuses1); + $reffile = reset($contentuses1); + $this->assertEquals($alias, $reffile); + + // Check a different content hasn't any reference. + $content2 = array_shift($contents); + $this->assertCount(0, $content2->get_uses()); + } } diff --git a/contentbank/upgrade.txt b/contentbank/upgrade.txt new file mode 100644 index 00000000000..a94fbcf61d0 --- /dev/null +++ b/contentbank/upgrade.txt @@ -0,0 +1,5 @@ +This files describes API changes in core libraries and APIs, +information provided here is intended especially for developers. + +=== 3.11 === +* Added "get_uses()" method to content class to return places where a content is used. From d83b4b4abfc68c0810b7475702019f74b1832e1a Mon Sep 17 00:00:00 2001 From: Sara Arjona Date: Wed, 9 Dec 2020 14:42:39 +0100 Subject: [PATCH 2/3] MDL-70438 contentbank: Display content uses before removal --- contentbank/amd/build/actions.min.js | 2 +- contentbank/amd/build/actions.min.js.map | 2 +- contentbank/amd/src/actions.js | 10 +++++- .../tests/behat/delete_content.feature | 32 ++++++++++++++++--- contentbank/view.php | 1 + lang/en/contentbank.php | 1 + 6 files changed, 40 insertions(+), 8 deletions(-) diff --git a/contentbank/amd/build/actions.min.js b/contentbank/amd/build/actions.min.js index 0f2b538f2ec..584951ade92 100644 --- a/contentbank/amd/build/actions.min.js +++ b/contentbank/amd/build/actions.min.js @@ -1,2 +1,2 @@ -define ("core_contentbank/actions",["jquery","core/ajax","core/notification","core/str","core/templates","core/url","core/modal_factory","core/modal_events"],function(a,b,c,d,e,f,g,h){var k={DELETE_CONTENT:"[data-action=\"deletecontent\"]",RENAME_CONTENT:"[data-action=\"renamecontent\"]"},l=function(){this.registerEvents()};l.prototype.registerEvents=function(){a(k.DELETE_CONTENT).click(function(b){b.preventDefault();var e=a(this).data("contentname"),f=a(this).data("contentid"),j=a(this).data("contextid"),k="";d.get_strings([{key:"deletecontent",component:"core_contentbank"},{key:"deletecontentconfirm",component:"core_contentbank",param:{name:e}},{key:"delete",component:"core"}]).then(function(a){var b=a[0],c=a[1];k=a[2];return g.create({title:b,body:c,type:g.types.SAVE_CANCEL,large:!0})}).done(function(a){a.setSaveButtonText(k);a.getRoot().on(h.save,function(){return i(f,j)});a.getRoot().on(h.hidden,function(){a.destroy()});a.show()}).catch(c.exception)});a(k.RENAME_CONTENT).click(function(b){b.preventDefault();var f=a(this).data("contentname"),i=a(this).data("contentid"),k="";d.get_strings([{key:"renamecontent",component:"core_contentbank"},{key:"rename",component:"core_contentbank"}]).then(function(a){var b=a[0];k=a[1];return g.create({title:b,body:e.render("core_contentbank/renamecontent",{contentid:i,name:f}),type:g.types.SAVE_CANCEL})}).then(function(b){b.setSaveButtonText(k);b.getRoot().on(h.save,function(b){var e=a("#newname").val().trim();if(e){j(i,e)}else{d.get_strings([{key:"error"},{key:"emptynamenotallowed",component:"core_contentbank"}]).then(function(a){c.alert(a[0],a[1])}).catch(c.exception);b.preventDefault()}});b.getRoot().on(h.hidden,function(){b.destroy()});b.show()}).catch(c.exception)})};function i(a,d){var e="success";b.call([{methodname:"core_contentbank_delete_content",args:{contentids:{contentid:a}}}])[0].then(function(a){if(a.result){return"contentdeleted"}e="error";return"contentnotdeleted"}).done(function(a){var b={contextid:d};if("success"==e){b.statusmsg=a}else{b.errormsg=a}window.location.href=f.relativeUrl("contentbank/index.php",b,!1)}).fail(c.exception)}function j(a,d){var e="success";b.call([{methodname:"core_contentbank_rename_content",args:{contentid:a,name:d}}])[0].then(function(a){if(a.result){return"contentrenamed"}e="error";return a.warnings[0].message}).then(function(b){var d=null;if("success"==e){d={id:a,statusmsg:b};window.location.href=f.relativeUrl("contentbank/view.php",d,!1)}else{c.addNotification({message:b,type:"error"});c.fetchNotifications()}}).catch(c.exception)}return{init:function init(){return new l}}}); +define ("core_contentbank/actions",["jquery","core/ajax","core/notification","core/str","core/templates","core/url","core/modal_factory","core/modal_events"],function(a,b,c,d,e,f,g,h){var k={DELETE_CONTENT:"[data-action=\"deletecontent\"]",RENAME_CONTENT:"[data-action=\"renamecontent\"]"},l=function(){this.registerEvents()};l.prototype.registerEvents=function(){a(k.DELETE_CONTENT).click(function(b){b.preventDefault();var e=a(this).data("contentname"),f=a(this).data("uses"),j=a(this).data("contentid"),k=a(this).data("contextid"),l="";d.get_strings([{key:"deletecontent",component:"core_contentbank"},{key:"deletecontentconfirm",component:"core_contentbank",param:{name:e}},{key:"deletecontentconfirmlinked",component:"core_contentbank"},{key:"delete",component:"core"}]).then(function(a){var b=a[0],c=a[1];if(0.\n\n/**\n * Module to manage content bank actions, such as delete or rename.\n *\n * @module core_contentbank/actions\n * @package core_contentbank\n * @copyright 2020 Sara Arjona \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine([\n 'jquery',\n 'core/ajax',\n 'core/notification',\n 'core/str',\n 'core/templates',\n 'core/url',\n 'core/modal_factory',\n 'core/modal_events'],\nfunction($, Ajax, Notification, Str, Templates, Url, ModalFactory, ModalEvents) {\n\n /**\n * List of action selectors.\n *\n * @type {{DELETE_CONTENT: string}}\n */\n var ACTIONS = {\n DELETE_CONTENT: '[data-action=\"deletecontent\"]',\n RENAME_CONTENT: '[data-action=\"renamecontent\"]',\n };\n\n /**\n * Actions class.\n */\n var Actions = function() {\n this.registerEvents();\n };\n\n /**\n * Register event listeners.\n */\n Actions.prototype.registerEvents = function() {\n $(ACTIONS.DELETE_CONTENT).click(function(e) {\n e.preventDefault();\n\n var contentname = $(this).data('contentname');\n var contentid = $(this).data('contentid');\n var contextid = $(this).data('contextid');\n\n var strings = [\n {\n key: 'deletecontent',\n component: 'core_contentbank'\n },\n {\n key: 'deletecontentconfirm',\n component: 'core_contentbank',\n param: {\n name: contentname,\n }\n },\n {\n key: 'delete',\n component: 'core'\n },\n ];\n\n var deleteButtonText = '';\n Str.get_strings(strings).then(function(langStrings) {\n var modalTitle = langStrings[0];\n var modalContent = langStrings[1];\n deleteButtonText = langStrings[2];\n\n return ModalFactory.create({\n title: modalTitle,\n body: modalContent,\n type: ModalFactory.types.SAVE_CANCEL,\n large: true\n });\n }).done(function(modal) {\n modal.setSaveButtonText(deleteButtonText);\n modal.getRoot().on(ModalEvents.save, function() {\n // The action is now confirmed, sending an action for it.\n return deleteContent(contentid, contextid);\n });\n\n // Handle hidden event.\n modal.getRoot().on(ModalEvents.hidden, function() {\n // Destroy when hidden.\n modal.destroy();\n });\n\n // Show the modal.\n modal.show();\n\n return;\n }).catch(Notification.exception);\n });\n\n $(ACTIONS.RENAME_CONTENT).click(function(e) {\n e.preventDefault();\n\n var contentname = $(this).data('contentname');\n var contentid = $(this).data('contentid');\n\n var strings = [\n {\n key: 'renamecontent',\n component: 'core_contentbank'\n },\n {\n key: 'rename',\n component: 'core_contentbank'\n },\n ];\n\n var saveButtonText = '';\n Str.get_strings(strings).then(function(langStrings) {\n var modalTitle = langStrings[0];\n saveButtonText = langStrings[1];\n\n return ModalFactory.create({\n title: modalTitle,\n body: Templates.render('core_contentbank/renamecontent', {'contentid': contentid, 'name': contentname}),\n type: ModalFactory.types.SAVE_CANCEL\n });\n }).then(function(modal) {\n modal.setSaveButtonText(saveButtonText);\n modal.getRoot().on(ModalEvents.save, function(e) {\n // The action is now confirmed, sending an action for it.\n var newname = $(\"#newname\").val().trim();\n if (newname) {\n renameContent(contentid, newname);\n } else {\n var errorStrings = [\n {\n key: 'error',\n },\n {\n key: 'emptynamenotallowed',\n component: 'core_contentbank',\n },\n ];\n Str.get_strings(errorStrings).then(function(langStrings) {\n Notification.alert(langStrings[0], langStrings[1]);\n }).catch(Notification.exception);\n e.preventDefault();\n }\n });\n\n // Handle hidden event.\n modal.getRoot().on(ModalEvents.hidden, function() {\n // Destroy when hidden.\n modal.destroy();\n });\n\n // Show the modal.\n modal.show();\n\n return;\n }).catch(Notification.exception);\n });\n };\n\n /**\n * Delete content from the content bank.\n *\n * @param {int} contentid The content to delete.\n * @param {int} contextid The contextid where the content belongs.\n */\n function deleteContent(contentid, contextid) {\n var request = {\n methodname: 'core_contentbank_delete_content',\n args: {\n contentids: {contentid}\n }\n };\n\n var requestType = 'success';\n Ajax.call([request])[0].then(function(data) {\n if (data.result) {\n return 'contentdeleted';\n }\n requestType = 'error';\n return 'contentnotdeleted';\n\n }).done(function(message) {\n var params = {\n contextid: contextid\n };\n if (requestType == 'success') {\n params.statusmsg = message;\n } else {\n params.errormsg = message;\n }\n // Redirect to the main content bank page and display the message as a notification.\n window.location.href = Url.relativeUrl('contentbank/index.php', params, false);\n }).fail(Notification.exception);\n }\n\n /**\n * Rename content in the content bank.\n *\n * @param {int} contentid The content to rename.\n * @param {string} name The new name for the content.\n */\n function renameContent(contentid, name) {\n var request = {\n methodname: 'core_contentbank_rename_content',\n args: {\n contentid: contentid,\n name: name\n }\n };\n var requestType = 'success';\n Ajax.call([request])[0].then(function(data) {\n if (data.result) {\n return 'contentrenamed';\n }\n requestType = 'error';\n return data.warnings[0].message;\n\n }).then(function(message) {\n var params = null;\n if (requestType == 'success') {\n params = {\n id: contentid,\n statusmsg: message\n };\n // Redirect to the content view page and display the message as a notification.\n window.location.href = Url.relativeUrl('contentbank/view.php', params, false);\n } else {\n // Fetch error notifications.\n Notification.addNotification({\n message: message,\n type: 'error'\n });\n Notification.fetchNotifications();\n }\n return;\n }).catch(Notification.exception);\n }\n\n return /** @alias module:core_contentbank/actions */ {\n // Public variables and functions.\n\n /**\n * Initialise the contentbank actions.\n *\n * @method init\n * @return {Actions}\n */\n 'init': function() {\n return new Actions();\n }\n };\n});\n"],"file":"actions.min.js"} \ No newline at end of file +{"version":3,"sources":["../src/actions.js"],"names":["define","$","Ajax","Notification","Str","Templates","Url","ModalFactory","ModalEvents","ACTIONS","DELETE_CONTENT","RENAME_CONTENT","Actions","registerEvents","prototype","click","e","preventDefault","contentname","data","contentuses","contentid","contextid","deleteButtonText","get_strings","key","component","param","name","then","langStrings","modalTitle","modalContent","create","title","body","type","types","SAVE_CANCEL","large","done","modal","setSaveButtonText","getRoot","on","save","deleteContent","hidden","destroy","show","catch","exception","saveButtonText","render","newname","val","trim","renameContent","alert","requestType","call","methodname","args","contentids","result","message","params","statusmsg","errormsg","window","location","href","relativeUrl","fail","warnings","id","addNotification","fetchNotifications"],"mappings":"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","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Module to manage content bank actions, such as delete or rename.\n *\n * @module core_contentbank/actions\n * @package core_contentbank\n * @copyright 2020 Sara Arjona \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine([\n 'jquery',\n 'core/ajax',\n 'core/notification',\n 'core/str',\n 'core/templates',\n 'core/url',\n 'core/modal_factory',\n 'core/modal_events'],\nfunction($, Ajax, Notification, Str, Templates, Url, ModalFactory, ModalEvents) {\n\n /**\n * List of action selectors.\n *\n * @type {{DELETE_CONTENT: string}}\n */\n var ACTIONS = {\n DELETE_CONTENT: '[data-action=\"deletecontent\"]',\n RENAME_CONTENT: '[data-action=\"renamecontent\"]',\n };\n\n /**\n * Actions class.\n */\n var Actions = function() {\n this.registerEvents();\n };\n\n /**\n * Register event listeners.\n */\n Actions.prototype.registerEvents = function() {\n $(ACTIONS.DELETE_CONTENT).click(function(e) {\n e.preventDefault();\n\n var contentname = $(this).data('contentname');\n var contentuses = $(this).data('uses');\n var contentid = $(this).data('contentid');\n var contextid = $(this).data('contextid');\n\n var strings = [\n {\n key: 'deletecontent',\n component: 'core_contentbank'\n },\n {\n key: 'deletecontentconfirm',\n component: 'core_contentbank',\n param: {\n name: contentname,\n }\n },\n {\n key: 'deletecontentconfirmlinked',\n component: 'core_contentbank',\n },\n {\n key: 'delete',\n component: 'core'\n },\n ];\n\n var deleteButtonText = '';\n Str.get_strings(strings).then(function(langStrings) {\n var modalTitle = langStrings[0];\n var modalContent = langStrings[1];\n if (contentuses > 0) {\n modalContent += ' ' + langStrings[2];\n }\n deleteButtonText = langStrings[3];\n\n return ModalFactory.create({\n title: modalTitle,\n body: modalContent,\n type: ModalFactory.types.SAVE_CANCEL,\n large: true\n });\n }).done(function(modal) {\n modal.setSaveButtonText(deleteButtonText);\n modal.getRoot().on(ModalEvents.save, function() {\n // The action is now confirmed, sending an action for it.\n return deleteContent(contentid, contextid);\n });\n\n // Handle hidden event.\n modal.getRoot().on(ModalEvents.hidden, function() {\n // Destroy when hidden.\n modal.destroy();\n });\n\n // Show the modal.\n modal.show();\n\n return;\n }).catch(Notification.exception);\n });\n\n $(ACTIONS.RENAME_CONTENT).click(function(e) {\n e.preventDefault();\n\n var contentname = $(this).data('contentname');\n var contentid = $(this).data('contentid');\n\n var strings = [\n {\n key: 'renamecontent',\n component: 'core_contentbank'\n },\n {\n key: 'rename',\n component: 'core_contentbank'\n },\n ];\n\n var saveButtonText = '';\n Str.get_strings(strings).then(function(langStrings) {\n var modalTitle = langStrings[0];\n saveButtonText = langStrings[1];\n\n return ModalFactory.create({\n title: modalTitle,\n body: Templates.render('core_contentbank/renamecontent', {'contentid': contentid, 'name': contentname}),\n type: ModalFactory.types.SAVE_CANCEL\n });\n }).then(function(modal) {\n modal.setSaveButtonText(saveButtonText);\n modal.getRoot().on(ModalEvents.save, function(e) {\n // The action is now confirmed, sending an action for it.\n var newname = $(\"#newname\").val().trim();\n if (newname) {\n renameContent(contentid, newname);\n } else {\n var errorStrings = [\n {\n key: 'error',\n },\n {\n key: 'emptynamenotallowed',\n component: 'core_contentbank',\n },\n ];\n Str.get_strings(errorStrings).then(function(langStrings) {\n Notification.alert(langStrings[0], langStrings[1]);\n }).catch(Notification.exception);\n e.preventDefault();\n }\n });\n\n // Handle hidden event.\n modal.getRoot().on(ModalEvents.hidden, function() {\n // Destroy when hidden.\n modal.destroy();\n });\n\n // Show the modal.\n modal.show();\n\n return;\n }).catch(Notification.exception);\n });\n };\n\n /**\n * Delete content from the content bank.\n *\n * @param {int} contentid The content to delete.\n * @param {int} contextid The contextid where the content belongs.\n */\n function deleteContent(contentid, contextid) {\n var request = {\n methodname: 'core_contentbank_delete_content',\n args: {\n contentids: {contentid}\n }\n };\n\n var requestType = 'success';\n Ajax.call([request])[0].then(function(data) {\n if (data.result) {\n return 'contentdeleted';\n }\n requestType = 'error';\n return 'contentnotdeleted';\n\n }).done(function(message) {\n var params = {\n contextid: contextid\n };\n if (requestType == 'success') {\n params.statusmsg = message;\n } else {\n params.errormsg = message;\n }\n // Redirect to the main content bank page and display the message as a notification.\n window.location.href = Url.relativeUrl('contentbank/index.php', params, false);\n }).fail(Notification.exception);\n }\n\n /**\n * Rename content in the content bank.\n *\n * @param {int} contentid The content to rename.\n * @param {string} name The new name for the content.\n */\n function renameContent(contentid, name) {\n var request = {\n methodname: 'core_contentbank_rename_content',\n args: {\n contentid: contentid,\n name: name\n }\n };\n var requestType = 'success';\n Ajax.call([request])[0].then(function(data) {\n if (data.result) {\n return 'contentrenamed';\n }\n requestType = 'error';\n return data.warnings[0].message;\n\n }).then(function(message) {\n var params = null;\n if (requestType == 'success') {\n params = {\n id: contentid,\n statusmsg: message\n };\n // Redirect to the content view page and display the message as a notification.\n window.location.href = Url.relativeUrl('contentbank/view.php', params, false);\n } else {\n // Fetch error notifications.\n Notification.addNotification({\n message: message,\n type: 'error'\n });\n Notification.fetchNotifications();\n }\n return;\n }).catch(Notification.exception);\n }\n\n return /** @alias module:core_contentbank/actions */ {\n // Public variables and functions.\n\n /**\n * Initialise the contentbank actions.\n *\n * @method init\n * @return {Actions}\n */\n 'init': function() {\n return new Actions();\n }\n };\n});\n"],"file":"actions.min.js"} \ No newline at end of file diff --git a/contentbank/amd/src/actions.js b/contentbank/amd/src/actions.js index 176b1277c1c..47a5e107a92 100644 --- a/contentbank/amd/src/actions.js +++ b/contentbank/amd/src/actions.js @@ -57,6 +57,7 @@ function($, Ajax, Notification, Str, Templates, Url, ModalFactory, ModalEvents) e.preventDefault(); var contentname = $(this).data('contentname'); + var contentuses = $(this).data('uses'); var contentid = $(this).data('contentid'); var contextid = $(this).data('contextid'); @@ -72,6 +73,10 @@ function($, Ajax, Notification, Str, Templates, Url, ModalFactory, ModalEvents) name: contentname, } }, + { + key: 'deletecontentconfirmlinked', + component: 'core_contentbank', + }, { key: 'delete', component: 'core' @@ -82,7 +87,10 @@ function($, Ajax, Notification, Str, Templates, Url, ModalFactory, ModalEvents) Str.get_strings(strings).then(function(langStrings) { var modalTitle = langStrings[0]; var modalContent = langStrings[1]; - deleteButtonText = langStrings[2]; + if (contentuses > 0) { + modalContent += ' ' + langStrings[2]; + } + deleteButtonText = langStrings[3]; return ModalFactory.create({ title: modalTitle, diff --git a/contentbank/tests/behat/delete_content.feature b/contentbank/tests/behat/delete_content.feature index 9b142b7b3e6..1a41bb6f267 100644 --- a/contentbank/tests/behat/delete_content.feature +++ b/contentbank/tests/behat/delete_content.feature @@ -22,6 +22,7 @@ Feature: Delete H5P file from the content bank And I click on "Choose a file..." "button" And I click on "Private files" "link" in the ".fp-repo-area" "css_element" And I click on "filltheblanks.h5p" "link" + And I set the field "Save as" to "content2delete.h5p" And I click on "Select this file" "button" And I click on "Save changes" "button" @@ -29,15 +30,16 @@ Feature: Delete H5P file from the content bank Given I open the action menu in "region-main-settings-menu" "region" And I should see "Delete" When I choose "Delete" in the open action menu - And I should see "Are you sure you want to delete the content 'filltheblanks.h5p'" + And I should see "Are you sure you want to delete the content 'content2delete.h5p'" + And I should not see "The content will only be deleted from the content bank" And I click on "Cancel" "button" in the "Delete content" "dialogue" - Then I should see "filltheblanks.h5p" + Then I should see "content2delete.h5p" And I open the action menu in "region-main-settings-menu" "region" And I choose "Delete" in the open action menu And I click on "Delete" "button" in the "Delete content" "dialogue" And I wait until the page is ready And I should see "The content has been deleted." - And I should not see "filltheblanks.h5p" + And I should not see "content2delete.h5p" Scenario: Users without the required capability can only delete their own content Given the following "permission overrides" exist: @@ -56,8 +58,8 @@ Feature: Delete H5P file from the content bank And I click on "Save changes" "button" When I click on "Site pages" "list_item" in the "Navigation" "block" And I click on "Content bank" "link" in the "Navigation" "block" - And I should see "filltheblanks.h5p" - And I follow "filltheblanks.h5p" + And I should see "content2delete.h5p" + And I follow "content2delete.h5p" Then ".header-actions-container" "css_element" should not exist And I click on "Content bank" "link" And I click on "Upload" "link" @@ -68,3 +70,23 @@ Feature: Delete H5P file from the content bank And I click on "Save changes" "button" And I open the action menu in "region-main-settings-menu" "region" And I should see "Delete" + + Scenario: The number of times a content is used is displayed before removing it + Given I follow "Dashboard" in the user menu + And I follow "Manage private files..." + And I click on "Add..." "button" + And I select "Content bank" repository in file picker + And I click on "content2delete.h5p" "file" in repository content area + And I click on "Link to the file" "radio" + And I click on "Select this file" "button" + And I click on "Save changes" "button" + When I click on "Site pages" "list_item" in the "Navigation" "block" + And I click on "Content bank" "link" in the "Navigation" "block" + And I follow "content2delete.h5p" + And I open the action menu in "region-main-settings-menu" "region" + And I choose "Delete" in the open action menu + Then I should see "Are you sure you want to delete the content 'content2delete.h5p'" + And I should see "The content will only be deleted from the content bank" + And I click on "Delete" "button" in the "Delete content" "dialogue" + And I should see "The content has been deleted." + And I should not see "content2delete.h5p" diff --git a/contentbank/view.php b/contentbank/view.php index 46daffbc5b4..5e4cf3d2f0b 100644 --- a/contentbank/view.php +++ b/contentbank/view.php @@ -103,6 +103,7 @@ if ($contenttype->can_delete($content)) { $attributes = [ 'data-action' => 'deletecontent', 'data-contentname' => $content->get_name(), + 'data-uses' => count($content->get_uses()), 'data-contentid' => $content->get_id(), 'data-contextid' => $context->id, ]; diff --git a/lang/en/contentbank.php b/lang/en/contentbank.php index 1eb44630634..1c680b45474 100644 --- a/lang/en/contentbank.php +++ b/lang/en/contentbank.php @@ -45,6 +45,7 @@ $string['errordeletingcontentfromcategory'] = 'Error deleting content from categ $string['errornofile'] = 'A compatible file is needed to create content.'; $string['deletecontent'] = 'Delete content'; $string['deletecontentconfirm'] = 'Are you sure you want to delete the content \'{$a->name}\' and all associated files? This action cannot be undone.'; +$string['deletecontentconfirmlinked'] = 'The content will only be deleted from the content bank. Any places which currently link to it will be automatically updated to use a copy of the content instead.'; $string['displaydetails'] = 'Display content bank with file details'; $string['displayicons'] = 'Display content bank with icons'; $string['file'] = 'Upload content'; From 92674c614ad6269fdbc58c0e6b50f584dcae2ad4 Mon Sep 17 00:00:00 2001 From: Sara Arjona Date: Wed, 9 Dec 2020 15:32:59 +0100 Subject: [PATCH 3/3] MDL-70438 contentbank: Add column to view mode for content uses --- contentbank/amd/build/selectors.min.js | 2 +- contentbank/amd/build/selectors.min.js.map | 2 +- contentbank/amd/build/sort.min.js | 2 +- contentbank/amd/build/sort.min.js.map | 2 +- contentbank/amd/src/selectors.js | 1 + contentbank/amd/src/sort.js | 10 +++- contentbank/classes/output/bankcontent.php | 1 + contentbank/templates/bankcontent.mustache | 14 +++++ .../tests/behat/view_preferences.feature | 57 +++++++++++++++---- lang/en/contentbank.php | 1 + theme/boost/scss/moodle/contentbank.scss | 6 +- theme/boost/style/moodle.css | 6 +- theme/classic/style/moodle.css | 6 +- 13 files changed, 89 insertions(+), 21 deletions(-) diff --git a/contentbank/amd/build/selectors.min.js b/contentbank/amd/build/selectors.min.js index 5aba987d6d3..46d90ce697f 100644 --- a/contentbank/amd/build/selectors.min.js +++ b/contentbank/amd/build/selectors.min.js @@ -1,2 +1,2 @@ -define ("core_contentbank/selectors",["exports"],function(a){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;var b=function(a,b){return"[data-".concat(a,"=\"").concat(b,"\"]")},c={regions:{cbcontentname:b("region","cb-content-name"),contentbank:b("region","contentbank"),filearea:b("region","filearea")},actions:{search:b("action","searchcontent"),clearSearch:b("action","clearsearch"),viewgrid:b("action","viewgrid"),viewlist:b("action","viewlist"),sortname:b("action","sortname"),sortdate:b("action","sortdate"),sortsize:b("action","sortsize"),sorttype:b("action","sorttype"),sortauthor:b("action","sortauthor")},elements:{listitem:".cb-listitem",cbnavbarbreadcrumb:".cb-navbar-breadbrumb",cbnavbartotalsearch:".cb-navbar-totalsearch",searchinput:"#searchinput",sortbutton:".cb-btnsort"}};a.default=c;return a.default}); +define ("core_contentbank/selectors",["exports"],function(a){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;var b=function(a,b){return"[data-".concat(a,"=\"").concat(b,"\"]")},c={regions:{cbcontentname:b("region","cb-content-name"),contentbank:b("region","contentbank"),filearea:b("region","filearea")},actions:{search:b("action","searchcontent"),clearSearch:b("action","clearsearch"),viewgrid:b("action","viewgrid"),viewlist:b("action","viewlist"),sortname:b("action","sortname"),sortuses:b("action","sortuses"),sortdate:b("action","sortdate"),sortsize:b("action","sortsize"),sorttype:b("action","sorttype"),sortauthor:b("action","sortauthor")},elements:{listitem:".cb-listitem",cbnavbarbreadcrumb:".cb-navbar-breadbrumb",cbnavbartotalsearch:".cb-navbar-totalsearch",searchinput:"#searchinput",sortbutton:".cb-btnsort"}};a.default=c;return a.default}); //# sourceMappingURL=selectors.min.js.map diff --git a/contentbank/amd/build/selectors.min.js.map b/contentbank/amd/build/selectors.min.js.map index 62be4440b62..9b352d889f1 100644 --- a/contentbank/amd/build/selectors.min.js.map +++ b/contentbank/amd/build/selectors.min.js.map @@ -1 +1 @@ -{"version":3,"sources":["../src/selectors.js"],"names":["getDataSelector","name","value","regions","cbcontentname","contentbank","filearea","actions","search","clearSearch","viewgrid","viewlist","sortname","sortdate","sortsize","sorttype","sortauthor","elements","listitem","cbnavbarbreadcrumb","cbnavbartotalsearch","searchinput","sortbutton"],"mappings":"+IAgCMA,CAAAA,CAAe,CAAG,SAACC,CAAD,CAAOC,CAAP,CAAiB,CACrC,sBAAgBD,CAAhB,eAAyBC,CAAzB,OACH,C,GAEc,CACXC,OAAO,CAAE,CACLC,aAAa,CAAEJ,CAAe,CAAC,QAAD,CAAW,iBAAX,CADzB,CAELK,WAAW,CAAEL,CAAe,CAAC,QAAD,CAAW,aAAX,CAFvB,CAGLM,QAAQ,CAAEN,CAAe,CAAC,QAAD,CAAW,UAAX,CAHpB,CADE,CAMXO,OAAO,CAAE,CACLC,MAAM,CAAER,CAAe,CAAC,QAAD,CAAW,eAAX,CADlB,CAELS,WAAW,CAAET,CAAe,CAAC,QAAD,CAAW,aAAX,CAFvB,CAGLU,QAAQ,CAAEV,CAAe,CAAC,QAAD,CAAW,UAAX,CAHpB,CAILW,QAAQ,CAAEX,CAAe,CAAC,QAAD,CAAW,UAAX,CAJpB,CAKLY,QAAQ,CAAEZ,CAAe,CAAC,QAAD,CAAW,UAAX,CALpB,CAMLa,QAAQ,CAAEb,CAAe,CAAC,QAAD,CAAW,UAAX,CANpB,CAOLc,QAAQ,CAAEd,CAAe,CAAC,QAAD,CAAW,UAAX,CAPpB,CAQLe,QAAQ,CAAEf,CAAe,CAAC,QAAD,CAAW,UAAX,CARpB,CASLgB,UAAU,CAAEhB,CAAe,CAAC,QAAD,CAAW,YAAX,CATtB,CANE,CAiBXiB,QAAQ,CAAE,CACNC,QAAQ,CAAE,cADJ,CAENC,kBAAkB,CAAE,uBAFd,CAGNC,mBAAmB,CAAE,wBAHf,CAINC,WAAW,CAAE,cAJP,CAKNC,UAAU,CAAE,aALN,CAjBC,C","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Define all of the selectors we will be using on the contentbank interface.\n *\n * @module core_contentbank/selectors\n * @package core_contentbank\n * @copyright 2020 Sara Arjona \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/**\n * A small helper function to build queryable data selectors.\n *\n * @method getDataSelector\n * @param {String} name\n * @param {String} value\n * @return {string}\n */\nconst getDataSelector = (name, value) => {\n return `[data-${name}=\"${value}\"]`;\n};\n\nexport default {\n regions: {\n cbcontentname: getDataSelector('region', 'cb-content-name'),\n contentbank: getDataSelector('region', 'contentbank'),\n filearea: getDataSelector('region', 'filearea')\n },\n actions: {\n search: getDataSelector('action', 'searchcontent'),\n clearSearch: getDataSelector('action', 'clearsearch'),\n viewgrid: getDataSelector('action', 'viewgrid'),\n viewlist: getDataSelector('action', 'viewlist'),\n sortname: getDataSelector('action', 'sortname'),\n sortdate: getDataSelector('action', 'sortdate'),\n sortsize: getDataSelector('action', 'sortsize'),\n sorttype: getDataSelector('action', 'sorttype'),\n sortauthor: getDataSelector('action', 'sortauthor'),\n },\n elements: {\n listitem: '.cb-listitem',\n cbnavbarbreadcrumb: '.cb-navbar-breadbrumb',\n cbnavbartotalsearch: '.cb-navbar-totalsearch',\n searchinput: '#searchinput',\n sortbutton: '.cb-btnsort'\n },\n};\n"],"file":"selectors.min.js"} \ No newline at end of file +{"version":3,"sources":["../src/selectors.js"],"names":["getDataSelector","name","value","regions","cbcontentname","contentbank","filearea","actions","search","clearSearch","viewgrid","viewlist","sortname","sortuses","sortdate","sortsize","sorttype","sortauthor","elements","listitem","cbnavbarbreadcrumb","cbnavbartotalsearch","searchinput","sortbutton"],"mappings":"+IAgCMA,CAAAA,CAAe,CAAG,SAACC,CAAD,CAAOC,CAAP,CAAiB,CACrC,sBAAgBD,CAAhB,eAAyBC,CAAzB,OACH,C,GAEc,CACXC,OAAO,CAAE,CACLC,aAAa,CAAEJ,CAAe,CAAC,QAAD,CAAW,iBAAX,CADzB,CAELK,WAAW,CAAEL,CAAe,CAAC,QAAD,CAAW,aAAX,CAFvB,CAGLM,QAAQ,CAAEN,CAAe,CAAC,QAAD,CAAW,UAAX,CAHpB,CADE,CAMXO,OAAO,CAAE,CACLC,MAAM,CAAER,CAAe,CAAC,QAAD,CAAW,eAAX,CADlB,CAELS,WAAW,CAAET,CAAe,CAAC,QAAD,CAAW,aAAX,CAFvB,CAGLU,QAAQ,CAAEV,CAAe,CAAC,QAAD,CAAW,UAAX,CAHpB,CAILW,QAAQ,CAAEX,CAAe,CAAC,QAAD,CAAW,UAAX,CAJpB,CAKLY,QAAQ,CAAEZ,CAAe,CAAC,QAAD,CAAW,UAAX,CALpB,CAMLa,QAAQ,CAAEb,CAAe,CAAC,QAAD,CAAW,UAAX,CANpB,CAOLc,QAAQ,CAAEd,CAAe,CAAC,QAAD,CAAW,UAAX,CAPpB,CAQLe,QAAQ,CAAEf,CAAe,CAAC,QAAD,CAAW,UAAX,CARpB,CASLgB,QAAQ,CAAEhB,CAAe,CAAC,QAAD,CAAW,UAAX,CATpB,CAULiB,UAAU,CAAEjB,CAAe,CAAC,QAAD,CAAW,YAAX,CAVtB,CANE,CAkBXkB,QAAQ,CAAE,CACNC,QAAQ,CAAE,cADJ,CAENC,kBAAkB,CAAE,uBAFd,CAGNC,mBAAmB,CAAE,wBAHf,CAINC,WAAW,CAAE,cAJP,CAKNC,UAAU,CAAE,aALN,CAlBC,C","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Define all of the selectors we will be using on the contentbank interface.\n *\n * @module core_contentbank/selectors\n * @package core_contentbank\n * @copyright 2020 Sara Arjona \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/**\n * A small helper function to build queryable data selectors.\n *\n * @method getDataSelector\n * @param {String} name\n * @param {String} value\n * @return {string}\n */\nconst getDataSelector = (name, value) => {\n return `[data-${name}=\"${value}\"]`;\n};\n\nexport default {\n regions: {\n cbcontentname: getDataSelector('region', 'cb-content-name'),\n contentbank: getDataSelector('region', 'contentbank'),\n filearea: getDataSelector('region', 'filearea')\n },\n actions: {\n search: getDataSelector('action', 'searchcontent'),\n clearSearch: getDataSelector('action', 'clearsearch'),\n viewgrid: getDataSelector('action', 'viewgrid'),\n viewlist: getDataSelector('action', 'viewlist'),\n sortname: getDataSelector('action', 'sortname'),\n sortuses: getDataSelector('action', 'sortuses'),\n sortdate: getDataSelector('action', 'sortdate'),\n sortsize: getDataSelector('action', 'sortsize'),\n sorttype: getDataSelector('action', 'sorttype'),\n sortauthor: getDataSelector('action', 'sortauthor'),\n },\n elements: {\n listitem: '.cb-listitem',\n cbnavbarbreadcrumb: '.cb-navbar-breadbrumb',\n cbnavbartotalsearch: '.cb-navbar-totalsearch',\n searchinput: '#searchinput',\n sortbutton: '.cb-btnsort'\n },\n};\n"],"file":"selectors.min.js"} \ No newline at end of file diff --git a/contentbank/amd/build/sort.min.js b/contentbank/amd/build/sort.min.js index 4660de4a485..dcbccf7f874 100644 --- a/contentbank/amd/build/sort.min.js +++ b/contentbank/amd/build/sort.min.js @@ -1,2 +1,2 @@ -define ("core_contentbank/sort",["exports","./selectors","core/str","core/prefetch","core/ajax","core/notification"],function(a,b,c,d,e,f){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.init=void 0;b=g(b);d=g(d);e=g(e);f=g(f);function g(a){return a&&a.__esModule?a:{default:a}}var h=function(){var a=document.querySelector(b.default.regions.contentbank);d.default.prefetchStrings("contentbank",["contentname","lastmodified","size","type","author"]);d.default.prefetchStrings("moodle",["sortbyx","sortbyxreverse"]);i(a)};a.init=h;var i=function(a){a.addEventListener("click",function(c){var d=a.querySelector(b.default.actions.viewlist),e=a.querySelector(b.default.actions.viewgrid);if(c.target.closest(b.default.actions.viewgrid)){a.classList.remove("view-list");a.classList.add("view-grid");e.classList.add("active");d.classList.remove("active");j(!1);return}if(c.target.closest(b.default.actions.viewlist)){a.classList.remove("view-grid");a.classList.add("view-list");d.classList.add("active");e.classList.remove("active");j(!0);return}var f=document.querySelector(b.default.regions.filearea),g=f.querySelectorAll(b.default.elements.listitem);if(f&&g){var h=c.target.closest(b.default.actions.sortname);if(h){var i=k(a,h);m(f,g,"data-file",i);return}var l=c.target.closest(b.default.actions.sortdate);if(l){var n=k(a,l);m(f,g,"data-timemodified",n);return}var o=c.target.closest(b.default.actions.sortsize);if(o){var p=k(a,o);m(f,g,"data-bytes",p);return}var q=c.target.closest(b.default.actions.sorttype);if(q){var r=k(a,q);m(f,g,"data-type",r);return}var s=c.target.closest(b.default.actions.sortauthor);if(s){var t=k(a,s);m(f,g,"data-author",t)}}})},j=function(a){if(!1===a){a=null}var b={methodname:"core_user_update_user_preferences",args:{preferences:[{type:"core_contentbank_view_list",value:a}]}};return e.default.call([b])[0].catch(f.default.exception)},k=function(a,c){var d=a.querySelectorAll(b.default.elements.sortbutton);d.forEach(function(a){if(a!==c){a.classList.remove("dir-asc");a.classList.remove("dir-desc");a.classList.add("dir-none");l(a,!1)}});var e=!0;if(c.classList.contains("dir-none")){c.classList.remove("dir-none");c.classList.add("dir-asc")}else if(c.classList.contains("dir-asc")){c.classList.remove("dir-asc");c.classList.add("dir-desc");e=!1}else if(c.classList.contains("dir-desc")){c.classList.remove("dir-desc");c.classList.add("dir-asc")}l(c,e);return e},l=function(a,b){var d=b?"sortbyxreverse":"sortbyx";return(0,c.get_string)(a.dataset.string,"contentbank").then(function(a){return(0,c.get_string)(d,"core",a)}).then(function(b){a.setAttribute("title",b);return b}).catch()},m=function(a,b,c,d){var e=[].slice.call(b).sort(function(e,a){var b=e.getAttribute(c),f=a.getAttribute(c);if(!isNaN(b)){b=parseInt(b);f=parseInt(f)}if(d){return b>f?1:-1}else{return bf?1:-1}else{return b.\n\n/**\n * Content bank UI actions.\n *\n * @module core_contentbank/sort\n * @package core_contentbank\n * @copyright 2020 Bas Brands \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport selectors from './selectors';\nimport {get_string as getString} from 'core/str';\nimport Prefetch from 'core/prefetch';\nimport Ajax from 'core/ajax';\nimport Notification from 'core/notification';\n\n/**\n * Set up the contentbank views.\n *\n * @method init\n */\nexport const init = () => {\n const contentBank = document.querySelector(selectors.regions.contentbank);\n Prefetch.prefetchStrings('contentbank', ['contentname', 'lastmodified', 'size', 'type', 'author']);\n Prefetch.prefetchStrings('moodle', ['sortbyx', 'sortbyxreverse']);\n registerListenerEvents(contentBank);\n};\n\n/**\n * Register contentbank related event listeners.\n *\n * @method registerListenerEvents\n * @param {HTMLElement} contentBank The DOM node of the content bank\n */\nconst registerListenerEvents = (contentBank) => {\n\n contentBank.addEventListener('click', e => {\n const viewList = contentBank.querySelector(selectors.actions.viewlist);\n const viewGrid = contentBank.querySelector(selectors.actions.viewgrid);\n\n // View as Grid button.\n if (e.target.closest(selectors.actions.viewgrid)) {\n contentBank.classList.remove('view-list');\n contentBank.classList.add('view-grid');\n viewGrid.classList.add('active');\n viewList.classList.remove('active');\n setViewListPreference(false);\n\n return;\n }\n\n // View as List button.\n if (e.target.closest(selectors.actions.viewlist)) {\n contentBank.classList.remove('view-grid');\n contentBank.classList.add('view-list');\n viewList.classList.add('active');\n viewGrid.classList.remove('active');\n setViewListPreference(true);\n\n return;\n }\n\n // TODO: This should _not_ use `document`. Every query should be constrained to the content bank container.\n const fileArea = document.querySelector(selectors.regions.filearea);\n const shownItems = fileArea.querySelectorAll(selectors.elements.listitem);\n\n if (fileArea && shownItems) {\n\n // Sort by file name alphabetical\n const sortByName = e.target.closest(selectors.actions.sortname);\n if (sortByName) {\n const ascending = updateSortButtons(contentBank, sortByName);\n updateSortOrder(fileArea, shownItems, 'data-file', ascending);\n return;\n }\n\n // Sort by date.\n const sortByDate = e.target.closest(selectors.actions.sortdate);\n if (sortByDate) {\n const ascending = updateSortButtons(contentBank, sortByDate);\n updateSortOrder(fileArea, shownItems, 'data-timemodified', ascending);\n return;\n }\n\n // Sort by size.\n const sortBySize = e.target.closest(selectors.actions.sortsize);\n if (sortBySize) {\n const ascending = updateSortButtons(contentBank, sortBySize);\n updateSortOrder(fileArea, shownItems, 'data-bytes', ascending);\n return;\n }\n\n // Sort by type.\n const sortByType = e.target.closest(selectors.actions.sorttype);\n if (sortByType) {\n const ascending = updateSortButtons(contentBank, sortByType);\n updateSortOrder(fileArea, shownItems, 'data-type', ascending);\n return;\n }\n\n // Sort by author.\n const sortByAuthor = e.target.closest(selectors.actions.sortauthor);\n if (sortByAuthor) {\n const ascending = updateSortButtons(contentBank, sortByAuthor);\n updateSortOrder(fileArea, shownItems, 'data-author', ascending);\n }\n return;\n }\n });\n};\n\n\n/**\n * Set the contentbank user preference in list view\n *\n * @param {Bool} viewList view ContentBank as list.\n * @return {Promise} Repository promise.\n */\nconst setViewListPreference = function(viewList) {\n\n // If the given status is not hidden, the preference has to be deleted with a null value.\n if (viewList === false) {\n viewList = null;\n }\n\n const request = {\n methodname: 'core_user_update_user_preferences',\n args: {\n preferences: [\n {\n type: 'core_contentbank_view_list',\n value: viewList\n }\n ]\n }\n };\n\n return Ajax.call([request])[0].catch(Notification.exception);\n};\n\n/**\n * Update the sort button view.\n *\n * @method updateSortButtons\n * @param {HTMLElement} contentBank The DOM node of the contentbank button\n * @param {HTMLElement} sortButton The DOM node of the sort button\n * @return {Bool} sort ascending\n */\nconst updateSortButtons = (contentBank, sortButton) => {\n const sortButtons = contentBank.querySelectorAll(selectors.elements.sortbutton);\n\n sortButtons.forEach((button) => {\n if (button !== sortButton) {\n button.classList.remove('dir-asc');\n button.classList.remove('dir-desc');\n button.classList.add('dir-none');\n\n updateButtonTitle(button, false);\n }\n });\n\n let ascending = true;\n\n if (sortButton.classList.contains('dir-none')) {\n sortButton.classList.remove('dir-none');\n sortButton.classList.add('dir-asc');\n } else if (sortButton.classList.contains('dir-asc')) {\n sortButton.classList.remove('dir-asc');\n sortButton.classList.add('dir-desc');\n ascending = false;\n } else if (sortButton.classList.contains('dir-desc')) {\n sortButton.classList.remove('dir-desc');\n sortButton.classList.add('dir-asc');\n }\n\n updateButtonTitle(sortButton, ascending);\n\n return ascending;\n};\n\n/**\n * Update the button title.\n *\n * @method updateButtonTitle\n * @param {HTMLElement} button Button to update\n * @param {Bool} ascending Sort direction\n * @return {Promise} string promise\n */\nconst updateButtonTitle = (button, ascending) => {\n\n const sortString = (ascending ? 'sortbyxreverse' : 'sortbyx');\n\n return getString(button.dataset.string, 'contentbank')\n .then(columnName => {\n return getString(sortString, 'core', columnName);\n })\n .then(sortByString => {\n button.setAttribute('title', sortByString);\n return sortByString;\n })\n .catch();\n};\n\n/**\n * Update the sort order of the itemlist and update the DOM\n *\n * @method updateSortOrder\n * @param {HTMLElement} fileArea the Dom container for the itemlist\n * @param {Array} itemList Nodelist of Dom elements\n * @param {String} attribute, the attribut to sort on\n * @param {Bool} ascending, Sort Ascending\n */\nconst updateSortOrder = (fileArea, itemList, attribute, ascending) => {\n const sortList = [].slice.call(itemList).sort(function(a, b) {\n\n let aa = a.getAttribute(attribute);\n let bb = b.getAttribute(attribute);\n if (!isNaN(aa)) {\n aa = parseInt(aa);\n bb = parseInt(bb);\n }\n\n if (ascending) {\n return aa > bb ? 1 : -1;\n } else {\n return aa < bb ? 1 : -1;\n }\n });\n sortList.forEach(listItem => fileArea.appendChild(listItem));\n};\n"],"file":"sort.min.js"} \ No newline at end of file +{"version":3,"sources":["../src/sort.js"],"names":["init","contentBank","document","querySelector","selectors","regions","contentbank","Prefetch","prefetchStrings","registerListenerEvents","addEventListener","e","viewList","actions","viewlist","viewGrid","viewgrid","target","closest","classList","remove","add","setViewListPreference","fileArea","filearea","shownItems","querySelectorAll","elements","listitem","sortByName","sortname","ascending","updateSortButtons","updateSortOrder","sortByUses","sortuses","sortByDate","sortdate","sortBySize","sortsize","sortByType","sorttype","sortByAuthor","sortauthor","request","methodname","args","preferences","type","value","Ajax","call","catch","Notification","exception","sortButton","sortButtons","sortbutton","forEach","button","updateButtonTitle","contains","sortString","dataset","string","then","columnName","sortByString","setAttribute","itemList","attribute","sortList","slice","sort","a","b","aa","getAttribute","bb","isNaN","parseInt","listItem","appendChild"],"mappings":"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","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Content bank UI actions.\n *\n * @module core_contentbank/sort\n * @package core_contentbank\n * @copyright 2020 Bas Brands \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport selectors from './selectors';\nimport {get_string as getString} from 'core/str';\nimport Prefetch from 'core/prefetch';\nimport Ajax from 'core/ajax';\nimport Notification from 'core/notification';\n\n/**\n * Set up the contentbank views.\n *\n * @method init\n */\nexport const init = () => {\n const contentBank = document.querySelector(selectors.regions.contentbank);\n Prefetch.prefetchStrings('contentbank', ['contentname', 'uses', 'lastmodified', 'size', 'type', 'author']);\n Prefetch.prefetchStrings('moodle', ['sortbyx', 'sortbyxreverse']);\n registerListenerEvents(contentBank);\n};\n\n/**\n * Register contentbank related event listeners.\n *\n * @method registerListenerEvents\n * @param {HTMLElement} contentBank The DOM node of the content bank\n */\nconst registerListenerEvents = (contentBank) => {\n\n contentBank.addEventListener('click', e => {\n const viewList = contentBank.querySelector(selectors.actions.viewlist);\n const viewGrid = contentBank.querySelector(selectors.actions.viewgrid);\n\n // View as Grid button.\n if (e.target.closest(selectors.actions.viewgrid)) {\n contentBank.classList.remove('view-list');\n contentBank.classList.add('view-grid');\n viewGrid.classList.add('active');\n viewList.classList.remove('active');\n setViewListPreference(false);\n\n return;\n }\n\n // View as List button.\n if (e.target.closest(selectors.actions.viewlist)) {\n contentBank.classList.remove('view-grid');\n contentBank.classList.add('view-list');\n viewList.classList.add('active');\n viewGrid.classList.remove('active');\n setViewListPreference(true);\n\n return;\n }\n\n // TODO: This should _not_ use `document`. Every query should be constrained to the content bank container.\n const fileArea = document.querySelector(selectors.regions.filearea);\n const shownItems = fileArea.querySelectorAll(selectors.elements.listitem);\n\n if (fileArea && shownItems) {\n\n // Sort by file name alphabetical\n const sortByName = e.target.closest(selectors.actions.sortname);\n if (sortByName) {\n const ascending = updateSortButtons(contentBank, sortByName);\n updateSortOrder(fileArea, shownItems, 'data-file', ascending);\n return;\n }\n\n // Sort by uses.\n const sortByUses = e.target.closest(selectors.actions.sortuses);\n if (sortByUses) {\n const ascending = updateSortButtons(contentBank, sortByUses);\n updateSortOrder(fileArea, shownItems, 'data-uses', ascending);\n return;\n }\n\n // Sort by date.\n const sortByDate = e.target.closest(selectors.actions.sortdate);\n if (sortByDate) {\n const ascending = updateSortButtons(contentBank, sortByDate);\n updateSortOrder(fileArea, shownItems, 'data-timemodified', ascending);\n return;\n }\n\n // Sort by size.\n const sortBySize = e.target.closest(selectors.actions.sortsize);\n if (sortBySize) {\n const ascending = updateSortButtons(contentBank, sortBySize);\n updateSortOrder(fileArea, shownItems, 'data-bytes', ascending);\n return;\n }\n\n // Sort by type.\n const sortByType = e.target.closest(selectors.actions.sorttype);\n if (sortByType) {\n const ascending = updateSortButtons(contentBank, sortByType);\n updateSortOrder(fileArea, shownItems, 'data-type', ascending);\n return;\n }\n\n // Sort by author.\n const sortByAuthor = e.target.closest(selectors.actions.sortauthor);\n if (sortByAuthor) {\n const ascending = updateSortButtons(contentBank, sortByAuthor);\n updateSortOrder(fileArea, shownItems, 'data-author', ascending);\n }\n return;\n }\n });\n};\n\n\n/**\n * Set the contentbank user preference in list view\n *\n * @param {Bool} viewList view ContentBank as list.\n * @return {Promise} Repository promise.\n */\nconst setViewListPreference = function(viewList) {\n\n // If the given status is not hidden, the preference has to be deleted with a null value.\n if (viewList === false) {\n viewList = null;\n }\n\n const request = {\n methodname: 'core_user_update_user_preferences',\n args: {\n preferences: [\n {\n type: 'core_contentbank_view_list',\n value: viewList\n }\n ]\n }\n };\n\n return Ajax.call([request])[0].catch(Notification.exception);\n};\n\n/**\n * Update the sort button view.\n *\n * @method updateSortButtons\n * @param {HTMLElement} contentBank The DOM node of the contentbank button\n * @param {HTMLElement} sortButton The DOM node of the sort button\n * @return {Bool} sort ascending\n */\nconst updateSortButtons = (contentBank, sortButton) => {\n const sortButtons = contentBank.querySelectorAll(selectors.elements.sortbutton);\n\n sortButtons.forEach((button) => {\n if (button !== sortButton) {\n button.classList.remove('dir-asc');\n button.classList.remove('dir-desc');\n button.classList.add('dir-none');\n\n updateButtonTitle(button, false);\n }\n });\n\n let ascending = true;\n\n if (sortButton.classList.contains('dir-none')) {\n sortButton.classList.remove('dir-none');\n sortButton.classList.add('dir-asc');\n } else if (sortButton.classList.contains('dir-asc')) {\n sortButton.classList.remove('dir-asc');\n sortButton.classList.add('dir-desc');\n ascending = false;\n } else if (sortButton.classList.contains('dir-desc')) {\n sortButton.classList.remove('dir-desc');\n sortButton.classList.add('dir-asc');\n }\n\n updateButtonTitle(sortButton, ascending);\n\n return ascending;\n};\n\n/**\n * Update the button title.\n *\n * @method updateButtonTitle\n * @param {HTMLElement} button Button to update\n * @param {Bool} ascending Sort direction\n * @return {Promise} string promise\n */\nconst updateButtonTitle = (button, ascending) => {\n\n const sortString = (ascending ? 'sortbyxreverse' : 'sortbyx');\n\n return getString(button.dataset.string, 'contentbank')\n .then(columnName => {\n return getString(sortString, 'core', columnName);\n })\n .then(sortByString => {\n button.setAttribute('title', sortByString);\n return sortByString;\n })\n .catch();\n};\n\n/**\n * Update the sort order of the itemlist and update the DOM\n *\n * @method updateSortOrder\n * @param {HTMLElement} fileArea the Dom container for the itemlist\n * @param {Array} itemList Nodelist of Dom elements\n * @param {String} attribute, the attribut to sort on\n * @param {Bool} ascending, Sort Ascending\n */\nconst updateSortOrder = (fileArea, itemList, attribute, ascending) => {\n const sortList = [].slice.call(itemList).sort(function(a, b) {\n\n let aa = a.getAttribute(attribute);\n let bb = b.getAttribute(attribute);\n if (!isNaN(aa)) {\n aa = parseInt(aa);\n bb = parseInt(bb);\n }\n\n if (ascending) {\n return aa > bb ? 1 : -1;\n } else {\n return aa < bb ? 1 : -1;\n }\n });\n sortList.forEach(listItem => fileArea.appendChild(listItem));\n};\n"],"file":"sort.min.js"} \ No newline at end of file diff --git a/contentbank/amd/src/selectors.js b/contentbank/amd/src/selectors.js index f6de1b2a0fd..e311ea846e7 100644 --- a/contentbank/amd/src/selectors.js +++ b/contentbank/amd/src/selectors.js @@ -46,6 +46,7 @@ export default { viewgrid: getDataSelector('action', 'viewgrid'), viewlist: getDataSelector('action', 'viewlist'), sortname: getDataSelector('action', 'sortname'), + sortuses: getDataSelector('action', 'sortuses'), sortdate: getDataSelector('action', 'sortdate'), sortsize: getDataSelector('action', 'sortsize'), sorttype: getDataSelector('action', 'sorttype'), diff --git a/contentbank/amd/src/sort.js b/contentbank/amd/src/sort.js index ed67de5233f..2c80c2ed6b9 100644 --- a/contentbank/amd/src/sort.js +++ b/contentbank/amd/src/sort.js @@ -35,7 +35,7 @@ import Notification from 'core/notification'; */ export const init = () => { const contentBank = document.querySelector(selectors.regions.contentbank); - Prefetch.prefetchStrings('contentbank', ['contentname', 'lastmodified', 'size', 'type', 'author']); + Prefetch.prefetchStrings('contentbank', ['contentname', 'uses', 'lastmodified', 'size', 'type', 'author']); Prefetch.prefetchStrings('moodle', ['sortbyx', 'sortbyxreverse']); registerListenerEvents(contentBank); }; @@ -88,6 +88,14 @@ const registerListenerEvents = (contentBank) => { return; } + // Sort by uses. + const sortByUses = e.target.closest(selectors.actions.sortuses); + if (sortByUses) { + const ascending = updateSortButtons(contentBank, sortByUses); + updateSortOrder(fileArea, shownItems, 'data-uses', ascending); + return; + } + // Sort by date. const sortByDate = e.target.closest(selectors.actions.sortdate); if (sortByDate) { diff --git a/contentbank/classes/output/bankcontent.php b/contentbank/classes/output/bankcontent.php index 6c799df38e3..edd6356599e 100644 --- a/contentbank/classes/output/bankcontent.php +++ b/contentbank/classes/output/bankcontent.php @@ -92,6 +92,7 @@ class bankcontent implements renderable, templatable { 'title' => strtolower($name), 'link' => $contenttype->get_view_url($content), 'icon' => $contenttype->get_icon($content), + 'uses' => count($content->get_uses()), 'timemodified' => $content->get_timemodified(), 'bytes' => $filesize, 'size' => display_size($filesize), diff --git a/contentbank/templates/bankcontent.mustache b/contentbank/templates/bankcontent.mustache index e0b2095a928..50d1e6d5e8e 100644 --- a/contentbank/templates/bankcontent.mustache +++ b/contentbank/templates/bankcontent.mustache @@ -36,6 +36,7 @@ "title": "resume", "timemodified": 1589792039, "size": "699.3KB", + "uses": 3, "bytes": 716126, "type": "Archive (PDF)", "author": "Admin user", @@ -107,6 +108,15 @@ data-region="contentbank"> {{#pix}} t/sort_asc, core, {{#str}}asc, core{{/str}} {{/pix}} +
+
{{#str}} uses, contentbank {{/str}}
+ +
{{#str}} lastmodified, contentbank {{/str}}
+
+ {{ uses }} +
{{#userdate}} {{ timemodified }}, {{#str}} strftimedatetimeshort, core_langconfig {{/str}} {{/userdate}}
diff --git a/contentbank/tests/behat/view_preferences.feature b/contentbank/tests/behat/view_preferences.feature index 44fa8bfc969..8e3d2d7423c 100644 --- a/contentbank/tests/behat/view_preferences.feature +++ b/contentbank/tests/behat/view_preferences.feature @@ -1,28 +1,65 @@ -@core @core_contentbank @contentbank_h5p @javascript +@core @core_contentbank @contentbank_h5p @_file_upload @javascript Feature: Store the content bank view preference In order to consistantly view the content bank in icons or details view As an admin I need to be able to store my view preference Background: - Given the following "contentbank content" exist: - | contextlevel | reference | contenttype | user | contentname | - | System | | contenttype_h5p | admin | filltheblanks.h5p | - | System | | contenttype_h5p | admin | mathsbook.h5p | - - Scenario: Admins can order content in the content bank Given I log in as "admin" + And I follow "Manage private files..." + And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "Files" filemanager + And I upload "h5p/tests/fixtures/greeting-card-887.h5p" file to "Files" filemanager + And I click on "Save changes" "button" And I am on site homepage And I turn editing mode on And I add the "Navigation" block if not present + And I configure the "Navigation" block + And I set the following fields to these values: + | Page contexts | Display throughout the entire site | + And I press "Save changes" And I expand "Site pages" node And I click on "Content bank" "link" + And I click on "Upload" "link" + And I click on "Choose a file..." "button" + And I click on "Private files" "link" in the ".fp-repo-area" "css_element" + And I click on "filltheblanks.h5p" "link" + And I set the field "Save as" to "fib.h5p" + And I click on "Select this file" "button" + And I click on "Save changes" "button" + And I click on "Content bank" "link" + And I click on "Upload" "link" + And I click on "Choose a file..." "button" + And I click on "Private files" "link" in the ".fp-repo-area" "css_element" + And I click on "greeting-card-887.h5p" "link" + And I set the field "Save as" to "greetingcard.h5p" + And I click on "Select this file" "button" + And I click on "Save changes" "button" + + Scenario: There are several views for displaying contents into the content bank + Given I am on site homepage + And I click on "Content bank" "link" When I click on "Display content bank with file details" "button" - And I should see "Last modified" - And I follow "filltheblanks.h5p" + Then I should see "Last modified" + And I follow "greetingcard.h5p" And I click on "Content bank" "link" And I should see "Last modified" And I click on "Display content bank with icons" "button" - And I follow "filltheblanks.h5p" + And I follow "greetingcard.h5p" And I click on "Content bank" "link" And I should not see "Last modified" + + Scenario: Display the number of times a content is used in file details view + Given I follow "Dashboard" in the user menu + And I follow "Manage private files..." + And I click on "Add..." "button" + And I select "Content bank" repository in file picker + And I click on "fib.h5p" "file" in repository content area + And I click on "Link to the file" "radio" + And I click on "Select this file" "button" + And I click on "Save changes" "button" + When I am on site homepage + And I expand "Site pages" node + And I click on "Content bank" "link" + And I click on "Display content bank with file details" "button" + Then I should see "1" in the "[data-file='fib.h5p'] .cb-uses" "css_element" + And I should see "0" in the "[data-file='greetingcard.h5p'] .cb-uses" "css_element" diff --git a/lang/en/contentbank.php b/lang/en/contentbank.php index 1c680b45474..648a47429cd 100644 --- a/lang/en/contentbank.php +++ b/lang/en/contentbank.php @@ -75,3 +75,4 @@ $string['timecreated'] = 'Time created'; $string['type'] = 'Type'; $string['unsupported'] = 'This content type is not supported.'; $string['upload'] = 'Upload'; +$string['uses'] = 'Places linked'; diff --git a/theme/boost/scss/moodle/contentbank.scss b/theme/boost/scss/moodle/contentbank.scss index 8bed4e2a49a..b56ca61d3ae 100644 --- a/theme/boost/scss/moodle/contentbank.scss +++ b/theme/boost/scss/moodle/contentbank.scss @@ -45,6 +45,7 @@ margin-bottom: 0.5rem; } .cb-heading, + .cb-uses, .cb-date, .cb-size, .cb-type, @@ -99,12 +100,13 @@ flex: 0 0 40%; max-width: 40%; } + .cb-uses, .cb-date, .cb-size, .cb-type, .cb-author { - flex: 0 0 15%; - max-width: 15%; + flex: 0 0 12%; + max-width: 12%; } .cb-column.last { border-right: 0; diff --git a/theme/boost/style/moodle.css b/theme/boost/style/moodle.css index 045bdff6315..e3a76469231 100644 --- a/theme/boost/style/moodle.css +++ b/theme/boost/style/moodle.css @@ -13135,6 +13135,7 @@ table.calendartable caption { margin-bottom: 0.5rem; } .content-bank-container.view-grid .cb-heading, +.content-bank-container.view-grid .cb-uses, .content-bank-container.view-grid .cb-date, .content-bank-container.view-grid .cb-size, .content-bank-container.view-grid .cb-type, @@ -13176,12 +13177,13 @@ table.calendartable caption { .content-bank-container.view-list .cb-file { flex: 0 0 40%; max-width: 40%; } + .content-bank-container.view-list .cb-uses, .content-bank-container.view-list .cb-date, .content-bank-container.view-list .cb-size, .content-bank-container.view-list .cb-type, .content-bank-container.view-list .cb-author { - flex: 0 0 15%; - max-width: 15%; } + flex: 0 0 12%; + max-width: 12%; } .content-bank-container.view-list .cb-column.last { border-right: 0; } } diff --git a/theme/classic/style/moodle.css b/theme/classic/style/moodle.css index c104721dda9..21d48152b5f 100644 --- a/theme/classic/style/moodle.css +++ b/theme/classic/style/moodle.css @@ -13349,6 +13349,7 @@ table.calendartable caption { margin-bottom: 0.5rem; } .content-bank-container.view-grid .cb-heading, +.content-bank-container.view-grid .cb-uses, .content-bank-container.view-grid .cb-date, .content-bank-container.view-grid .cb-size, .content-bank-container.view-grid .cb-type, @@ -13390,12 +13391,13 @@ table.calendartable caption { .content-bank-container.view-list .cb-file { flex: 0 0 40%; max-width: 40%; } + .content-bank-container.view-list .cb-uses, .content-bank-container.view-list .cb-date, .content-bank-container.view-list .cb-size, .content-bank-container.view-list .cb-type, .content-bank-container.view-list .cb-author { - flex: 0 0 15%; - max-width: 15%; } + flex: 0 0 12%; + max-width: 12%; } .content-bank-container.view-list .cb-column.last { border-right: 0; } }