diff --git a/admin/settings/users.php b/admin/settings/users.php index c54a169ce34..6e905dc0cad 100644 --- a/admin/settings/users.php +++ b/admin/settings/users.php @@ -87,6 +87,14 @@ if ($hassiteconfig $choices['1'] = new lang_string('trackforumsyes'); $temp->add(new admin_setting_configselect('defaultpreference_trackforums', new lang_string('trackforums'), '', 0, $choices)); + + $choices = []; + $choices[\core_contentbank\content::VISIBILITY_PUBLIC] = new lang_string('visibilitychoicepublic', 'core_contentbank'); + $choices[\core_contentbank\content::VISIBILITY_UNLISTED] = new lang_string('visibilitychoiceunlisted', 'core_contentbank'); + $temp->add(new admin_setting_configselect('defaultpreference_core_contentbank_visibility', + new lang_string('visibilitypref', 'core_contentbank'), + new lang_string('visibilitypref_help', 'core_contentbank'), + \core_contentbank\content::VISIBILITY_PUBLIC, $choices)); } $ADMIN->add('accounts', $temp); diff --git a/contentbank/amd/build/actions.min.js b/contentbank/amd/build/actions.min.js index 584951ade92..c6587fd187e 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("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 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 +{"version":3,"sources":["../src/actions.js"],"names":["define","$","Ajax","Notification","Str","Templates","Url","ModalFactory","ModalEvents","ACTIONS","DELETE_CONTENT","RENAME_CONTENT","SET_CONTENT_VISIBILITY","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","visibility","setContentVisibility","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 SET_CONTENT_VISIBILITY: '[data-action=\"setcontentvisibility\"]',\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 $(ACTIONS.SET_CONTENT_VISIBILITY).click(function(e) {\n e.preventDefault();\n\n var contentid = $(this).data('contentid');\n var visibility = $(this).data('visibility');\n\n setContentVisibility(contentid, visibility);\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 /**\n * Set content visibility in the content bank.\n *\n * @param {int} contentid The content to modify\n * @param {int} visibility The new visibility value\n */\n function setContentVisibility(contentid, visibility) {\n var request = {\n methodname: 'core_contentbank_set_content_visibility',\n args: {\n contentid: contentid,\n visibility: visibility\n }\n };\n var requestType = 'success';\n Ajax.call([request])[0].then(function(data) {\n if (data.result) {\n return 'contentvisibilitychanged';\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 47a5e107a92..4adb9ef40fb 100644 --- a/contentbank/amd/src/actions.js +++ b/contentbank/amd/src/actions.js @@ -40,6 +40,7 @@ function($, Ajax, Notification, Str, Templates, Url, ModalFactory, ModalEvents) var ACTIONS = { DELETE_CONTENT: '[data-action="deletecontent"]', RENAME_CONTENT: '[data-action="renamecontent"]', + SET_CONTENT_VISIBILITY: '[data-action="setcontentvisibility"]', }; /** @@ -181,6 +182,15 @@ function($, Ajax, Notification, Str, Templates, Url, ModalFactory, ModalEvents) return; }).catch(Notification.exception); }); + + $(ACTIONS.SET_CONTENT_VISIBILITY).click(function(e) { + e.preventDefault(); + + var contentid = $(this).data('contentid'); + var visibility = $(this).data('visibility'); + + setContentVisibility(contentid, visibility); + }); }; /** @@ -262,6 +272,49 @@ function($, Ajax, Notification, Str, Templates, Url, ModalFactory, ModalEvents) }).catch(Notification.exception); } + /** + * Set content visibility in the content bank. + * + * @param {int} contentid The content to modify + * @param {int} visibility The new visibility value + */ + function setContentVisibility(contentid, visibility) { + var request = { + methodname: 'core_contentbank_set_content_visibility', + args: { + contentid: contentid, + visibility: visibility + } + }; + var requestType = 'success'; + Ajax.call([request])[0].then(function(data) { + if (data.result) { + return 'contentvisibilitychanged'; + } + requestType = 'error'; + return data.warnings[0].message; + + }).then(function(message) { + var params = null; + if (requestType == 'success') { + params = { + id: contentid, + statusmsg: message + }; + // Redirect to the content view page and display the message as a notification. + window.location.href = Url.relativeUrl('contentbank/view.php', params, false); + } else { + // Fetch error notifications. + Notification.addNotification({ + message: message, + type: 'error' + }); + Notification.fetchNotifications(); + } + return; + }).catch(Notification.exception); + } + return /** @alias module:core_contentbank/actions */ { // Public variables and functions. diff --git a/contentbank/classes/content.php b/contentbank/classes/content.php index 8048afefb5e..d9ba6688cb8 100644 --- a/contentbank/classes/content.php +++ b/contentbank/classes/content.php @@ -40,6 +40,17 @@ use core\event\contentbank_content_updated; * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class content { + /** + * @var int Visibility value. Public content is visible to all users with access to the content bank of the + * appropriate context. + */ + public const VISIBILITY_PUBLIC = 1; + + /** + * @var int Visibility value. Unlisted content is only visible to the author and to users with + * moodle/contentbank:viewunlistedcontent capability. + */ + public const VISIBILITY_UNLISTED = 2; /** @var stdClass $content The content of the current instance. **/ protected $content = null; @@ -250,6 +261,30 @@ abstract class content { return $this->content->configdata; } + /** + * Sets a new content visibility and saves it to database. + * + * @param int $visibility Must be self::PUBLIC or self::UNLISTED + * @return bool + * @throws coding_exception + */ + public function set_visibility(int $visibility): bool { + if (!in_array($visibility, [self::VISIBILITY_PUBLIC, self::VISIBILITY_UNLISTED])) { + return false; + } + $this->content->visibility = $visibility; + return $this->update_content(); + } + + /** + * Return true if the content may be shown to other users in the content bank. + * + * @return boolean + */ + public function get_visibility(): int { + return $this->content->visibility; + } + /** * Import a file as a valid content. * @@ -356,8 +391,12 @@ abstract class content { * @return bool True if content could be accessed. False otherwise. */ public function is_view_allowed(): bool { - // There's no capability at content level to check, - // but plugins can overwrite this method in case they want to check something related to content properties. - return true; + // Plugins can overwrite this method in case they want to check something related to content properties. + global $USER; + $context = \context::instance_by_id($this->get_contextid()); + + return $USER->id == $this->content->usercreated || + $this->get_visibility() == self::VISIBILITY_PUBLIC || + has_capability('moodle/contentbank:viewunlistedcontent', $context); } } diff --git a/contentbank/classes/contenttype.php b/contentbank/classes/contenttype.php index 8f9fec3be42..3295f24c5dd 100644 --- a/contentbank/classes/contenttype.php +++ b/contentbank/classes/contenttype.php @@ -75,9 +75,16 @@ abstract class contenttype { * @return content Object with content bank information. */ public function create_content(\stdClass $record = null): content { - global $USER, $DB; + global $USER, $DB, $CFG; $entry = new \stdClass(); + if (isset($record->visibility)) { + $entry->visibility = $record->visibility; + } else { + $usercreated = $record->usercreated ?? $USER->id; + $entry->visibility = get_user_preferences('core_contentbank_visibility', + $CFG->defaultpreference_core_contentbank_visibility, $usercreated); + } $entry->contenttype = $this->get_contenttype_name(); $entry->contextid = $this->context->id; $entry->name = $record->name ?? ''; diff --git a/contentbank/classes/external/set_content_visibility.php b/contentbank/classes/external/set_content_visibility.php new file mode 100644 index 00000000000..dd5b880feb2 --- /dev/null +++ b/contentbank/classes/external/set_content_visibility.php @@ -0,0 +1,130 @@ +. + +namespace core_contentbank\external; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->libdir . '/externallib.php'); + +use external_api; +use external_function_parameters; +use external_single_structure; +use external_value; +use external_warnings; + +/** + * External API to set the visibility of content bank content. + * + * @package core_contentbank + * @copyright 2020 François Moreau + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class set_content_visibility extends external_api { + /** + * set_content_visibility parameters. + * + * @since Moodle 3.11 + * @return external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters( + [ + 'contentid' => new external_value(PARAM_INT, 'The content id to rename', VALUE_REQUIRED), + 'visibility' => new external_value(PARAM_INT, 'The new visibility for the content', VALUE_REQUIRED), + ] + ); + } + + /** + * Set visibility of a content from the contentbank. + * + * @since Moodle 3.11 + * @param int $contentid The content id to rename. + * @param int $visibility The new visibility. + * @return array + */ + public static function execute(int $contentid, int $visibility): array { + global $DB; + + $result = false; + $warnings = []; + + $params = self::validate_parameters(self::execute_parameters(), [ + 'contentid' => $contentid, + 'visibility' => $visibility, + ]); + + try { + $record = $DB->get_record('contentbank_content', ['id' => $params['contentid']], '*', MUST_EXIST); + $contenttypeclass = "\\$record->contenttype\\contenttype"; + if (class_exists($contenttypeclass)) { + $context = \context::instance_by_id($record->contextid, MUST_EXIST); + self::validate_context($context); + $contenttype = new $contenttypeclass($context); + $contentclass = "\\$record->contenttype\\content"; + $content = new $contentclass($record); + // Check capability. + if ($contenttype->can_manage($content)) { + // This content's visibility can be changed. + if ($content->set_visibility($params['visibility'])) { + $result = true; + } else { + $warnings[] = [ + 'item' => $params['contentid'], + 'warningcode' => 'contentvisibilitynotset', + 'message' => get_string('contentvisibilitynotset', 'core_contentbank') + ]; + } + + } else { + // The user has no permission to manage this content. + $warnings[] = [ + 'item' => $params['contentid'], + 'warningcode' => 'nopermissiontomanage', + 'message' => get_string('nopermissiontomanage', 'core_contentbank') + ]; + } + } + } catch (\moodle_exception $e) { + // The content or the context don't exist. + $warnings[] = [ + 'item' => $params['contentid'], + 'warningcode' => 'exception', + 'message' => $e->getMessage() + ]; + } + + return [ + 'result' => $result, + 'warnings' => $warnings + ]; + } + + /** + * set_content_visibility return. + * + * @since Moodle 3.11 + * @return external_single_structure + */ + public static function execute_returns(): external_single_structure { + return new external_single_structure([ + 'result' => new external_value(PARAM_BOOL, 'The processing result'), + 'warnings' => new external_warnings() + ]); + } +} diff --git a/contentbank/classes/output/bankcontent.php b/contentbank/classes/output/bankcontent.php index edd6356599e..2678eaf41e9 100644 --- a/contentbank/classes/output/bankcontent.php +++ b/contentbank/classes/output/bankcontent.php @@ -28,6 +28,7 @@ use renderable; use templatable; use renderer_base; use stdClass; +use core_contentbank\content; /** * Class containing data for bank content @@ -85,7 +86,11 @@ class bankcontent implements renderable, templatable { $mimetype = $file ? get_mimetype_description($file) : ''; $contenttypeclass = $content->get_content_type().'\\contenttype'; $contenttype = new $contenttypeclass($this->context); - $name = $content->get_name(); + if ($content->get_visibility() == content::VISIBILITY_UNLISTED) { + $name = get_string('visibilitytitleunlisted', 'contentbank', $content->get_name()); + } else { + $name = $content->get_name(); + } $author = \core_user::get_user($content->get_content()->usercreated); $contentdata[] = array( 'name' => $name, @@ -98,6 +103,7 @@ class bankcontent implements renderable, templatable { 'size' => display_size($filesize), 'type' => $mimetype, 'author' => fullname($author), + 'visibilityunlisted' => $content->get_visibility() == content::VISIBILITY_UNLISTED ); } $data->viewlist = get_user_preferences('core_contentbank_view_list'); diff --git a/contentbank/lib.php b/contentbank/lib.php index e709df77df0..f2ec543afa6 100644 --- a/contentbank/lib.php +++ b/contentbank/lib.php @@ -22,6 +22,8 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +use \core_contentbank\content; + /** * Get the current user preferences that are available * @@ -35,5 +37,10 @@ function core_contentbank_user_preferences() { 'null' => NULL_NOT_ALLOWED, 'default' => 'none' ], + 'core_contentbank_visibility' => [ + 'choices' => [content::VISIBILITY_UNLISTED, content::VISIBILITY_PUBLIC], + 'type' => PARAM_INT, + 'null' => NULL_NOT_ALLOWED + ] ]; } diff --git a/contentbank/templates/bankcontent.mustache b/contentbank/templates/bankcontent.mustache index 50d1e6d5e8e..6b500e1898e 100644 --- a/contentbank/templates/bankcontent.mustache +++ b/contentbank/templates/bankcontent.mustache @@ -29,7 +29,8 @@ "type": "Archive (H5P)", "author": "Admin user", "link": "http://something/contentbank/contenttype/h5p/view.php?url=http://something/pluginfile.php/1/contentbank/public/accordion.h5p", - "icon" : "http://something/theme/image.php/boost/core/1581597850/f/h5p-64" + "icon" : "http://something/theme/image.php/boost/core/1581597850/f/h5p-64", + "visibilityunlisted": true }, { "name": "resume.pdf", @@ -40,7 +41,8 @@ "bytes": 716126, "type": "Archive (PDF)", "author": "Admin user", - "icon": "http://something/theme/image.php/boost/core/1584597850/f/pdf-64" + "icon": "http://something/theme/image.php/boost/core/1584597850/f/pdf-64", + "visibilityunlisted": false } ], "tools": [ @@ -155,7 +157,7 @@ data-region="contentbank"> {{#contents}} -
assertEquals($newcontext->id, $file->get_contextid()); } + /** + * Tests for set_visibility behaviour + * + * @covers ::set_visibility + */ + public function test_set_visibility() { + $this->resetAfterTest(); + $this->setAdminUser(); + $context = context_system::instance(); + $oldvisibility = content::VISIBILITY_PUBLIC; + $newvisibility = content::VISIBILITY_UNLISTED; + $illegalvisibility = -1; + + $record = new stdClass(); + $record->visibility = $oldvisibility; + $contenttype = new contenttype($context); + $content = $contenttype->create_content($record); + + $this->assertEquals($oldvisibility, $content->get_visibility()); + + $content->set_visibility($newvisibility); + + $this->assertEquals($newvisibility, $content->get_visibility()); + + $content->set_visibility($illegalvisibility); + + $this->assertEquals($newvisibility, $content->get_visibility()); + } + /** * Tests for 'import_file' behaviour when replacing a file. * @@ -299,6 +328,44 @@ class core_contenttype_content_testcase extends \advanced_testcase { $this->assertInstanceOf(get_class($type), $contenttype); } + /** + * Tests for 'is_view_allowed'. + * + * @covers ::is_view_allowed + */ + public function test_is_view_allowed() { + $this->resetAfterTest(); + $this->setAdminUser(); + $context = context_system::instance(); + + $userauthor = $this->getDataGenerator()->create_user(); + $userother = $this->getDataGenerator()->create_user(); + + $contenttype = new contenttype($context); + + $unlistedrecord = new stdClass(); + $unlistedrecord->visibility = content::VISIBILITY_UNLISTED; + $unlistedrecord->usercreated = $userauthor->id; + $unlistedcontent = $contenttype->create_content($unlistedrecord); + + $publicrecord = new stdClass(); + $publicrecord->visibility = content::VISIBILITY_PUBLIC; + $publicrecord->usercreated = $userauthor->id; + $publiccontent = $contenttype->create_content($publicrecord); + + $this->setUser($userother); + $this->assertFalse($unlistedcontent->is_view_allowed()); + $this->assertTrue($publiccontent->is_view_allowed()); + + $this->setUser($userauthor); + $this->assertTrue($unlistedcontent->is_view_allowed()); + $this->assertTrue($publiccontent->is_view_allowed()); + + $this->setAdminUser(); + $this->assertTrue($unlistedcontent->is_view_allowed()); + $this->assertTrue($publiccontent->is_view_allowed()); + } + /** * Tests for 'get_uses' behaviour. * diff --git a/contentbank/tests/generator/lib.php b/contentbank/tests/generator/lib.php index 4d7e0228cae..5ca338eea18 100644 --- a/contentbank/tests/generator/lib.php +++ b/contentbank/tests/generator/lib.php @@ -22,6 +22,9 @@ * @copyright 2020 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ + +use core_contentbank\content; + defined('MOODLE_INTERNAL') || die(); global $CFG; @@ -46,11 +49,12 @@ class core_contentbank_generator extends \component_generator_base { * @param bool $convert2class Whether the class should return stdClass or plugin instance. * @param string $filepath The filepath of the file associated to the content to create. * @param string $contentname The name of the content that will be created. + * @param int $visibility The visibility of the content that will be created. * @return array An array with all the records added to the content bank. */ public function generate_contentbank_data(?string $contenttype, int $itemstocreate = 1, int $userid = 0, ?\context $context = null, bool $convert2class = true, string $filepath = 'contentfile.h5p', - string $contentname = 'Test content '): array { + string $contentname = 'Test content ', int $visibility = content::VISIBILITY_PUBLIC): array { global $DB, $USER; $records = []; @@ -73,6 +77,7 @@ class core_contentbank_generator extends \component_generator_base { $record->name = ($itemstocreate === 1) ? $contentname : $contentname . $i; $record->configdata = ''; $record->usercreated = $userid ?? $USER->id; + $record->visibility = $visibility; $content = $type->create_content($record); $record = $content->get_content(); diff --git a/contentbank/upgrade.txt b/contentbank/upgrade.txt index a94fbcf61d0..8eca72b6fa0 100644 --- a/contentbank/upgrade.txt +++ b/contentbank/upgrade.txt @@ -3,3 +3,4 @@ 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. +* Added set_visibility()/get_visibility() methods to let users decide if their content should be listed in the content bank. diff --git a/contentbank/view.php b/contentbank/view.php index 5e4cf3d2f0b..8258fb2db09 100644 --- a/contentbank/view.php +++ b/contentbank/view.php @@ -22,6 +22,8 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +use core_contentbank\content; + require('../config.php'); require_login(); @@ -50,22 +52,61 @@ if ($PAGE->course) { require_login($PAGE->course->id); } -$PAGE->set_url(new \moodle_url('/contentbank/view.php', ['id' => $id])); -$PAGE->set_context($context); -$PAGE->navbar->add($record->name); -$PAGE->set_heading($record->name); -$title .= ": ".$record->name; -$PAGE->set_title($title); -$PAGE->set_pagetype('contentbank'); - $cb = new \core_contentbank\contentbank(); $content = $cb->get_content_from_id($record->id); $contenttype = $content->get_content_type_instance(); +$pageheading = $record->name; + +if (!$content->is_view_allowed()) { + print_error('notavailable', 'contentbank'); +} + +if ($content->get_visibility() == content::VISIBILITY_UNLISTED) { + $pageheading = get_string('visibilitytitleunlisted', 'contentbank', $record->name); +} + +$PAGE->set_url(new \moodle_url('/contentbank/view.php', ['id' => $id])); +$PAGE->set_context($context); +$PAGE->navbar->add($record->name); +$PAGE->set_heading($pageheading); +$title .= ": ".$record->name; +$PAGE->set_title($title); +$PAGE->set_pagetype('contentbank'); // Create the cog menu with all the secondary actions, such as delete, rename... $actionmenu = new action_menu(); $actionmenu->set_alignment(action_menu::TR, action_menu::BR); if ($contenttype->can_manage($content)) { + // Add the visibility item to the menu. + switch($content->get_visibility()) { + case content::VISIBILITY_UNLISTED: + $visibilitylabel = get_string('visibilitysetpublic', 'core_contentbank'); + $newvisibility = content::VISIBILITY_PUBLIC; + $visibilityicon = 't/hide'; + break; + case content::VISIBILITY_PUBLIC: + $visibilitylabel = get_string('visibilitysetunlisted', 'core_contentbank'); + $newvisibility = content::VISIBILITY_UNLISTED; + $visibilityicon = 't/show'; + break; + default: + print_error('contentvisibilitynotfound', 'error', $returnurl, $content->get_visibility()); + break; + } + + $attributes = [ + 'data-action' => 'setcontentvisibility', + 'data-visibility' => $newvisibility, + 'data-contentid' => $content->get_id(), + ]; + $actionmenu->add_secondary_action(new action_menu_link( + new moodle_url('#'), + new pix_icon($visibilityicon, $visibilitylabel), + $visibilitylabel, + false, + $attributes + )); + // Add the rename content item to the menu. $attributes = [ 'data-action' => 'renamecontent', @@ -130,7 +171,22 @@ if ($errormsg !== '' && get_string_manager()->string_exists($errormsg, 'core_con $errormsg = get_string($errormsg, 'core_contentbank'); echo $OUTPUT->notification($errormsg); } else if ($statusmsg !== '' && get_string_manager()->string_exists($statusmsg, 'core_contentbank')) { - $statusmsg = get_string($statusmsg, 'core_contentbank'); + if ($statusmsg == 'contentvisibilitychanged') { + switch ($content->get_visibility()) { + case content::VISIBILITY_PUBLIC: + $visibilitymsg = get_string('public', 'core_contentbank'); + break; + case content::VISIBILITY_UNLISTED: + $visibilitymsg = get_string('unlisted', 'core_contentbank'); + break; + default: + print_error('contentvisibilitynotfound', 'error', $returnurl, $content->get_visibility()); + break; + } + $statusmsg = get_string($statusmsg, 'core_contentbank', $visibilitymsg); + } else { + $statusmsg = get_string($statusmsg, 'core_contentbank'); + } echo $OUTPUT->notification($statusmsg, 'notifysuccess'); } if ($contenttype->can_access()) { diff --git a/lang/en/contentbank.php b/lang/en/contentbank.php index 648a47429cd..ceeb7cb3d32 100644 --- a/lang/en/contentbank.php +++ b/lang/en/contentbank.php @@ -25,6 +25,7 @@ $string['author'] = 'Author'; $string['contentbank'] = 'Content bank'; $string['close'] = 'Close'; +$string['contentbankpreferences'] = 'Content bank preferences'; $string['contentdeleted'] = 'The content has been deleted.'; $string['contentname'] = 'Content name'; $string['contentnotdeleted'] = 'An error was encountered while trying to delete the content.'; @@ -33,6 +34,8 @@ $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 can not edit this content'; +$string['contentvisibilitychanged'] = 'The content has been made {$a}.'; +$string['contentvisibilitynotset'] = 'An error was encountered while trying to set the content visibility.'; $string['contextnotallowed'] = 'You are not allowed to access the content bank in this context.'; $string['emptynamenotallowed'] = 'Empty name is not allowed'; $string['eventcontentcreated'] = 'Content created'; @@ -55,6 +58,7 @@ $string['lastmodified'] = 'Last modified'; $string['name'] = 'Content'; $string['nocontentavailable'] = 'No content available'; $string['nocontenttypes'] = 'No content types available'; +$string['notavailable'] = 'Sorry, this content is not available.'; $string['nopermissiontodelete'] = 'You do not have permission to delete content.'; $string['nopermissiontomanage'] = 'You do not have permission to manage content.'; $string['privacy:metadata:content:contenttype'] = 'The contenttype plugin of the content in the content bank.'; @@ -76,3 +80,12 @@ $string['type'] = 'Type'; $string['unsupported'] = 'This content type is not supported.'; $string['upload'] = 'Upload'; $string['uses'] = 'Places linked'; +$string['visibilitychoicepublic'] = 'Public'; +$string['visibilitychoiceunlisted'] = 'Unlisted'; +$string['public'] = 'public'; +$string['unlisted'] = 'unlisted'; +$string['visibilitypref'] = 'Default content visibility'; +$string['visibilitypref_help'] = 'Content you create in the content bank will use this visibility setting by default.'; +$string['visibilitysetpublic'] = 'Make public'; +$string['visibilitysetunlisted'] = 'Make unlisted'; +$string['visibilitytitleunlisted'] = '{$a} (Unlisted)'; diff --git a/lang/en/error.php b/lang/en/error.php index 6631f6203be..92ae88e82e5 100644 --- a/lang/en/error.php +++ b/lang/en/error.php @@ -182,6 +182,7 @@ $string['componentisuptodate'] = 'Component is up-to-date'; $string['confirmationnotenabled'] = 'User confirmation is not enabled on this site'; $string['confirmsesskeybad'] = 'Sorry, but your session key could not be confirmed to carry out this action. This security feature prevents against accidental or malicious execution of important functions in your name. Please make sure you really wanted to execute this function.'; $string['contenttypenotfound'] = 'The \'{$a}\' content bank type doesn\'t exist or is not recognised.'; +$string['contentvisibilitynotfound'] = 'The content visibility with value \'{$a}\' doesn\'t exist or is not recognised.'; $string['couldnotassignrole'] = 'A serious but unspecified error occurred while trying to assign a role to you'; $string['couldnotupdatenoexistinguser'] = 'Cannot update the user - user doesn\'t exist'; $string['couldnotverifyagedigitalconsent'] = 'An error occurred while trying to verify the age of digital consent.
Please contact administrator.'; diff --git a/lang/en/role.php b/lang/en/role.php index a9500d7e524..318e8746af4 100644 --- a/lang/en/role.php +++ b/lang/en/role.php @@ -156,6 +156,7 @@ $string['contentbank:deleteowncontent'] = 'Delete content from own content bank' $string['contentbank:downloadcontent'] = 'Download content from the content bank'; $string['contentbank:manageanycontent'] = 'Manage any content from the content bank'; $string['contentbank:manageowncontent'] = 'Manage content from own content bank'; +$string['contentbank:viewunlistedcontent'] = 'View unlisted content from the content bank'; $string['contentbank:upload'] = 'Upload new content to the content bank'; $string['contentbank:useeditor'] = 'Create or edit content using a content type editor'; $string['context'] = 'Context'; diff --git a/lib/behat/classes/behat_core_generator.php b/lib/behat/classes/behat_core_generator.php index f36955c5022..8851be97ce2 100644 --- a/lib/behat/classes/behat_core_generator.php +++ b/lib/behat/classes/behat_core_generator.php @@ -925,6 +925,9 @@ class behat_core_generator extends behat_generator_base { $record = new stdClass(); $record->usercreated = $data['userid']; $record->name = $data['contentname']; + if (isset($data['visibility'])) { + $record->visibility = $data['visibility']; + } $content = $contenttype->create_content($record); if (!empty($data['filepath'])) { diff --git a/lib/db/access.php b/lib/db/access.php index bc812d5e155..d1cdce21a9d 100644 --- a/lib/db/access.php +++ b/lib/db/access.php @@ -2614,4 +2614,14 @@ $capabilities = array( 'contextlevel' => CONTEXT_COURSE, 'archetypes' => [], ], + + // Allow users to view hidden content. + 'moodle/contentbank:viewunlistedcontent' => [ + 'captype' => 'read', + 'contextlevel' => CONTEXT_COURSE, + 'archetypes' => [ + 'manager' => CAP_ALLOW, + 'coursecreator' => CAP_ALLOW, + ] + ], ); diff --git a/lib/db/install.xml b/lib/db/install.xml index 87a8826c2fd..0dc74dbf64f 100644 --- a/lib/db/install.xml +++ b/lib/db/install.xml @@ -4271,6 +4271,7 @@ + diff --git a/lib/db/services.php b/lib/db/services.php index 17b2443d558..774fbd179e3 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -2728,6 +2728,15 @@ $functions = array( 'ajax' => 'true', 'capabilities' => 'moodle/contentbank:manageowncontent', ], + 'core_contentbank_set_content_visibility' => [ + 'classname' => 'core_contentbank\external\set_content_visibility', + 'methodname' => 'execute', + 'classpath' => '', + 'description' => 'Set the visibility of a content in the content bank.', + 'type' => 'write', + 'ajax' => 'true', + 'capabilities' => 'moodle/contentbank:manageowncontent', + ], 'core_create_userfeedback_action_record' => [ 'classname' => 'core\external\record_userfeedback_action', 'methodname' => 'execute', diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index 40efad053fb..1f4a4e1306c 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -2393,5 +2393,19 @@ function xmldb_main_upgrade($oldversion) { upgrade_main_savepoint(true, 2021052500.55); } + if ($oldversion < 2021052500.59) { + // Define field visibility to be added to contentbank_content. + $table = new xmldb_table('contentbank_content'); + $field = new xmldb_field('visibility', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '1', 'contextid'); + + // Conditionally launch add field visibility. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Main savepoint reached. + upgrade_main_savepoint(true, 2021052500.59); + } + return true; } diff --git a/lib/navigationlib.php b/lib/navigationlib.php index 1af265bd66d..531056ef798 100644 --- a/lib/navigationlib.php +++ b/lib/navigationlib.php @@ -5104,12 +5104,22 @@ class settings_navigation extends navigation_node { } } + // Add "Content bank preferences" link. + if (isloggedin() && !isguestuser($user)) { + if ($currentuser && has_capability('moodle/user:editownprofile', $systemcontext) || + has_capability('moodle/user:editprofile', $usercontext)) { + $url = new moodle_url('/user/contentbank.php', ['id' => $user->id]); + $useraccount->add(get_string('contentbankpreferences', 'core_contentbank'), $url, self::TYPE_SETTING, + null, 'contentbankpreferences'); + } + } + // View the roles settings. - if (has_any_capability(array('moodle/role:assign', 'moodle/role:safeoverride', 'moodle/role:override', - 'moodle/role:manage'), $usercontext)) { + if (has_any_capability(['moodle/role:assign', 'moodle/role:safeoverride', 'moodle/role:override', + 'moodle/role:manage'], $usercontext)) { $roles = $usersetting->add(get_string('roles'), null, self::TYPE_SETTING); - $url = new moodle_url('/admin/roles/usersroles.php', array('userid'=>$user->id, 'courseid'=>$course->id)); + $url = new moodle_url('/admin/roles/usersroles.php', ['userid' => $user->id, 'courseid' => $course->id]); $roles->add(get_string('thisusersroles', 'role'), $url, self::TYPE_SETTING); $assignableroles = get_assignable_roles($usercontext, ROLENAME_BOTH); diff --git a/theme/boost/scss/moodle/contentbank.scss b/theme/boost/scss/moodle/contentbank.scss index b56ca61d3ae..92afeb41cba 100644 --- a/theme/boost/scss/moodle/contentbank.scss +++ b/theme/boost/scss/moodle/contentbank.scss @@ -18,6 +18,10 @@ margin-bottom: 0.5rem; } + .cb-listitem.cb-unlisted { + position: relative; + } + @include media-breakpoint-down(sm) { .cb-listitem { flex-basis: 50%; @@ -44,6 +48,24 @@ margin-right: auto; margin-bottom: 0.5rem; } + + .cb-unlisted .cb-thumbnail { + opacity: .3; + } + + /* Display a centered eye slash on top of unlisted content icons. */ + .cb-unlisted::after { + position: absolute; + top: 20px; + left: 0; + width: 100%; + content: $fa-var-eye-slash; + font-family: FontAwesome; + font-size: 26px; + text-align: center; + opacity: 0.8; + } + .cb-heading, .cb-uses, .cb-date, @@ -82,6 +104,15 @@ border-right: $border-width solid $border-color; } + .cb-listitem.cb-unlisted .cb-thumbnail { + opacity: .3; + } + + .cb-listitem.cb-unlisted .cb-column, + .cb-listitem.cb-unlisted .cb-column a { + color: $text-muted; + } + @include media-breakpoint-down(sm) { .cb-column { flex: 0 0 50%; diff --git a/theme/boost/style/moodle.css b/theme/boost/style/moodle.css index ce346d505b9..076e9532536 100644 --- a/theme/boost/style/moodle.css +++ b/theme/boost/style/moodle.css @@ -13109,38 +13109,48 @@ table.calendartable caption { background-position: center; background-size: cover; } -.content-bank-container.view-grid .cb-listitem { - margin-bottom: 0.5rem; } - -@media (max-width: 767.98px) { +.content-bank-container.view-grid { + /* Display a centered eye slash on top of unlisted content icons. */ } .content-bank-container.view-grid .cb-listitem { - flex-basis: 50%; } } - -@media (min-width: 576px) { - .content-bank-container.view-grid .cb-listitem { - max-width: 120px; - min-width: 120px; } } - -.content-bank-container.view-grid .cb-name { - text-align: center; } - -.content-bank-container.view-grid .cb-file { - padding: 0.5rem; } - -.content-bank-container.view-grid .cb-thumbnail { - width: 64px; - height: 64px; - margin-left: auto; - margin-right: auto; - 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, -.content-bank-container.view-grid .cb-author { - display: none; } + margin-bottom: 0.5rem; } + .content-bank-container.view-grid .cb-listitem.cb-unlisted { + position: relative; } + @media (max-width: 767.98px) { + .content-bank-container.view-grid .cb-listitem { + flex-basis: 50%; } } + @media (min-width: 576px) { + .content-bank-container.view-grid .cb-listitem { + max-width: 120px; + min-width: 120px; } } + .content-bank-container.view-grid .cb-name { + text-align: center; } + .content-bank-container.view-grid .cb-file { + padding: 0.5rem; } + .content-bank-container.view-grid .cb-thumbnail { + width: 64px; + height: 64px; + margin-left: auto; + margin-right: auto; + margin-bottom: 0.5rem; } + .content-bank-container.view-grid .cb-unlisted .cb-thumbnail { + opacity: .3; } + .content-bank-container.view-grid .cb-unlisted::after { + position: absolute; + top: 20px; + left: 0; + width: 100%; + content: ""; + font-family: FontAwesome; + font-size: 26px; + text-align: center; + opacity: 0.8; } + .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, + .content-bank-container.view-grid .cb-author { + display: none; } .content-bank-container.view-list .cb-content-wrapper { padding: 0 0.5rem; @@ -13164,6 +13174,13 @@ table.calendartable caption { .content-bank-container.view-list .cb-column { border-right: 1px solid #dee2e6; } +.content-bank-container.view-list .cb-listitem.cb-unlisted .cb-thumbnail { + opacity: .3; } + +.content-bank-container.view-list .cb-listitem.cb-unlisted .cb-column, +.content-bank-container.view-list .cb-listitem.cb-unlisted .cb-column a { + color: #6c757d; } + @media (max-width: 767.98px) { .content-bank-container.view-list .cb-column { flex: 0 0 50%; diff --git a/theme/classic/style/moodle.css b/theme/classic/style/moodle.css index 7cdfba1f72b..60e19a260f9 100644 --- a/theme/classic/style/moodle.css +++ b/theme/classic/style/moodle.css @@ -13323,38 +13323,48 @@ table.calendartable caption { background-position: center; background-size: cover; } -.content-bank-container.view-grid .cb-listitem { - margin-bottom: 0.5rem; } - -@media (max-width: 767.98px) { +.content-bank-container.view-grid { + /* Display a centered eye slash on top of unlisted content icons. */ } .content-bank-container.view-grid .cb-listitem { - flex-basis: 50%; } } - -@media (min-width: 576px) { - .content-bank-container.view-grid .cb-listitem { - max-width: 120px; - min-width: 120px; } } - -.content-bank-container.view-grid .cb-name { - text-align: center; } - -.content-bank-container.view-grid .cb-file { - padding: 0.5rem; } - -.content-bank-container.view-grid .cb-thumbnail { - width: 64px; - height: 64px; - margin-left: auto; - margin-right: auto; - 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, -.content-bank-container.view-grid .cb-author { - display: none; } + margin-bottom: 0.5rem; } + .content-bank-container.view-grid .cb-listitem.cb-unlisted { + position: relative; } + @media (max-width: 767.98px) { + .content-bank-container.view-grid .cb-listitem { + flex-basis: 50%; } } + @media (min-width: 576px) { + .content-bank-container.view-grid .cb-listitem { + max-width: 120px; + min-width: 120px; } } + .content-bank-container.view-grid .cb-name { + text-align: center; } + .content-bank-container.view-grid .cb-file { + padding: 0.5rem; } + .content-bank-container.view-grid .cb-thumbnail { + width: 64px; + height: 64px; + margin-left: auto; + margin-right: auto; + margin-bottom: 0.5rem; } + .content-bank-container.view-grid .cb-unlisted .cb-thumbnail { + opacity: .3; } + .content-bank-container.view-grid .cb-unlisted::after { + position: absolute; + top: 20px; + left: 0; + width: 100%; + content: ""; + font-family: FontAwesome; + font-size: 26px; + text-align: center; + opacity: 0.8; } + .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, + .content-bank-container.view-grid .cb-author { + display: none; } .content-bank-container.view-list .cb-content-wrapper { padding: 0 0.5rem; @@ -13378,6 +13388,13 @@ table.calendartable caption { .content-bank-container.view-list .cb-column { border-right: 1px solid #dee2e6; } +.content-bank-container.view-list .cb-listitem.cb-unlisted .cb-thumbnail { + opacity: .3; } + +.content-bank-container.view-list .cb-listitem.cb-unlisted .cb-column, +.content-bank-container.view-list .cb-listitem.cb-unlisted .cb-column a { + color: #6c757d; } + @media (max-width: 767.98px) { .content-bank-container.view-list .cb-column { flex: 0 0 50%; diff --git a/user/classes/form/contentbank_user_preferences_form.php b/user/classes/form/contentbank_user_preferences_form.php new file mode 100644 index 00000000000..02782d0f3f1 --- /dev/null +++ b/user/classes/form/contentbank_user_preferences_form.php @@ -0,0 +1,53 @@ +. + +namespace core_user\form; + +use \core_contentbank\content; + +defined('MOODLE_INTERNAL') || die; + +require_once($CFG->dirroot.'/lib/formslib.php'); + +/** + * Form to edit a user's preferences concerning the content bank. + * + * @package core_user + * @copyright 2020 François Moreau + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class contentbank_user_preferences_form extends \moodleform { + + /** + * Define the form. + */ + public function definition () { + global $CFG, $USER; + + $mform = $this->_form; + + $mform->addElement('hidden', 'id'); + $mform->setType('id', PARAM_INT); + + $options = [ + content::VISIBILITY_PUBLIC => get_string('visibilitychoicepublic', 'core_contentbank'), + content::VISIBILITY_UNLISTED => get_string('visibilitychoiceunlisted', 'core_contentbank') + ]; + $mform->addElement('select', 'contentvisibility', get_string('visibilitypref', 'core_contentbank'), $options); + $mform->addHelpButton('contentvisibility', 'visibilitypref', 'core_contentbank'); + $this->add_action_buttons(true, get_string('savechanges')); + } +} diff --git a/user/contentbank.php b/user/contentbank.php new file mode 100644 index 00000000000..e435cff6e3e --- /dev/null +++ b/user/contentbank.php @@ -0,0 +1,71 @@ +. + +/** + * Allows you to edit a users profile + * + * @copyright 2020 François Moreau + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @package core_user + */ + +require_once('../config.php'); +require_once($CFG->dirroot.'/user/editlib.php'); +require_once($CFG->dirroot.'/user/lib.php'); + +require_login(); + +$userid = optional_param('id', $USER->id, PARAM_INT); // User id. + +$PAGE->set_url('/user/contentbank.php', ['id' => $userid]); + +list($user, $course) = useredit_setup_preference_page($userid, SITEID); + +$form = new \core_user\form\contentbank_user_preferences_form(null, ['userid' => $user->id]); + +$user->contentvisibility = get_user_preferences('core_contentbank_visibility', + $CFG->defaultpreference_core_contentbank_visibility, $user->id); + +$form->set_data($user); + +$redirect = new moodle_url("/user/preferences.php", ['userid' => $user->id]); + +if ($form->is_cancelled()) { + redirect($redirect); +} else if ($data = $form->get_data()) { + $data = $form->get_data(); + $usernew = [ + 'id' => $user->id, + 'preference_core_contentbank_visibility' => $data->contentvisibility + ]; + useredit_update_user_preference($usernew); + + \core\event\user_updated::create_from_userid($user->id)->trigger(); + redirect($redirect); +} + +$title = get_string('contentbankpreferences', 'core_contentbank'); +$userfullname = fullname($user, true); + +$PAGE->navbar->includesettingsbase = true; + +$PAGE->set_title("$course->shortname: $title"); +$PAGE->set_heading($userfullname); + +echo $OUTPUT->header(); +echo $OUTPUT->heading($title); +$form->display(); +echo $OUTPUT->footer(); diff --git a/version.php b/version.php index 4094cb8cf06..8a0d56b7a43 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2021052500.58; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2021052500.59; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes. $release = '4.0dev (Build: 20210211)'; // Human-friendly version name