first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+10
View File
@@ -0,0 +1,10 @@
define("core_contentbank/search",["exports","jquery","core_contentbank/selectors","core/str","core/pending","core/utils"],(function(_exports,_jquery,_selectors,_str,_pending,_utils){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
/**
* Search methods for finding contents in the content bank.
*
* @module core_contentbank/search
* @copyright 2020 Sara Arjona <sara@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_jquery=_interopRequireDefault(_jquery),_selectors=_interopRequireDefault(_selectors),_pending=_interopRequireDefault(_pending);_exports.init=()=>{const pendingPromise=new _pending.default,root=(0,_jquery.default)(_selectors.default.regions.contentbank);registerListenerEvents(root),pendingPromise.resolve()};const registerListenerEvents=root=>{const searchInput=root.find(_selectors.default.elements.searchinput)[0];root.on("click",_selectors.default.actions.search,(function(e){e.preventDefault(),toggleSearchResultsView(root,searchInput.value)})),root.on("click",_selectors.default.actions.clearSearch,(function(e){e.preventDefault(),searchInput.value="",searchInput.focus(),toggleSearchResultsView(root,searchInput.value)})),searchInput.addEventListener("input",(0,_utils.debounce)((()=>{toggleSearchResultsView(root,searchInput.value)}),300))},toggleSearchResultsView=async(body,searchQuery)=>{const clearSearchButton=body.find(_selectors.default.actions.clearSearch)[0],navbarBreadcrumb=body.find(_selectors.default.elements.cbnavbarbreadcrumb)[0],navbarTotal=body.find(_selectors.default.elements.cbnavbartotalsearch)[0],filteredContents=filterContents(body,searchQuery);searchQuery.length>0?(clearSearchButton.classList.remove("d-none"),navbarBreadcrumb.classList.add("d-none"),navbarTotal.innerHTML=await(0,_str.getString)("itemsfound","core_contentbank",filteredContents.length),navbarTotal.classList.remove("d-none")):(clearSearchButton.classList.add("d-none"),navbarBreadcrumb.classList.remove("d-none"),navbarTotal.classList.add("d-none"))},filterContents=(body,searchTerm)=>{const contents=Array.from(body.find(_selectors.default.elements.listitem)),searchResults=[];return contents.forEach((content=>{const contentName=content.getAttribute("data-name");if(""===searchTerm||contentName.toLowerCase().includes(searchTerm.toLowerCase())){searchResults.push(content);content.querySelector(_selectors.default.regions.cbcontentname).innerHTML=highlight(contentName,searchTerm),content.classList.remove("d-none")}else content.classList.add("d-none")})),searchResults},highlight=(text,highlightText)=>{let result=text;if(""!==highlightText){const pos=text.toLowerCase().indexOf(highlightText.toLowerCase());pos>-1&&(result=text.substr(0,pos)+'<span class="matchtext">'+text.substr(pos,highlightText.length)+"</span>"+text.substr(pos+highlightText.length))}return result}}));
//# sourceMappingURL=search.min.js.map
File diff suppressed because one or more lines are too long
+11
View File
@@ -0,0 +1,11 @@
define("core_contentbank/selectors",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;
/**
* Define all of the selectors we will be using on the contentbank interface.
*
* @module core_contentbank/selectors
* @copyright 2020 Sara Arjona <sara@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
const getDataSelector=(name,value)=>"[data-".concat(name,'="').concat(value,'"]');var _default={regions:{cbcontentname:getDataSelector("region","cb-content-name"),contentbank:getDataSelector("region","contentbank"),filearea:getDataSelector("region","filearea")},actions:{search:getDataSelector("action","searchcontent"),clearSearch:getDataSelector("action","clearsearch"),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"),sortauthor:getDataSelector("action","sortauthor")},elements:{listitem:".cb-listitem",heading:".cb-heading",cell:".cb-column",cbnavbarbreadcrumb:".cb-navbar-breadbrumb",cbnavbartotalsearch:".cb-navbar-totalsearch",searchinput:'[data-action="search"]',sortbutton:".cb-btnsort"}};return _exports.default=_default,_exports.default}));
//# sourceMappingURL=selectors.min.js.map
@@ -0,0 +1 @@
{"version":3,"file":"selectors.min.js","sources":["../src/selectors.js"],"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 <http://www.gnu.org/licenses/>.\n\n/**\n * Define all of the selectors we will be using on the contentbank interface.\n *\n * @module core_contentbank/selectors\n * @copyright 2020 Sara Arjona <sara@moodle.com>\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 heading: '.cb-heading',\n cell: '.cb-column',\n cbnavbarbreadcrumb: '.cb-navbar-breadbrumb',\n cbnavbartotalsearch: '.cb-navbar-totalsearch',\n searchinput: '[data-action=\"search\"]',\n sortbutton: '.cb-btnsort'\n },\n};\n"],"names":["getDataSelector","name","value","regions","cbcontentname","contentbank","filearea","actions","search","clearSearch","viewgrid","viewlist","sortname","sortuses","sortdate","sortsize","sorttype","sortauthor","elements","listitem","heading","cell","cbnavbarbreadcrumb","cbnavbartotalsearch","searchinput","sortbutton"],"mappings":";;;;;;;;MA+BMA,gBAAkB,CAACC,KAAMC,wBACXD,kBAASC,yBAGd,CACXC,QAAS,CACLC,cAAeJ,gBAAgB,SAAU,mBACzCK,YAAaL,gBAAgB,SAAU,eACvCM,SAAUN,gBAAgB,SAAU,aAExCO,QAAS,CACLC,OAAQR,gBAAgB,SAAU,iBAClCS,YAAaT,gBAAgB,SAAU,eACvCU,SAAUV,gBAAgB,SAAU,YACpCW,SAAUX,gBAAgB,SAAU,YACpCY,SAAUZ,gBAAgB,SAAU,YACpCa,SAAUb,gBAAgB,SAAU,YACpCc,SAAUd,gBAAgB,SAAU,YACpCe,SAAUf,gBAAgB,SAAU,YACpCgB,SAAUhB,gBAAgB,SAAU,YACpCiB,WAAYjB,gBAAgB,SAAU,eAE1CkB,SAAU,CACNC,SAAU,eACVC,QAAS,cACTC,KAAM,aACNC,mBAAoB,wBACpBC,oBAAqB,yBACrBC,YAAa,yBACbC,WAAY"}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+10
View File
@@ -0,0 +1,10 @@
define("core_contentbank/upload",["exports","core_form/modalform","core/str"],(function(_exports,_modalform,_str){var obj;
/**
* Module to handle AJAX interactions with content bank upload files.
*
* @module core_contentbank/upload
* @copyright 2021 Sara Arjona <sara@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.initModal=void 0,_modalform=(obj=_modalform)&&obj.__esModule?obj:{default:obj};_exports.initModal=(elementSelector,formClass,contextId,contentId)=>{document.querySelector(elementSelector).addEventListener("click",(function(e){e.preventDefault();const form=new _modalform.default({formClass:formClass,args:{contextid:contextId,id:contentId},modalConfig:{title:(0,_str.getString)("upload","contentbank")},returnFocus:e.target});form.addEventListener(form.events.FORM_SUBMITTED,(event=>{document.location=event.detail.returnurl})),form.show()}))}}));
//# sourceMappingURL=upload.min.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"upload.min.js","sources":["../src/upload.js"],"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 <http://www.gnu.org/licenses/>.\n\n/**\n * Module to handle AJAX interactions with content bank upload files.\n *\n * @module core_contentbank/upload\n * @copyright 2021 Sara Arjona <sara@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport ModalForm from 'core_form/modalform';\nimport {getString} from 'core/str';\n\n/**\n * Initialize upload files to the content bank form as Modal form.\n *\n * @param {String} elementSelector\n * @param {String} formClass\n * @param {Integer} contextId\n * @param {Integer} contentId\n */\nexport const initModal = (elementSelector, formClass, contextId, contentId) => {\n const element = document.querySelector(elementSelector);\n element.addEventListener('click', function(e) {\n e.preventDefault();\n const form = new ModalForm({\n formClass,\n args: {\n contextid: contextId,\n id: contentId,\n },\n modalConfig: {title: getString('upload', 'contentbank')},\n returnFocus: e.target,\n });\n form.addEventListener(form.events.FORM_SUBMITTED, (event) => {\n document.location = event.detail.returnurl;\n });\n form.show();\n });\n};\n"],"names":["elementSelector","formClass","contextId","contentId","document","querySelector","addEventListener","e","preventDefault","form","ModalForm","args","contextid","id","modalConfig","title","returnFocus","target","events","FORM_SUBMITTED","event","location","detail","returnurl","show"],"mappings":";;;;;;;wKAiCyB,CAACA,gBAAiBC,UAAWC,UAAWC,aAC7CC,SAASC,cAAcL,iBAC/BM,iBAAiB,SAAS,SAASC,GACvCA,EAAEC,uBACIC,KAAO,IAAIC,mBAAU,CACvBT,UAAAA,UACAU,KAAM,CACFC,UAAWV,UACXW,GAAIV,WAERW,YAAa,CAACC,OAAO,kBAAU,SAAU,gBACzCC,YAAaT,EAAEU,SAEnBR,KAAKH,iBAAiBG,KAAKS,OAAOC,gBAAiBC,QAC/ChB,SAASiB,SAAWD,MAAME,OAAOC,aAErCd,KAAKe"}
+401
View File
@@ -0,0 +1,401 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Module to manage content bank actions, such as delete or rename.
*
* @module core_contentbank/actions
* @copyright 2020 Sara Arjona <sara@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define([
'jquery',
'core/ajax',
'core/notification',
'core/str',
'core/templates',
'core/url',
'core/modal_save_cancel',
'core/modal_events'],
function($, Ajax, Notification, Str, Templates, Url, ModalSaveCancel, ModalEvents) {
/**
* List of action selectors.
*
* @type {{DELETE_CONTENT: string}}
*/
var ACTIONS = {
DELETE_CONTENT: '[data-action="deletecontent"]',
RENAME_CONTENT: '[data-action="renamecontent"]',
SET_CONTENT_VISIBILITY: '[data-action="setcontentvisibility"]',
COPY_CONTENT: '[data-action="copycontent"]',
};
/**
* Actions class.
*/
var Actions = function() {
this.registerEvents();
};
/**
* Register event listeners.
*/
Actions.prototype.registerEvents = function() {
$(ACTIONS.DELETE_CONTENT).click(function(e) {
e.preventDefault();
var contentname = $(this).data('contentname');
var contentuses = $(this).data('uses');
var contentid = $(this).data('contentid');
var contextid = $(this).data('contextid');
var strings = [
{
key: 'deletecontent',
component: 'core_contentbank'
},
{
key: 'deletecontentconfirm',
component: 'core_contentbank',
param: {
name: contentname,
}
},
{
key: 'deletecontentconfirmlinked',
component: 'core_contentbank',
},
{
key: 'delete',
component: 'core'
},
];
var deleteButtonText = '';
Str.get_strings(strings).then(function(langStrings) {
var modalTitle = langStrings[0];
var modalContent = langStrings[1];
if (contentuses > 0) {
modalContent += ' ' + langStrings[2];
}
deleteButtonText = langStrings[3];
return ModalSaveCancel.create({
title: modalTitle,
body: modalContent,
large: true,
removeOnClose: true,
show: true,
buttons: {
save: deleteButtonText,
},
});
}).then(function(modal) {
modal.getRoot().on(ModalEvents.save, function() {
// The action is now confirmed, sending an action for it.
return deleteContent(contentid, contextid);
});
return;
}).catch(Notification.exception);
});
$(ACTIONS.RENAME_CONTENT).click(function(e) {
e.preventDefault();
var contentname = $(this).data('contentname');
var contentid = $(this).data('contentid');
var strings = [
{
key: 'renamecontent',
component: 'core_contentbank'
},
{
key: 'rename',
component: 'core_contentbank'
},
];
var saveButtonText = '';
Str.get_strings(strings).then(function(langStrings) {
var modalTitle = langStrings[0];
saveButtonText = langStrings[1];
return ModalSaveCancel.create({
title: modalTitle,
body: Templates.render('core_contentbank/renamecontent', {'contentid': contentid, 'name': contentname}),
removeOnClose: true,
show: true,
buttons: {
save: saveButtonText,
},
});
}).then(function(modal) {
modal.getRoot().on(ModalEvents.save, function(e) {
// The action is now confirmed, sending an action for it.
var newname = $("#newname").val().trim();
if (newname) {
renameContent(contentid, newname);
} else {
var errorStrings = [
{
key: 'error',
},
{
key: 'emptynamenotallowed',
component: 'core_contentbank',
},
];
Str.get_strings(errorStrings).then(function(langStrings) {
Notification.alert(langStrings[0], langStrings[1]);
}).catch(Notification.exception);
e.preventDefault();
}
});
return;
}).catch(Notification.exception);
});
$(ACTIONS.COPY_CONTENT).click(function(e) {
e.preventDefault();
var contentname = $(this).data('contentname');
var contentid = $(this).data('contentid');
var strings = [
{
key: 'copycontent',
component: 'core_contentbank'
},
{
key: 'error',
},
{
key: 'emptynamenotallowed',
component: 'core_contentbank',
},
];
let errorTitle, errorMessage;
Str.get_strings(strings).then(function(langStrings) {
var modalTitle = langStrings[0];
errorTitle = langStrings[1];
errorMessage = langStrings[2];
return ModalSaveCancel.create({
title: modalTitle,
body: Templates.render('core_contentbank/copycontent', {'contentid': contentid, 'name': contentname}),
removeOnClose: true,
show: true,
});
}).then(function(modal) {
modal.getRoot().on(ModalEvents.save, function() {
// The action is now confirmed, sending an action for it.
var newname = $("#newname").val().trim();
if (newname) {
copyContent(contentid, newname);
} else {
Notification.alert(errorTitle, errorMessage);
return false;
}
});
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);
});
};
/**
* Delete content from the content bank.
*
* @param {int} contentid The content to delete.
* @param {int} contextid The contextid where the content belongs.
*/
function deleteContent(contentid, contextid) {
var request = {
methodname: 'core_contentbank_delete_content',
args: {
contentids: {contentid}
}
};
var requestType = 'success';
Ajax.call([request])[0].then(function(data) {
if (data.result) {
return 'contentdeleted';
}
requestType = 'error';
return 'contentnotdeleted';
}).done(function(message) {
var params = {
contextid: contextid
};
if (requestType == 'success') {
params.statusmsg = message;
} else {
params.errormsg = message;
}
// Redirect to the main content bank page and display the message as a notification.
window.location.href = Url.relativeUrl('contentbank/index.php', params, false);
}).fail(Notification.exception);
}
/**
* Rename content in the content bank.
*
* @param {int} contentid The content to rename.
* @param {string} name The new name for the content.
*/
function renameContent(contentid, name) {
var request = {
methodname: 'core_contentbank_rename_content',
args: {
contentid: contentid,
name: name
}
};
var requestType = 'success';
Ajax.call([request])[0].then(function(data) {
if (data.result) {
return 'contentrenamed';
}
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);
}
/**
* Copy content in the content bank.
*
* @param {int} contentid The content to copy.
* @param {string} name The name for the new content.
*/
function copyContent(contentid, name) {
var request = {
methodname: 'core_contentbank_copy_content',
args: {
contentid: contentid,
name: name
}
};
Ajax.call([request])[0].then(function(data) {
if (data.id == 0) {
// Fetch error notifications.
Notification.addNotification({
message: data.warnings[0].message,
type: 'error'
});
Notification.fetchNotifications();
return data.warnings[0].message;
} else {
let params = {
id: data.id,
statusmsg: 'contentcopied'
};
// Redirect to the content view page and display the message as a notification.
window.location.href = Url.relativeUrl('contentbank/view.php', params, false);
}
return '';
}).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.
/**
* Initialise the contentbank actions.
*
* @method init
* @return {Actions}
*/
'init': function() {
return new Actions();
}
};
});
+156
View File
@@ -0,0 +1,156 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Search methods for finding contents in the content bank.
*
* @module core_contentbank/search
* @copyright 2020 Sara Arjona <sara@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import $ from 'jquery';
import selectors from 'core_contentbank/selectors';
import {getString} from 'core/str';
import Pending from 'core/pending';
import {debounce} from 'core/utils';
/**
* Set up the search.
*
* @method init
*/
export const init = () => {
const pendingPromise = new Pending();
const root = $(selectors.regions.contentbank);
registerListenerEvents(root);
pendingPromise.resolve();
};
/**
* Register contentbank search related event listeners.
*
* @method registerListenerEvents
* @param {Object} root The root element for the contentbank.
*/
const registerListenerEvents = (root) => {
const searchInput = root.find(selectors.elements.searchinput)[0];
root.on('click', selectors.actions.search, function(e) {
e.preventDefault();
toggleSearchResultsView(root, searchInput.value);
});
root.on('click', selectors.actions.clearSearch, function(e) {
e.preventDefault();
searchInput.value = "";
searchInput.focus();
toggleSearchResultsView(root, searchInput.value);
});
// The search input is also triggered.
searchInput.addEventListener('input', debounce(() => {
// Display the search results.
toggleSearchResultsView(root, searchInput.value);
}, 300));
};
/**
* Toggle (display/hide) the search results depending on the value of the search query.
*
* @method toggleSearchResultsView
* @param {HTMLElement} body The root element for the contentbank.
* @param {String} searchQuery The search query.
*/
const toggleSearchResultsView = async(body, searchQuery) => {
const clearSearchButton = body.find(selectors.actions.clearSearch)[0];
const navbarBreadcrumb = body.find(selectors.elements.cbnavbarbreadcrumb)[0];
const navbarTotal = body.find(selectors.elements.cbnavbartotalsearch)[0];
// Update the results.
const filteredContents = filterContents(body, searchQuery);
if (searchQuery.length > 0) {
// As the search query is present, search results should be displayed.
// Display the "clear" search button in the activity chooser search bar.
clearSearchButton.classList.remove('d-none');
// Change the cb-navbar to display total items found.
navbarBreadcrumb.classList.add('d-none');
navbarTotal.innerHTML = await getString('itemsfound', 'core_contentbank', filteredContents.length);
navbarTotal.classList.remove('d-none');
} else {
// As search query is not present, the search results should be removed.
// Hide the "clear" search button in the activity chooser search bar.
clearSearchButton.classList.add('d-none');
// Display again the breadcrumb in the navbar.
navbarBreadcrumb.classList.remove('d-none');
navbarTotal.classList.add('d-none');
}
};
/**
* Return the list of contents which have a name that matches the given search term.
*
* @method filterContents
* @param {HTMLElement} body The root element for the contentbank.
* @param {String} searchTerm The search term to match.
* @return {Array}
*/
const filterContents = (body, searchTerm) => {
const contents = Array.from(body.find(selectors.elements.listitem));
const searchResults = [];
contents.forEach((content) => {
const contentName = content.getAttribute('data-name');
if (searchTerm === '' || contentName.toLowerCase().includes(searchTerm.toLowerCase())) {
// The content matches the search criteria so it should be displayed and hightlighted.
searchResults.push(content);
const contentNameElement = content.querySelector(selectors.regions.cbcontentname);
contentNameElement.innerHTML = highlight(contentName, searchTerm);
content.classList.remove('d-none');
} else {
content.classList.add('d-none');
}
});
return searchResults;
};
/**
* Highlight a given string in a text.
*
* @method highlight
* @param {String} text The whole text.
* @param {String} highlightText The piece of text to highlight.
* @return {String}
*/
const highlight = (text, highlightText) => {
let result = text;
if (highlightText !== '') {
const pos = text.toLowerCase().indexOf(highlightText.toLowerCase());
if (pos > -1) {
result = text.substr(0, pos) + '<span class="matchtext">' + text.substr(pos, highlightText.length) + '</span>' +
text.substr(pos + highlightText.length);
}
}
return result;
};
+63
View File
@@ -0,0 +1,63 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Define all of the selectors we will be using on the contentbank interface.
*
* @module core_contentbank/selectors
* @copyright 2020 Sara Arjona <sara@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* A small helper function to build queryable data selectors.
*
* @method getDataSelector
* @param {String} name
* @param {String} value
* @return {string}
*/
const getDataSelector = (name, value) => {
return `[data-${name}="${value}"]`;
};
export default {
regions: {
cbcontentname: getDataSelector('region', 'cb-content-name'),
contentbank: getDataSelector('region', 'contentbank'),
filearea: getDataSelector('region', 'filearea')
},
actions: {
search: getDataSelector('action', 'searchcontent'),
clearSearch: getDataSelector('action', 'clearsearch'),
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'),
sortauthor: getDataSelector('action', 'sortauthor'),
},
elements: {
listitem: '.cb-listitem',
heading: '.cb-heading',
cell: '.cb-column',
cbnavbarbreadcrumb: '.cb-navbar-breadbrumb',
cbnavbartotalsearch: '.cb-navbar-totalsearch',
searchinput: '[data-action="search"]',
sortbutton: '.cb-btnsort'
},
};
+269
View File
@@ -0,0 +1,269 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Content bank UI actions.
*
* @module core_contentbank/sort
* @copyright 2020 Bas Brands <bas@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import selectors from './selectors';
import {getString} from 'core/str';
import Prefetch from 'core/prefetch';
import Notification from 'core/notification';
import {setUserPreference} from 'core_user/repository';
/**
* Set up the contentbank views.
*
* @method init
*/
export const init = () => {
const contentBank = document.querySelector(selectors.regions.contentbank);
Prefetch.prefetchStrings('contentbank', ['contentname', 'uses', 'lastmodified', 'size', 'type', 'author']);
Prefetch.prefetchStrings('moodle', ['sortbyx', 'sortbyxreverse']);
registerListenerEvents(contentBank);
};
/**
* Register contentbank related event listeners.
*
* @method registerListenerEvents
* @param {HTMLElement} contentBank The DOM node of the content bank
*/
const registerListenerEvents = (contentBank) => {
contentBank.addEventListener('click', e => {
const viewList = contentBank.querySelector(selectors.actions.viewlist);
const viewGrid = contentBank.querySelector(selectors.actions.viewgrid);
const fileArea = contentBank.querySelector(selectors.regions.filearea);
const shownItems = fileArea.querySelectorAll(selectors.elements.listitem);
// View as Grid button.
if (e.target.closest(selectors.actions.viewgrid)) {
contentBank.classList.remove('view-list');
contentBank.classList.add('view-grid');
if (fileArea && shownItems) {
fileArea.setAttribute('role', 'list');
shownItems.forEach(listItem => {
listItem.setAttribute('role', 'listitem');
listItem.querySelectorAll(selectors.elements.cell).forEach(cell => cell.removeAttribute('role'));
});
const heading = fileArea.querySelector(selectors.elements.heading);
if (heading) {
heading.removeAttribute('role');
heading.querySelectorAll(selectors.elements.cell).forEach(cell => cell.removeAttribute('role'));
}
}
viewGrid.classList.add('active');
viewList.classList.remove('active');
setViewListPreference(false);
return;
}
// View as List button.
if (e.target.closest(selectors.actions.viewlist)) {
contentBank.classList.remove('view-grid');
contentBank.classList.add('view-list');
if (fileArea && shownItems) {
fileArea.setAttribute('role', 'table');
shownItems.forEach(listItem => {
listItem.setAttribute('role', 'row');
listItem.querySelectorAll(selectors.elements.cell).forEach(cell => cell.setAttribute('role', 'cell'));
});
const heading = fileArea.querySelector(selectors.elements.heading);
if (heading) {
heading.setAttribute('role', 'row');
heading.querySelectorAll(selectors.elements.cell).forEach(cell => cell.setAttribute('role', 'columnheader'));
}
}
viewList.classList.add('active');
viewGrid.classList.remove('active');
setViewListPreference(true);
return;
}
if (fileArea && shownItems) {
// Sort by file name alphabetical
const sortByName = e.target.closest(selectors.actions.sortname);
if (sortByName) {
const ascending = updateSortButtons(contentBank, sortByName);
updateSortOrder(fileArea, shownItems, 'data-file', ascending);
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) {
const ascending = updateSortButtons(contentBank, sortByDate);
updateSortOrder(fileArea, shownItems, 'data-timemodified', ascending);
return;
}
// Sort by size.
const sortBySize = e.target.closest(selectors.actions.sortsize);
if (sortBySize) {
const ascending = updateSortButtons(contentBank, sortBySize);
updateSortOrder(fileArea, shownItems, 'data-bytes', ascending);
return;
}
// Sort by type.
const sortByType = e.target.closest(selectors.actions.sorttype);
if (sortByType) {
const ascending = updateSortButtons(contentBank, sortByType);
updateSortOrder(fileArea, shownItems, 'data-type', ascending);
return;
}
// Sort by author.
const sortByAuthor = e.target.closest(selectors.actions.sortauthor);
if (sortByAuthor) {
const ascending = updateSortButtons(contentBank, sortByAuthor);
updateSortOrder(fileArea, shownItems, 'data-author', ascending);
}
return;
}
});
};
/**
* Set the contentbank user preference in list view
*
* @param {Bool} viewList view ContentBank as list.
* @return {Promise} Repository promise.
*/
const setViewListPreference = function(viewList) {
// If the given status is not hidden, the preference has to be deleted with a null value.
if (viewList === false) {
viewList = null;
}
return setUserPreference('core_contentbank_view_list', viewList)
.catch(Notification.exception);
};
/**
* Update the sort button view.
*
* @method updateSortButtons
* @param {HTMLElement} contentBank The DOM node of the contentbank button
* @param {HTMLElement} sortButton The DOM node of the sort button
* @return {Bool} sort ascending
*/
const updateSortButtons = (contentBank, sortButton) => {
const sortButtons = contentBank.querySelectorAll(selectors.elements.sortbutton);
sortButtons.forEach((button) => {
if (button !== sortButton) {
button.classList.remove('dir-asc');
button.classList.remove('dir-desc');
button.classList.add('dir-none');
button.closest(selectors.elements.cell).setAttribute('aria-sort', 'none');
updateButtonTitle(button, false);
}
});
let ascending = true;
if (sortButton.classList.contains('dir-none')) {
sortButton.classList.remove('dir-none');
sortButton.classList.add('dir-asc');
sortButton.closest(selectors.elements.cell).setAttribute('aria-sort', 'ascending');
} else if (sortButton.classList.contains('dir-asc')) {
sortButton.classList.remove('dir-asc');
sortButton.classList.add('dir-desc');
sortButton.closest(selectors.elements.cell).setAttribute('aria-sort', 'descending');
ascending = false;
} else if (sortButton.classList.contains('dir-desc')) {
sortButton.classList.remove('dir-desc');
sortButton.classList.add('dir-asc');
sortButton.closest(selectors.elements.cell).setAttribute('aria-sort', 'ascending');
}
updateButtonTitle(sortButton, ascending);
return ascending;
};
/**
* Update the button title.
*
* @method updateButtonTitle
* @param {HTMLElement} button Button to update
* @param {Bool} ascending Sort direction
* @return {Promise} string promise
*/
const updateButtonTitle = (button, ascending) => {
const sortString = (ascending ? 'sortbyxreverse' : 'sortbyx');
return getString(button.dataset.string, 'contentbank')
.then(columnName => {
return getString(sortString, 'core', columnName);
})
.then(sortByString => {
button.setAttribute('title', sortByString);
return sortByString;
})
.catch();
};
/**
* Update the sort order of the itemlist and update the DOM
*
* @method updateSortOrder
* @param {HTMLElement} fileArea the Dom container for the itemlist
* @param {Array} itemList Nodelist of Dom elements
* @param {String} attribute the attribut to sort on
* @param {Bool} ascending Sort Ascending
*/
const updateSortOrder = (fileArea, itemList, attribute, ascending) => {
const sortList = [].slice.call(itemList).sort(function(a, b) {
let aa = a.getAttribute(attribute);
let bb = b.getAttribute(attribute);
if (!isNaN(aa)) {
aa = parseInt(aa);
bb = parseInt(bb);
}
if (ascending) {
return aa > bb ? 1 : -1;
} else {
return aa < bb ? 1 : -1;
}
});
sortList.forEach(listItem => fileArea.appendChild(listItem));
};
+52
View File
@@ -0,0 +1,52 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Module to handle AJAX interactions with content bank upload files.
*
* @module core_contentbank/upload
* @copyright 2021 Sara Arjona <sara@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import ModalForm from 'core_form/modalform';
import {getString} from 'core/str';
/**
* Initialize upload files to the content bank form as Modal form.
*
* @param {String} elementSelector
* @param {String} formClass
* @param {Integer} contextId
* @param {Integer} contentId
*/
export const initModal = (elementSelector, formClass, contextId, contentId) => {
const element = document.querySelector(elementSelector);
element.addEventListener('click', function(e) {
e.preventDefault();
const form = new ModalForm({
formClass,
args: {
contextid: contextId,
id: contentId,
},
modalConfig: {title: getString('upload', 'contentbank')},
returnFocus: e.target,
});
form.addEventListener(form.events.FORM_SUBMITTED, (event) => {
document.location = event.detail.returnurl;
});
form.show();
});
};
+400
View File
@@ -0,0 +1,400 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Content manager class
*
* @package core_contentbank
* @copyright 2020 Amaia Anabitarte <amaia@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_contentbank;
use core_text;
use stored_file;
use stdClass;
use coding_exception;
use context;
use moodle_url;
use core\event\contentbank_content_updated;
/**
* Content manager class
*
* @package core_contentbank
* @copyright 2020 Amaia Anabitarte <amaia@moodle.com>
* @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;
/**
* Content bank constructor
*
* @param stdClass $record A contentbank_content record.
* @throws coding_exception If content type is not right.
*/
public function __construct(stdClass $record) {
// Content type should exist and be linked to plugin classname.
$classname = $record->contenttype.'\\content';
if (get_class($this) != $classname) {
throw new coding_exception(get_string('contenttypenotfound', 'error', $record->contenttype));
}
$typeclass = $record->contenttype.'\\contenttype';
if (!class_exists($typeclass)) {
throw new coding_exception(get_string('contenttypenotfound', 'error', $record->contenttype));
}
// A record with the id must exist in 'contentbank_content' table.
// To improve performance, we are only checking the id is set, but no querying the database.
if (!isset($record->id)) {
throw new coding_exception(get_string('invalidcontentid', 'error'));
}
$this->content = $record;
}
/**
* Returns $this->content.
*
* @return stdClass $this->content.
*/
public function get_content(): stdClass {
return $this->content;
}
/**
* Returns $this->content->contenttype.
*
* @return string $this->content->contenttype.
*/
public function get_content_type(): string {
return $this->content->contenttype;
}
/**
* Return the contenttype instance of this content.
*
* @return contenttype The content type instance
*/
public function get_content_type_instance(): contenttype {
$context = context::instance_by_id($this->content->contextid);
$contenttypeclass = "\\{$this->content->contenttype}\\contenttype";
return new $contenttypeclass($context);
}
/**
* Returns $this->content->timemodified.
*
* @return int $this->content->timemodified.
*/
public function get_timemodified(): int {
return $this->content->timemodified;
}
/**
* Updates content_bank table with information in $this->content.
*
* @return boolean True if the content has been succesfully updated. False otherwise.
* @throws \coding_exception if not loaded.
*/
public function update_content(): bool {
global $USER, $DB;
// A record with the id must exist in 'contentbank_content' table.
// To improve performance, we are only checking the id is set, but no querying the database.
if (!isset($this->content->id)) {
throw new coding_exception(get_string('invalidcontentid', 'error'));
}
$this->content->usermodified = $USER->id;
$this->content->timemodified = time();
$result = $DB->update_record('contentbank_content', $this->content);
if ($result) {
// Trigger an event for updating this content.
$event = contentbank_content_updated::create_from_record($this->content);
$event->trigger();
}
return $result;
}
/**
* Set a new name to the content.
*
* @param string $name The name of the content.
* @return bool True if the content has been succesfully updated. False otherwise.
* @throws \coding_exception if not loaded.
*/
public function set_name(string $name): bool {
$name = trim($name);
if ($name === '') {
return false;
}
// Clean name.
$name = clean_param($name, PARAM_TEXT);
if (core_text::strlen($name) > 255) {
$name = core_text::substr($name, 0, 255);
}
$oldname = $this->content->name;
$this->content->name = $name;
$updated = $this->update_content();
if (!$updated) {
$this->content->name = $oldname;
}
return $updated;
}
/**
* Returns the name of the content.
*
* @return string The name of the content.
*/
public function get_name(): string {
return $this->content->name;
}
/**
* Set a new contextid to the content.
*
* @param int $contextid The new contextid of the content.
* @return bool True if the content has been succesfully updated. False otherwise.
*/
public function set_contextid(int $contextid): bool {
if ($this->content->contextid == $contextid) {
return true;
}
$oldcontextid = $this->content->contextid;
$this->content->contextid = $contextid;
$updated = $this->update_content();
if ($updated) {
// Move files to new context
$fs = get_file_storage();
$fs->move_area_files_to_new_context($oldcontextid, $contextid, 'contentbank', 'public', $this->content->id);
} else {
$this->content->contextid = $oldcontextid;
}
return $updated;
}
/**
* Returns the contextid of the content.
*
* @return int The id of the content context.
*/
public function get_contextid(): string {
return $this->content->contextid;
}
/**
* Returns the content ID.
*
* @return int The content ID.
*/
public function get_id(): int {
return $this->content->id;
}
/**
* Change the content instanceid value.
*
* @param int $instanceid New instanceid for this content
* @return boolean True if the instanceid has been succesfully updated. False otherwise.
*/
public function set_instanceid(int $instanceid): bool {
$this->content->instanceid = $instanceid;
return $this->update_content();
}
/**
* Returns the $instanceid of this content.
*
* @return int contentbank instanceid
*/
public function get_instanceid(): int {
return $this->content->instanceid;
}
/**
* Change the content config values.
*
* @param string $configdata New config information for this content
* @return boolean True if the configdata has been succesfully updated. False otherwise.
*/
public function set_configdata(string $configdata): bool {
$this->content->configdata = $configdata;
return $this->update_content();
}
/**
* Return the content config values.
*
* @return mixed Config information for this content (json decoded)
*/
public function get_configdata() {
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.
*
* By default, all content has a public file area to interact with the content bank
* repository. This method should be overridden by contentypes which does not simply
* upload to the public file area.
*
* If any, the method will return the final stored_file. This way it can be invoked
* as parent::import_file in case any plugin want to store the file in the public area
* and also parse it.
*
* @param stored_file $file File to store in the content file area.
* @return stored_file|null the stored content file or null if the file is discarted.
*/
public function import_file(stored_file $file): ?stored_file {
$originalfile = $this->get_file();
if ($originalfile) {
$originalfile->replace_file_with($file);
return $originalfile;
} else {
$fs = get_file_storage();
$filerecord = [
'contextid' => $this->get_contextid(),
'component' => 'contentbank',
'filearea' => 'public',
'itemid' => $this->get_id(),
'filepath' => '/',
'filename' => $file->get_filename(),
'timecreated' => time(),
];
return $fs->create_file_from_storedfile($filerecord, $file);
}
}
/**
* Returns the $file related to this content.
*
* @return stored_file File stored in content bank area related to the given itemid.
* @throws \coding_exception if not loaded.
*/
public function get_file(): ?stored_file {
$itemid = $this->get_id();
$fs = get_file_storage();
$files = $fs->get_area_files(
$this->content->contextid,
'contentbank',
'public',
$itemid,
'itemid, filepath, filename',
false
);
if (!empty($files)) {
$file = reset($files);
return $file;
}
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.
*
* @return string URL of the file stored in content bank area related to the given itemid.
* @throws \coding_exception if not loaded.
*/
public function get_file_url(): string {
if (!$file = $this->get_file()) {
return '';
}
$fileurl = moodle_url::make_pluginfile_url(
$this->content->contextid,
'contentbank',
'public',
$file->get_itemid(),
$file->get_filepath(),
$file->get_filename()
);
return $fileurl;
}
/**
* Returns user has access permission for the content itself (based on what plugin needs).
*
* @return bool True if content could be accessed. False otherwise.
*/
public function is_view_allowed(): bool {
// 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);
}
}
+380
View File
@@ -0,0 +1,380 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_contentbank;
use core_plugin_manager;
use stored_file;
use context;
/**
* Content bank class
*
* @package core_contentbank
* @copyright 2020 Amaia Anabitarte <amaia@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class contentbank {
/** @var array All the context levels allowed in the content bank */
private const ALLOWED_CONTEXT_LEVELS = [CONTEXT_SYSTEM, CONTEXT_COURSECAT, CONTEXT_COURSE];
/** @var array Enabled content types. */
private $enabledcontenttypes = null;
/**
* Obtains the list of core_contentbank_content objects currently active.
*
* The list does not include players which are disabled.
*
* @return string[] Array of contentbank contenttypes.
*/
public function get_enabled_content_types(): array {
if (!is_null($this->enabledcontenttypes)) {
return $this->enabledcontenttypes;
}
$enabledtypes = \core\plugininfo\contenttype::get_enabled_plugins();
$types = [];
foreach ($enabledtypes as $name) {
$contenttypeclassname = "\\contenttype_$name\\contenttype";
$contentclassname = "\\contenttype_$name\\content";
if (class_exists($contenttypeclassname) && class_exists($contentclassname)) {
$types[$contenttypeclassname] = $name;
}
}
return $this->enabledcontenttypes = $types;
}
/**
* Obtains an array of supported extensions by active plugins.
*
* @return array The array with all the extensions supported and the supporting plugin names.
*/
public function load_all_supported_extensions(): array {
$extensionscache = \cache::make('core', 'contentbank_enabled_extensions');
$supportedextensions = $extensionscache->get('enabled_extensions');
if ($supportedextensions === false) {
// Load all enabled extensions.
$supportedextensions = [];
foreach ($this->get_enabled_content_types() as $type) {
$classname = "\\contenttype_$type\\contenttype";
$contenttype = new $classname;
if ($contenttype->is_feature_supported($contenttype::CAN_UPLOAD)) {
$extensions = $contenttype->get_manageable_extensions();
foreach ($extensions as $extension) {
if (array_key_exists($extension, $supportedextensions)) {
$supportedextensions[$extension][] = $type;
} else {
$supportedextensions[$extension] = [$type];
}
}
}
}
$extensionscache->set('enabled_extensions', $supportedextensions);
}
return $supportedextensions;
}
/**
* Obtains an array of supported extensions in the given context.
*
* @param context $context Optional context to check (default null)
* @return array The array with all the extensions supported and the supporting plugin names.
*/
public function load_context_supported_extensions(context $context = null): array {
$extensionscache = \cache::make('core', 'contentbank_context_extensions');
$contextextensions = $extensionscache->get($context->id);
if ($contextextensions === false) {
$contextextensions = [];
$supportedextensions = $this->load_all_supported_extensions();
foreach ($supportedextensions as $extension => $types) {
foreach ($types as $type) {
$classname = "\\contenttype_$type\\contenttype";
$contenttype = new $classname($context);
if ($contenttype->can_upload()) {
$contextextensions[$extension] = $type;
break;
}
}
}
$extensionscache->set($context->id, $contextextensions);
}
return $contextextensions;
}
/**
* Obtains a string with all supported extensions by active plugins.
* Mainly to use as filepicker options parameter.
*
* @param context $context Optional context to check (default null)
* @return string A string with all the extensions supported.
*/
public function get_supported_extensions_as_string(context $context = null) {
$supported = $this->load_context_supported_extensions($context);
$extensions = array_keys($supported);
return implode(',', $extensions);
}
/**
* Returns the file extension for a file.
*
* @param string $filename The name of the file
* @return string The extension of the file
*/
public function get_extension(string $filename) {
$dot = strrpos($filename, '.');
if ($dot === false) {
return '';
}
return strtolower(substr($filename, $dot));
}
/**
* Get the first content bank plugin supports a file extension.
*
* @param string $extension Content file extension
* @param context $context $context Optional context to check (default null)
* @return string contenttype name supports the file extension or null if the extension is not supported by any allowed plugin.
*/
public function get_extension_supporter(string $extension, context $context = null): ?string {
$supporters = $this->load_context_supported_extensions($context);
if (array_key_exists($extension, $supporters)) {
return $supporters[$extension];
}
return null;
}
/**
* Find the contents with %$search% in the contextid defined.
* If contextid and search are empty, all contents are returned.
* In all the cases, only the contents for the enabled contentbank-type plugins are returned.
* No content-type permissions are validated here. It is the caller responsability to check that the user can access to them.
* The only validation done here is, for each content, a call to the method $content->is_view_allowed().
*
* @param string|null $search Optional string to search (for now it will search only into the name).
* @param int $contextid Optional contextid to search.
* @param array $contenttypenames Optional array with the list of content-type names to search.
* @return array The contents for the enabled contentbank-type plugins having $search as name and placed in $contextid.
*/
public function search_contents(?string $search = null, ?int $contextid = 0, ?array $contenttypenames = null): array {
global $DB;
$contents = [];
// Get only contents for enabled content-type plugins.
$contenttypes = [];
$enabledcontenttypes = $this->get_enabled_content_types();
foreach ($enabledcontenttypes as $contenttypename) {
if (empty($contenttypenames) || in_array($contenttypename, $contenttypenames)) {
$contenttypes[] = "contenttype_$contenttypename";
}
}
if (empty($contenttypes)) {
// Early return if there are no content-type plugins enabled.
return $contents;
}
list($sqlcontenttypes, $params) = $DB->get_in_or_equal($contenttypes, SQL_PARAMS_NAMED);
$sql = " contenttype $sqlcontenttypes ";
// Filter contents on this context (if defined).
if (!empty($contextid)) {
$params['contextid'] = $contextid;
$sql .= ' AND contextid = :contextid ';
}
// Search for contents having this string (if defined).
if (!empty($search)) {
$sql .= ' AND ' . $DB->sql_like('name', ':name', false, false);
$params['name'] = '%' . $DB->sql_like_escape($search) . '%';
}
$records = $DB->get_records_select('contentbank_content', $sql, $params, 'name ASC');
foreach ($records as $record) {
$content = $this->get_content_from_id($record->id);
if ($content->is_view_allowed()) {
$contents[] = $content;
}
}
return $contents;
}
/**
* Return all the context where a user has all the given capabilities.
*
* @param string $capability The capability the user needs to have.
* @param int|null $userid Optional userid. $USER by default.
* @return array Array of the courses and course categories where the user has the given capability.
*/
public function get_contexts_with_capabilities_by_user($capability = 'moodle/contentbank:access', $userid = null): array {
global $USER;
if (!$userid) {
$userid = $USER->id;
}
$categoriescache = \cache::make('core', 'contentbank_allowed_categories');
$coursescache = \cache::make('core', 'contentbank_allowed_courses');
$categories = $categoriescache->get($userid);
$courses = $coursescache->get($userid);
if ($categories === false || $courses === false) {
// Required fields for preloading the context record.
$contextfields = 'ctxid, ctxpath, ctxdepth, ctxlevel, ctxinstance, ctxlocked';
list($categories, $courses) = get_user_capability_contexts($capability, true, $userid, true,
"fullname, {$contextfields}", "name, {$contextfields}", 'fullname', 'name');
$categoriescache->set($userid, $categories);
$coursescache->set($userid, $courses);
}
return [$categories, $courses];
}
/**
* Create content from a file information.
*
* @param \context $context Context where to upload the file and content.
* @param int $userid Id of the user uploading the file.
* @param stored_file $file The file to get information from
* @return content
*/
public function create_content_from_file(\context $context, int $userid, stored_file $file): ?content {
global $USER;
if (empty($userid)) {
$userid = $USER->id;
}
// Get the contenttype to manage given file's extension.
$filename = $file->get_filename();
$extension = $this->get_extension($filename);
$plugin = $this->get_extension_supporter($extension, $context);
$classname = '\\contenttype_'.$plugin.'\\contenttype';
$record = new \stdClass();
$record->name = $filename;
$record->usercreated = $userid;
$contentype = new $classname($context);
$content = $contentype->upload_content($file, $record);
$event = \core\event\contentbank_content_uploaded::create_from_record($content->get_content());
$event->trigger();
return $content;
}
/**
* Delete content bank content by context.
*
* @param context $context The context to delete content from.
* @return bool
*/
public function delete_contents(context $context): bool {
global $DB;
$result = true;
$records = $DB->get_records('contentbank_content', ['contextid' => $context->id]);
foreach ($records as $record) {
$content = $this->get_content_from_id($record->id);
$contenttype = $content->get_content_type_instance();
if (!$contenttype->delete_content($content)) {
$result = false;
}
}
return $result;
}
/**
* Move content bank content from a context to another.
*
* @param context $from The context to get content from.
* @param context $to The context to move content to.
* @return bool
*/
public function move_contents(context $from, context $to): bool {
global $DB;
$result = true;
$records = $DB->get_records('contentbank_content', ['contextid' => $from->id]);
foreach ($records as $record) {
$content = $this->get_content_from_id($record->id);
$contenttype = $content->get_content_type_instance();
if (!$contenttype->move_content($content, $to)) {
$result = false;
}
}
return $result;
}
/**
* Get the list of content types that have the requested feature.
*
* @param string $feature Feature code e.g CAN_UPLOAD.
* @param null|\context $context Optional context to check the permission to use the feature.
* @param bool $enabled Whether check only the enabled content types or all of them.
*
* @return string[] List of content types where the user has permission to access the feature.
*/
public function get_contenttypes_with_capability_feature(string $feature, \context $context = null, bool $enabled = true): array {
$contenttypes = [];
// Check enabled content types or all of them.
if ($enabled) {
$contenttypestocheck = $this->get_enabled_content_types();
} else {
$plugins = core_plugin_manager::instance()->get_plugins_of_type('contenttype');
foreach ($plugins as $plugin) {
$contenttypeclassname = "\\{$plugin->type}_{$plugin->name}\\contenttype";
$contenttypestocheck[$contenttypeclassname] = $plugin->name;
}
}
foreach ($contenttypestocheck as $classname => $name) {
$contenttype = new $classname($context);
// The method names that check the features permissions must follow the pattern can_feature.
if ($contenttype->{"can_$feature"}()) {
$contenttypes[$classname] = $name;
}
}
return $contenttypes;
}
/**
* Return a content class form a content id.
*
* @throws coding_exception if the ID is not valid or some class does no exists
* @param int $id the content id
* @return content the content class instance
*/
public function get_content_from_id(int $id): content {
global $DB;
$record = $DB->get_record('contentbank_content', ['id' => $id], '*', MUST_EXIST);
$contentclass = "\\$record->contenttype\\content";
return new $contentclass($record);
}
/**
* Whether the context is allowed.
*
* @param context $context Context to check.
* @return bool
*/
public function is_context_allowed(context $context): bool {
return in_array($context->contextlevel, self::ALLOWED_CONTEXT_LEVELS);
}
}
+554
View File
@@ -0,0 +1,554 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_contentbank;
use core\event\contentbank_content_created;
use core\event\contentbank_content_deleted;
use core\event\contentbank_content_viewed;
use stored_file;
use moodle_url;
/**
* Content type manager class
*
* @package core_contentbank
* @copyright 2020 Amaia Anabitarte <amaia@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class contenttype {
/** @var string Constant representing whether the plugin implements uploading feature */
const CAN_UPLOAD = 'upload';
/** @var string Constant representing whether the plugin implements edition feature */
const CAN_EDIT = 'edit';
/**
* @var string Constant representing whether the plugin implements download feature
* @since Moodle 3.10
*/
const CAN_DOWNLOAD = 'download';
/**
* @var string Constant representing whether the plugin implements copy feature
* @since Moodle 4.3
*/
const CAN_COPY = 'copy';
/** @var \context This contenttype's context. **/
protected $context = null;
/**
* Content type constructor
*
* @param \context $context Optional context to check (default null)
*/
public function __construct(\context $context = null) {
if (empty($context)) {
$context = \context_system::instance();
}
$this->context = $context;
}
/**
* Fills content_bank table with appropiate information.
*
* @throws dml_exception A DML specific exception is thrown for any creation error.
* @param \stdClass $record An optional content record compatible object (default null)
* @return content Object with content bank information.
*/
public function create_content(\stdClass $record = null): content {
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 ?? '';
$entry->usercreated = $record->usercreated ?? $USER->id;
$entry->timecreated = time();
$entry->usermodified = $entry->usercreated;
$entry->timemodified = $entry->timecreated;
$entry->configdata = $record->configdata ?? '';
$entry->instanceid = $record->instanceid ?? 0;
$entry->id = $DB->insert_record('contentbank_content', $entry);
$classname = '\\'.$entry->contenttype.'\\content';
$content = new $classname($entry);
// Trigger an event for creating the content.
$event = contentbank_content_created::create_from_record($content->get_content());
$event->trigger();
return $content;
}
/**
* Create a new content from an uploaded file.
*
* @throws file_exception If file operations fail
* @throws dml_exception if the content creation fails
* @param stored_file $file the uploaded file
* @param \stdClass|null $record an optional content record
* @return content Object with content bank information.
*/
public function upload_content(stored_file $file, \stdClass $record = null): content {
if (empty($record)) {
$record = new \stdClass();
$record->name = $file->get_filename();
}
$content = $this->create_content($record);
try {
$content->import_file($file);
} catch (\moodle_exception $e) {
$this->delete_content($content);
throw new \moodle_exception($e->errorcode);
}
return $content;
}
/**
* Replace a content using an uploaded file.
*
* @throws file_exception If file operations fail
* @throws dml_exception if the content creation fails
* @param stored_file $file the uploaded file
* @param content $content the original content record
* @return content Object with the updated content bank information.
*/
public function replace_content(stored_file $file, content $content): content {
$content->import_file($file);
$content->update_content();
return $content;
}
/**
* Delete this content from the content_bank.
* This method can be overwritten by the plugins if they need to delete specific information.
*
* @param content $content The content to delete.
* @return boolean true if the content has been deleted; false otherwise.
*/
public function delete_content(content $content): bool {
global $DB;
// Delete the file if it exists.
if ($file = $content->get_file()) {
$file->delete();
}
// Delete the contentbank DB entry.
$result = $DB->delete_records('contentbank_content', ['id' => $content->get_id()]);
if ($result) {
// Trigger an event for deleting this content.
$record = $content->get_content();
$event = contentbank_content_deleted::create([
'objectid' => $content->get_id(),
'relateduserid' => $record->usercreated,
'context' => \context::instance_by_id($record->contextid),
'other' => [
'contenttype' => $content->get_content_type(),
'name' => $content->get_name()
]
]);
$event->add_record_snapshot('contentbank_content', $record);
$event->trigger();
}
return $result;
}
/**
* Rename this content from the content_bank.
* This method can be overwritten by the plugins if they need to change some other specific information.
*
* @param content $content The content to rename.
* @param string $name The name of the content.
* @return boolean true if the content has been renamed; false otherwise.
*/
public function rename_content(content $content, string $name): bool {
return $content->set_name($name);
}
/**
* Move content to another context.
* This method can be overwritten by the plugins if they need to change some other specific information.
*
* @param content $content The content to rename.
* @param \context $context The new context.
* @return boolean true if the content has been renamed; false otherwise.
*/
public function move_content(content $content, \context $context): bool {
return $content->set_contextid($context->id);
}
/**
* Returns the contenttype name of this content.
*
* @return string Content type of the current instance
*/
public function get_contenttype_name(): string {
$classname = get_class($this);
$contenttype = explode('\\', $classname);
return array_shift($contenttype);
}
/**
* Returns the plugin name of the current instance.
*
* @return string Plugin name of the current instance
*/
public function get_plugin_name(): string {
$contenttype = $this->get_contenttype_name();
$plugin = explode('_', $contenttype);
return array_pop($plugin);
}
/**
* Returns the URL where the content will be visualized.
*
* @param content $content The content to be displayed.
* @return string URL where to visualize the given content.
*/
public function get_view_url(content $content): string {
return new moodle_url('/contentbank/view.php', ['id' => $content->get_id()]);
}
/**
* Returns the HTML content to add to view.php visualizer.
*
* @param content $content The content to be displayed.
* @return string HTML code to include in view.php.
*/
public function get_view_content(content $content): string {
// Trigger an event for viewing this content.
$event = contentbank_content_viewed::create_from_record($content->get_content());
$event->trigger();
return '';
}
/**
* Returns the URL to download the content.
*
* @since Moodle 3.10
* @param content $content The content to be downloaded.
* @return string URL with the content to download.
*/
public function get_download_url(content $content): string {
$downloadurl = '';
$file = $content->get_file();
if (!empty($file)) {
$url = \moodle_url::make_pluginfile_url(
$file->get_contextid(),
$file->get_component(),
$file->get_filearea(),
$file->get_itemid(),
$file->get_filepath(),
$file->get_filename()
);
$downloadurl = $url->out(false);
}
return $downloadurl;
}
/**
* Returns the HTML code to render the icon for content bank contents.
*
* @param content $content The content to be displayed.
* @return string HTML code to render the icon
*/
public function get_icon(content $content): string {
global $OUTPUT;
return $OUTPUT->image_url('f/unknown')->out(false);
}
/**
* Returns user has access capability for the main content bank and the content itself (base on is_access_allowed from plugin).
*
* @return bool True if content could be accessed. False otherwise.
*/
final public function can_access(): bool {
$classname = 'contenttype/'.$this->get_plugin_name();
$capability = $classname.":access";
$hascapabilities = has_capability('moodle/contentbank:access', $this->context)
&& has_capability($capability, $this->context);
return $hascapabilities && $this->is_access_allowed();
}
/**
* Returns user has access capability for the content itself.
*
* @return bool True if content could be accessed. False otherwise.
*/
protected function is_access_allowed(): bool {
// Plugins can overwrite this function to add any check they need.
return true;
}
/**
* Returns the user has permission to upload new content.
*
* @return bool True if content could be uploaded. False otherwise.
*/
final public function can_upload(): bool {
if (!$this->is_feature_supported(self::CAN_UPLOAD)) {
return false;
}
if (!$this->can_access()) {
return false;
}
$classname = 'contenttype/'.$this->get_plugin_name();
$uploadcap = $classname.':upload';
$hascapabilities = has_capability('moodle/contentbank:upload', $this->context)
&& has_capability($uploadcap, $this->context);
return $hascapabilities && $this->is_upload_allowed();
}
/**
* Returns plugin allows uploading.
*
* @return bool True if plugin allows uploading. False otherwise.
*/
protected function is_upload_allowed(): bool {
// Plugins can overwrite this function to add any check they need.
return true;
}
/**
* Check if the user can delete this content.
*
* @param content $content The content to be deleted.
* @return bool True if content could be uploaded. False otherwise.
*/
final public function can_delete(content $content): bool {
global $USER;
if ($this->context->id != $content->get_content()->contextid) {
// The content has to have exactly the same context as this contenttype.
return false;
}
$hascapability = has_capability('moodle/contentbank:deleteanycontent', $this->context);
if ($content->get_content()->usercreated == $USER->id) {
// This content has been created by the current user; check if she can delete her content.
$hascapability = $hascapability || has_capability('moodle/contentbank:deleteowncontent', $this->context);
}
return $hascapability && $this->is_delete_allowed($content);
}
/**
* Returns if content allows deleting.
*
* @param content $content The content to be deleted.
* @return bool True if content allows uploading. False otherwise.
*/
protected function is_delete_allowed(content $content): bool {
// Plugins can overwrite this function to add any check they need.
return true;
}
/**
* Check if the user can managed this content.
*
* @param content $content The content to be managed.
* @return bool True if content could be managed. False otherwise.
*/
final public function can_manage(content $content): bool {
global $USER;
if ($this->context->id != $content->get_content()->contextid) {
// The content has to have exactly the same context as this contenttype.
return false;
}
// Check main contentbank management permission.
$hascapability = has_capability('moodle/contentbank:manageanycontent', $this->context);
if ($content->get_content()->usercreated == $USER->id) {
// This content has been created by the current user; check if they can manage their content.
$hascapability = $hascapability || has_capability('moodle/contentbank:manageowncontent', $this->context);
}
return $hascapability && $this->is_manage_allowed($content);
}
/**
* Returns if content allows managing.
*
* @param content $content The content to be managed.
* @return bool True if content allows uploading. False otherwise.
*/
protected function is_manage_allowed(content $content): bool {
// Plugins can overwrite this function to add any check they need.
return true;
}
/**
* Returns whether or not the user has permission to use the editor.
* This function will be called with the content to be edited as parameter,
* or null when is checking permission to create a new content using the editor.
*
* @param content $content The content to be edited or null when creating a new content.
* @return bool True if the user can edit content. False otherwise.
*/
final public function can_edit(?content $content = null): bool {
if (!$this->is_feature_supported(self::CAN_EDIT)) {
return false;
}
if (!$this->can_access()) {
return false;
}
if (!is_null($content) && !$this->can_manage($content)) {
return false;
}
$classname = 'contenttype/'.$this->get_plugin_name();
$editioncap = $classname.':useeditor';
$hascapabilities = has_all_capabilities(['moodle/contentbank:useeditor', $editioncap], $this->context);
return $hascapabilities && $this->is_edit_allowed($content);
}
/**
* Returns plugin allows edition.
*
* @param content $content The content to be edited.
* @return bool True if plugin allows edition. False otherwise.
*/
protected function is_edit_allowed(?content $content): bool {
// Plugins can overwrite this function to add any check they need.
return true;
}
/**
* Returns whether or not the user has permission to download the content.
*
* @since Moodle 3.10
* @param content $content The content to be downloaded.
* @return bool True if the user can download the content. False otherwise.
*/
final public function can_download(content $content): bool {
if (!$this->is_feature_supported(self::CAN_DOWNLOAD)) {
return false;
}
if (!$this->can_access()) {
return false;
}
$hascapability = has_capability('moodle/contentbank:downloadcontent', $this->context);
return $hascapability && $this->is_download_allowed($content);
}
/**
* Returns whether or not the user has permission to copy the content.
*
* @since Moodle 4.3
* @param content $content The content to be copied.
* @return bool True if the user can copy the content. False otherwise.
*/
final public function can_copy(content $content): bool {
global $USER;
if (!$this->is_feature_supported(self::CAN_COPY)) {
return false;
}
if (!$this->can_access()) {
return false;
}
if (!$this->is_copy_allowed($content)) {
return false;
}
$hascapability = has_capability('moodle/contentbank:copyanycontent', $this->context);
if (!$hascapability && ($content->get_content()->usercreated == $USER->id)) {
$hascapability = has_capability('moodle/contentbank:copycontent', $this->context);
}
return $hascapability;
}
/**
* Returns plugin allows downloading.
*
* @since Moodle 3.10
* @param content $content The content to be downloaed.
* @return bool True if plugin allows downloading. False otherwise.
*/
protected function is_download_allowed(content $content): bool {
// Plugins can overwrite this function to add any check they need.
return true;
}
/**
* Returns plugin allows copying.
*
* @since Moodle 4.3
* @param content $content The content to be copied.
* @return bool True if plugin allows copying. False otherwise.
*/
protected function is_copy_allowed(content $content): bool {
// Plugins can overwrite this function to add any check they need.
return true;
}
/**
* Returns the plugin supports the feature.
*
* @param string $feature Feature code e.g CAN_UPLOAD
* @return bool True if content could be uploaded. False otherwise.
*/
final public function is_feature_supported(string $feature): bool {
return in_array($feature, $this->get_implemented_features());
}
/**
* Return an array of implemented features by the plugins.
*
* @return array
*/
abstract protected function get_implemented_features(): array;
/**
* Return an array of extensions the plugins could manage.
*
* @return array
*/
abstract public function get_manageable_extensions(): array;
/**
* Returns the list of different types of the given content type.
*
* A content type can have one or more options for creating content. This method will report all of them or only the content
* type itself if it has no other options.
*
* @return array An object for each type:
* - string typename: descriptive name of the type.
* - string typeeditorparams: params required by this content type editor.
* - url typeicon: this type icon.
*/
abstract public function get_contenttype_types(): array;
}
+145
View File
@@ -0,0 +1,145 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_contentbank\external;
use core_contentbank\contentbank;
use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_single_structure;
use core_external\external_value;
use core_external\external_warnings;
/**
* This is the external method for copying a content.
*
* @package core_contentbank
* @copyright 2023 Daniel Neis Araujo
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class copy_content extends external_api {
/**
* copy_content parameters.
*
* @since Moodle 4.3
* @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 copy', VALUE_REQUIRED),
'name' => new external_value(PARAM_RAW, 'The new name for the content', VALUE_REQUIRED),
]
);
}
/**
* Copy content from the contentbank.
*
* @since Moodle 4.3
* @param int $contentid The content id to copy.
* @param string $name The new name.
* @return array Id of the new content; false and the warning, otherwise.
*/
public static function execute(int $contentid, string $name): array {
global $DB;
$id = 0;
$warnings = [];
$params = self::validate_parameters(self::execute_parameters(), [
'contentid' => $contentid,
'name' => $name,
]);
$params['name'] = clean_param($params['name'], PARAM_TEXT);
// If name is empty don't try to copy and return a more detailed message.
if (trim($params['name']) === '') {
$warnings[] = [
'item' => $params['contentid'],
'warningcode' => 'emptynamenotallowed',
'message' => get_string('emptynamenotallowed', 'core_contentbank'),
];
} else {
try {
$record = $DB->get_record('contentbank_content', ['id' => $params['contentid']], '*', MUST_EXIST);
$cb = new contentbank();
$content = $cb->get_content_from_id($record->id);
$contenttype = $content->get_content_type_instance();
$context = \context::instance_by_id($record->contextid, MUST_EXIST);
self::validate_context($context);
// Check capability.
if ($contenttype->can_copy($content)) {
// This content can be copied.
$crecord = $content->get_content();
unset($crecord->id);
$crecord->name = $params['name'];
if ($content = $contenttype->create_content($crecord)) {
$fs = get_file_storage();
$files = $fs->get_area_files($context->id, 'contentbank', 'public', $params['contentid'], 'itemid, filepath,
filename', false);
if (!empty($files)) {
$file = reset($files);
$content->import_file($file);
}
$id = $content->get_id();
} else {
$warnings[] = [
'item' => $params['contentid'],
'warningcode' => 'contentnotcopied',
'message' => get_string('contentnotcopied', 'core_contentbank'),
];
}
} else {
// The user has no permission to manage this content.
$warnings[] = [
'item' => $params['contentid'],
'warningcode' => 'nopermissiontomanage',
'message' => get_string('nopermissiontocopy', 'core_contentbank'),
];
}
} catch (\moodle_exception $e) {
// The content or the context don't exist.
$warnings[] = [
'item' => $params['contentid'],
'warningcode' => 'exception',
'message' => $e->getMessage(),
];
}
}
return [
'id' => $id,
'warnings' => $warnings,
];
}
/**
* copy_content return.
*
* @since Moodle 4.3
* @return external_single_structure
*/
public static function execute_returns(): external_single_structure {
return new external_single_structure([
'id' => new external_value(PARAM_INT, 'The id of the new content'),
'warnings' => new external_warnings(),
]);
}
}
+119
View File
@@ -0,0 +1,119 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_contentbank\external;
use core_contentbank\contentbank;
use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_multiple_structure;
use core_external\external_single_structure;
use core_external\external_value;
use core_external\external_warnings;
/**
* This is the external method for deleting a content.
*
* @package core_contentbank
* @since Moodle 3.9
* @copyright 2020 Sara Arjona <sara@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class delete_content extends external_api {
/**
* Parameters.
*
* @return external_function_parameters
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters([
'contentids' => new external_multiple_structure(
new external_value(PARAM_INT, 'The content id to delete', VALUE_REQUIRED)
)
]);
}
/**
* Delete content from the contentbank.
*
* @param array $contentids List of content ids to delete.
* @return array True if the content has been deleted; false and the warning, otherwise.
*/
public static function execute(array $contentids): array {
global $DB;
$result = false;
$warnings = [];
$params = self::validate_parameters(self::execute_parameters(), ['contentids' => $contentids]);
$cb = new contentbank();
foreach ($params['contentids'] as $contentid) {
try {
$record = $DB->get_record('contentbank_content', ['id' => $contentid], '*', MUST_EXIST);
$content = $cb->get_content_from_id($record->id);
$contenttype = $content->get_content_type_instance();
$context = \context::instance_by_id($record->contextid, MUST_EXIST);
self::validate_context($context);
// Check capability.
if ($contenttype->can_delete($content)) {
// This content can be deleted.
if (!$contenttype->delete_content($content)) {
$warnings[] = [
'item' => $contentid,
'warningcode' => 'contentnotdeleted',
'message' => get_string('contentnotdeleted', 'core_contentbank')
];
}
} else {
// The user has no permission to delete this content.
$warnings[] = [
'item' => $contentid,
'warningcode' => 'nopermissiontodelete',
'message' => get_string('nopermissiontodelete', 'core_contentbank')
];
}
} catch (\moodle_exception $e) {
// The content or the context don't exist.
$warnings[] = [
'item' => $contentid,
'warningcode' => 'exception',
'message' => $e->getMessage()
];
}
}
if (empty($warnings)) {
$result = true;
}
return [
'result' => $result,
'warnings' => $warnings
];
}
/**
* Return.
*
* @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()
]);
}
}
+132
View File
@@ -0,0 +1,132 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_contentbank\external;
use core_contentbank\contentbank;
use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_single_structure;
use core_external\external_value;
use core_external\external_warnings;
/**
* This is the external method for renaming a content.
*
* @package core_contentbank
* @copyright 2020 Amaia Anabitarte <amaia@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class rename_content extends external_api {
/**
* rename_content parameters.
*
* @since Moodle 3.9
* @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),
'name' => new external_value(PARAM_RAW, 'The new name for the content', VALUE_REQUIRED),
]
);
}
/**
* Rename content from the contentbank.
*
* @since Moodle 3.9
* @param int $contentid The content id to rename.
* @param string $name The new name.
* @return array True if the content has been renamed; false and the warning, otherwise.
*/
public static function execute(int $contentid, string $name): array {
global $DB;
$result = false;
$warnings = [];
$params = self::validate_parameters(self::execute_parameters(), [
'contentid' => $contentid,
'name' => $name,
]);
$params['name'] = clean_param($params['name'], PARAM_TEXT);
// If name is empty don't try to rename and return a more detailed message.
if (trim($params['name']) === '') {
$warnings[] = [
'item' => $params['contentid'],
'warningcode' => 'emptynamenotallowed',
'message' => get_string('emptynamenotallowed', 'core_contentbank')
];
} else {
try {
$record = $DB->get_record('contentbank_content', ['id' => $params['contentid']], '*', MUST_EXIST);
$cb = new contentbank();
$content = $cb->get_content_from_id($record->id);
$contenttype = $content->get_content_type_instance();
$context = \context::instance_by_id($record->contextid, MUST_EXIST);
self::validate_context($context);
// Check capability.
if ($contenttype->can_manage($content)) {
// This content can be renamed.
if ($contenttype->rename_content($content, $params['name'])) {
$result = true;
} else {
$warnings[] = [
'item' => $params['contentid'],
'warningcode' => 'contentnotrenamed',
'message' => get_string('contentnotrenamed', '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
];
}
/**
* rename_content return.
*
* @since Moodle 3.9
* @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()
]);
}
}
+125
View File
@@ -0,0 +1,125 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_contentbank\external;
use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_single_structure;
use core_external\external_value;
use core_external\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()
]);
}
}
+90
View File
@@ -0,0 +1,90 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Provides {@see \core_contentbank\form\edit_content} class.
*
* @package core_contentbank
* @copyright 2020 Victor Deniz <victor@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_contentbank\form;
use moodleform;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir.'/formslib.php');
/**
* Defines the form for editing a content.
*
* @package core_contentbank
* @copyright 2020 Victor Deniz <victor@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class edit_content extends moodleform {
/** @var int Context the content belongs to. */
protected $contextid;
/** @var string Content type plugin name. */
protected $plugin;
/** @var int Content id in the content bank. */
protected $id;
/**
* Constructor.
*
* @param string $action The action attribute for the form.
* @param array $customdata Data to set during instance creation.
* @param string $method Form method.
*/
public function __construct(string $action = null, array $customdata = null, string $method = 'post') {
parent::__construct($action, $customdata, $method);
$this->contextid = $customdata['contextid'];
$this->plugin = $customdata['plugin'];
$this->id = $customdata['id'];
$mform =& $this->_form;
$mform->addElement('hidden', 'contextid', $this->contextid);
$this->_form->setType('contextid', PARAM_INT);
$mform->addElement('hidden', 'plugin', $this->plugin);
$this->_form->setType('plugin', PARAM_PLUGIN);
$mform->addElement('hidden', 'id', $this->id);
$this->_form->setType('id', PARAM_INT);
}
/**
* Overrides formslib's add_action_buttons() method.
*
*
* @param bool $cancel
* @param string|null $submitlabel
*
* @return void
*/
public function add_action_buttons($cancel = true, $submitlabel = null): void {
if (is_null($submitlabel)) {
$submitlabel = get_string('save');
}
parent::add_action_buttons($cancel, $submitlabel);
}
}
+230
View File
@@ -0,0 +1,230 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_contentbank\form;
use core\output\notification;
/**
* Upload files to content bank form
*
* @package core_contentbank
* @copyright 2020 Amaia Anabitarte <amaia@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class upload_files extends \core_form\dynamic_form {
/**
* Add elements to this form.
*/
public function definition() {
$mform = $this->_form;
$mform->addElement('hidden', 'contextid');
$mform->setType('contextid', PARAM_INT);
$mform->addElement('hidden', 'id');
$mform->setType('id', PARAM_INT);
$mform->addElement('filepicker', 'file', get_string('file', 'core_contentbank'), null, $this->get_options());
$mform->addHelpButton('file', 'file', 'core_contentbank');
$mform->addRule('file', null, 'required');
}
/**
* Validate incoming data.
*
* @param array $data
* @param array $files
* @return array
*/
public function validation($data, $files) {
$errors = array();
$draftitemid = $data['file'];
$options = $this->get_options();
if (file_is_draft_area_limit_reached($draftitemid, $options['areamaxbytes'])) {
$errors['file'] = get_string('userquotalimit', 'error');
}
return $errors;
}
/**
* Check if current user has access to this form, otherwise throw exception
*
* Sometimes permission check may depend on the action and/or id of the entity.
* If necessary, form data is available in $this->_ajaxformdata or
* by calling $this->optional_param()
*/
protected function check_access_for_dynamic_submission(): void {
require_capability('moodle/contentbank:upload', $this->get_context_for_dynamic_submission());
// Check the context used by the content bank is allowed.
$cb = new \core_contentbank\contentbank();
if (!$cb->is_context_allowed($this->get_context_for_dynamic_submission())) {
throw new \moodle_exception('contextnotallowed', 'core_contentbank');
}
// If $id is defined, the file content will be replaced (instead of uploading a new one).
// Check that the user has the right permissions to replace this content file.
$id = $this->optional_param('id', null, PARAM_INT);
if ($id) {
$content = $cb->get_content_from_id($id);
$contenttype = $content->get_content_type_instance();
if (!$contenttype->can_manage($content) || !$contenttype->can_upload()) {
throw new \moodle_exception('nopermissions', 'error', '', null, get_string('replacecontent', 'contentbank'));
}
}
}
/**
* Returns form context
*
* If context depends on the form data, it is available in $this->_ajaxformdata or
* by calling $this->optional_param()
*
* @return \context
*/
protected function get_context_for_dynamic_submission(): \context {
$contextid = $this->optional_param('contextid', null, PARAM_INT);
return \context::instance_by_id($contextid, MUST_EXIST);
}
/**
* File upload options
*
* @return array
* @throws \coding_exception
*/
protected function get_options(): array {
global $CFG;
$maxbytes = $CFG->userquota;
$maxareabytes = $CFG->userquota;
if (has_capability('moodle/user:ignoreuserquota', $this->get_context_for_dynamic_submission())) {
$maxbytes = USER_CAN_IGNORE_FILE_SIZE_LIMITS;
$maxareabytes = FILE_AREA_MAX_BYTES_UNLIMITED;
}
$cb = new \core_contentbank\contentbank();
$id = $this->optional_param('id', null, PARAM_INT);
if ($id) {
$content = $cb->get_content_from_id($id);
$contenttype = $content->get_content_type_instance();
$extensions = $contenttype->get_manageable_extensions();
$acceptedtypes = implode(',', $extensions);
} else {
$acceptedtypes = $cb->get_supported_extensions_as_string($this->get_context_for_dynamic_submission());
}
return ['subdirs' => 1, 'maxbytes' => $maxbytes, 'maxfiles' => -1, 'accepted_types' => $acceptedtypes,
'areamaxbytes' => $maxareabytes];
}
/**
* Process the form submission, used if form was submitted via AJAX
*
* This method can return scalar values or arrays that can be json-encoded, they will be passed to the caller JS.
*
* Submission data can be accessed as: $this->get_data()
*
* @return mixed
*/
public function process_dynamic_submission() {
global $USER;
// Get the file and create the content based on it.
$usercontext = \context_user::instance($USER->id);
$fs = get_file_storage();
$files = $fs->get_area_files($usercontext->id, 'user', 'draft', $this->get_data()->file, 'itemid, filepath,
filename', false);
if (!empty($files)) {
$file = reset($files);
$cb = new \core_contentbank\contentbank();
try {
if ($this->get_data()->id) {
$content = $cb->get_content_from_id($this->get_data()->id);
$contenttype = $content->get_content_type_instance();
$content = $contenttype->replace_content($file, $content);
} else {
$content = $cb->create_content_from_file($this->get_context_for_dynamic_submission(), $USER->id, $file);
}
$params = ['id' => $content->get_id(), 'contextid' => $this->get_context_for_dynamic_submission()->id];
$url = new \moodle_url('/contentbank/view.php', $params);
} catch (\moodle_exception $e) {
// Redirect to the right page (depending on if content is new or existing) and display an error.
if ($this->get_data()->id) {
$content = $cb->get_content_from_id($this->get_data()->id);
$params = [
'id' => $content->get_id(),
'contextid' => $this->get_context_for_dynamic_submission()->id,
'errormsg' => $e->errorcode,
];
$url = new \moodle_url('/contentbank/view.php', $params);
} else {
$url = new \moodle_url('/contentbank/index.php', [
'contextid' => $this->get_context_for_dynamic_submission()->id,
'errormsg' => $e->errorcode],
);
}
}
return ['returnurl' => $url->out(false)];
}
return null;
}
/**
* Load in existing data as form defaults
*
* Can be overridden to retrieve existing values from db by entity id and also
* to preprocess editor and filemanager elements
*
* Example:
* $this->set_data(get_entity($this->_ajaxformdata['id']));
*/
public function set_data_for_dynamic_submission(): void {
$data = (object)[
'contextid' => $this->optional_param('contextid', null, PARAM_INT),
'id' => $this->optional_param('id', null, PARAM_INT),
];
$this->set_data($data);
}
/**
* Returns url to set in $PAGE->set_url() when form is being rendered or submitted via AJAX
*
* This is used in the form elements sensitive to the page url, such as Atto autosave in 'editor'
*
* If the form has arguments (such as 'id' of the element being edited), the URL should
* also have respective argument.
*
* @return \moodle_url
*/
protected function get_page_url_for_dynamic_submission(): \moodle_url {
$params = ['contextid' => $this->get_context_for_dynamic_submission()->id];
$id = $this->optional_param('id', null, PARAM_INT);
if ($id) {
$url = '/contentbank/view.php';
$params['id'] = $id;
} else {
$url = '/contentbank/index.php';
}
return new \moodle_url($url, $params);
}
}
+96
View File
@@ -0,0 +1,96 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Contains helper class for the content bank.
*
* @package core_contentbank
* @copyright 2020 Amaia Anabitarte <amaia@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_contentbank;
/**
* Helper class for the content bank.
*
* @package core_contentbank
* @copyright 2020 Amaia Anabitarte <amaia@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class helper {
/**
* Getting content bank page ready for the breadcrumbs.
*
* @param \context $context Context of the current page.
* @param string $title Title of the current page.
* @param bool $internal True if is an internal page, false otherwise.
*/
public static function get_page_ready(\context $context, string $title, bool $internal = false): void {
global $PAGE, $DB;
$PAGE->set_context($context);
$PAGE->set_heading(self::get_page_heading($context));
$PAGE->set_secondary_active_tab('contentbank');
$cburl = new \moodle_url('/contentbank/index.php', ['contextid' => $context->id]);
switch ($context->contextlevel) {
case CONTEXT_COURSE:
$courseid = $context->instanceid;
$course = $DB->get_record('course', ['id' => $courseid], '*', MUST_EXIST);
$PAGE->set_course($course);
\navigation_node::override_active_url(new \moodle_url('/course/view.php', ['id' => $courseid]));
$PAGE->navbar->add($title, $cburl);
$PAGE->set_pagelayout('incourse');
break;
case CONTEXT_COURSECAT:
$coursecat = $context->instanceid;
\navigation_node::override_active_url(new \moodle_url('/course/index.php', ['categoryid' => $coursecat]));
$PAGE->navbar->add($title, $cburl);
$PAGE->set_pagelayout('coursecategory');
break;
default:
if ($node = $PAGE->navigation->find('contentbank', \global_navigation::TYPE_CUSTOM)) {
$node->make_active();
}
$PAGE->set_pagelayout('standard');
}
}
/**
* Get the page heading based on the current context
*
* @param \context $context The current context of the page
* @return string
*/
public static function get_page_heading(\context $context): string {
global $SITE;
$title = get_string('contentbank');
if ($context->id == \context_system::instance()->id) {
$title = $SITE->fullname;
} else if ($context->contextlevel == CONTEXT_COURSE) {
$course = get_course($context->instanceid);
$title = $course->fullname;
} else if ($context->contextlevel == CONTEXT_COURSECAT) {
$category = \core_course_category::get($context->instanceid);
$title = $category->get_formatted_name();
}
return $title;
}
}
+212
View File
@@ -0,0 +1,212 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_contentbank\output;
use core\context\{course, coursecat};
use core\context_helper;
use core_contentbank\content;
use core_contentbank\contentbank;
use renderable;
use templatable;
use renderer_base;
use stdClass;
/**
* Class containing data for bank content
*
* @package core_contentbank
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class bankcontent implements renderable, templatable {
/**
* @var \core_contentbank\content[] Array of content bank contents.
*/
private $contents;
/**
* @var array $toolbar object.
*/
private $toolbar;
/**
* @var \context Given context. Null by default.
*/
private $context;
/**
* @var array Course categories that the user has access to.
*/
private $allowedcategories;
/**
* @var array Courses that the user has access to.
*/
private $allowedcourses;
/**
* Construct this renderable.
*
* @param \core_contentbank\content[] $contents Array of content bank contents.
* @param array $toolbar List of content bank toolbar options.
* @param \context|null $context Optional context to check (default null)
* @param contentbank $cb Contenbank object.
*/
public function __construct(array $contents, array $toolbar, ?\context $context, contentbank $cb) {
$this->contents = $contents;
$this->toolbar = $toolbar;
$this->context = $context;
list($this->allowedcategories, $this->allowedcourses) = $cb->get_contexts_with_capabilities_by_user();
}
/**
* Export the data.
*
* @param renderer_base $output
* @return stdClass
*/
public function export_for_template(renderer_base $output): stdClass {
global $PAGE, $SITE;
$PAGE->requires->js_call_amd('core_contentbank/search', 'init');
$PAGE->requires->js_call_amd('core_contentbank/sort', 'init');
$data = new stdClass();
$contentdata = array();
foreach ($this->contents as $content) {
$file = $content->get_file();
$filesize = $file ? $file->get_filesize() : 0;
$mimetype = $file ? get_mimetype_description($file) : '';
$contenttypeclass = $content->get_content_type().'\\contenttype';
$contenttype = new $contenttypeclass($this->context);
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,
'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),
'type' => $mimetype,
'author' => fullname($author),
'visibilityunlisted' => $content->get_visibility() == content::VISIBILITY_UNLISTED
);
}
$data->viewlist = get_user_preferences('core_contentbank_view_list');
$data->contents = $contentdata;
// The tools are displayed in the action bar on the index page.
foreach ($this->toolbar as $tool) {
// Customize the output of a tool, like dropdowns.
$method = 'export_tool_'.$tool['action'];
if (method_exists($this, $method)) {
$this->$method($tool);
}
$data->tools[] = $tool;
}
$allowedcontexts = [];
$systemcontext = \context_system::instance();
if (has_capability('moodle/contentbank:access', $systemcontext)) {
$allowedcontexts[$systemcontext->id] = get_string('coresystem');
}
$options = [];
foreach ($this->allowedcategories as $allowedcategory) {
context_helper::preload_from_record(clone $allowedcategory);
$options[$allowedcategory->ctxid] = format_string($allowedcategory->name, true, [
'context' => coursecat::instance($allowedcategory->ctxinstance),
]);
}
if (!empty($options)) {
$allowedcontexts['categories'] = [get_string('coursecategories') => $options];
}
$options = [];
foreach ($this->allowedcourses as $allowedcourse) {
// Don't add the frontpage course to the list.
if ($allowedcourse->id != $SITE->id) {
context_helper::preload_from_record(clone $allowedcourse);
$options[$allowedcourse->ctxid] = format_string($allowedcourse->fullname, true, [
'context' => course::instance($allowedcourse->ctxinstance),
]);
}
}
if (!empty($options)) {
$allowedcontexts['courses'] = [get_string('courses') => $options];
}
if (!empty($allowedcontexts)) {
$strchoosecontext = get_string('choosecontext', 'core_contentbank');
$singleselect = new \single_select(
new \moodle_url('/contentbank/index.php'),
'contextid',
$allowedcontexts,
$this->context->id,
$strchoosecontext
);
$singleselect->set_label($strchoosecontext, ['class' => 'sr-only']);
$data->allowedcontexts = $singleselect->export_for_template($output);
}
return $data;
}
/**
* Adds the content type items to display to the Add dropdown.
*
* Each content type is represented as an object with the properties:
* - name: the name of the content type.
* - baseurl: the base content type editor URL.
* - types: different types of the content type to display as dropdown items.
*
* @param array $tool Data for rendering the Add dropdown, including the editable content types.
*/
private function export_tool_add(array &$tool) {
$editabletypes = $tool['contenttypes'];
$addoptions = [];
foreach ($editabletypes as $class => $type) {
$contentype = new $class($this->context);
// Get the creation options of each content type.
$types = $contentype->get_contenttype_types();
if ($types) {
// Add a text describing the content type as first option. This will be displayed in the drop down to
// separate the options for the different content types.
$contentdesc = new stdClass();
$contentdesc->typename = get_string('description', $contentype->get_contenttype_name());
array_unshift($types, $contentdesc);
// Context data for the template.
$addcontenttype = new stdClass();
// Content type name.
$addcontenttype->name = $type;
// Content type editor base URL.
$tool['link']->param('plugin', $type);
$addcontenttype->baseurl = $tool['link']->out();
// Different types of the content type.
$addcontenttype->types = $types;
$addoptions[] = $addcontenttype;
}
}
$tool['contenttypes'] = $addoptions;
}
}
+204
View File
@@ -0,0 +1,204 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_contentbank\output;
use context;
use core_contentbank\content;
use core_contentbank\contenttype;
use moodle_url;
use renderable;
use renderer_base;
use stdClass;
use templatable;
/**
* Class containing data for the content view.
*
* @copyright 2020 Victor Deniz <victor@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class viewcontent implements renderable, templatable {
/**
* @var contenttype Content bank content type.
*/
private $contenttype;
/**
* @var stdClass Record of the contentbank_content table.
*/
private $content;
/**
* Construct this renderable.
*
* @param contenttype $contenttype Content bank content type.
* @param content $content Record of the contentbank_content table.
*/
public function __construct(contenttype $contenttype, content $content) {
$this->contenttype = $contenttype;
$this->content = $content;
}
/**
* Get the content of the "More" dropdown in the tertiary navigation
*
* @return array|null The options to be displayed in a dropdown in the tertiary navigation
* @throws \moodle_exception
*/
protected function get_edit_actions_dropdown(): ?array {
global $PAGE;
$options = [];
if ($this->contenttype->can_manage($this->content)) {
// Add the visibility item to the menu.
switch($this->content->get_visibility()) {
case content::VISIBILITY_UNLISTED:
$visibilitylabel = get_string('visibilitysetpublic', 'core_contentbank');
$newvisibility = content::VISIBILITY_PUBLIC;
break;
case content::VISIBILITY_PUBLIC:
$visibilitylabel = get_string('visibilitysetunlisted', 'core_contentbank');
$newvisibility = content::VISIBILITY_UNLISTED;
break;
default:
$url = new \moodle_url('/contentbank/index.php', ['contextid' => $this->content->get_contextid()]);
throw new moodle_exception('contentvisibilitynotfound', 'error', $url, $this->content->get_visibility());
}
if ($visibilitylabel) {
$options[$visibilitylabel] = [
'data-action' => 'setcontentvisibility',
'data-visibility' => $newvisibility,
'data-contentid' => $this->content->get_id(),
];
}
// Add the rename content item to the menu.
$options[get_string('rename')] = [
'data-action' => 'renamecontent',
'data-contentname' => $this->content->get_name(),
'data-contentid' => $this->content->get_id(),
];
if ($this->contenttype->can_upload()) {
$options[get_string('replacecontent', 'contentbank')] = ['data-action' => 'upload'];
$PAGE->requires->js_call_amd(
'core_contentbank/upload',
'initModal',
[
'[data-action=upload]',
\core_contentbank\form\upload_files::class,
$this->content->get_contextid(),
$this->content->get_id()
]
);
}
}
if ($this->contenttype->can_download($this->content)) {
$url = new moodle_url($this->contenttype->get_download_url($this->content));
$options[get_string('download')] = [
'url' => $url->out()
];
}
if ($this->contenttype->can_copy($this->content)) {
// Add the copy content item to the menu.
$options[get_string('copycontent', 'contentbank')] = [
'data-action' => 'copycontent',
'data-contentname' => get_string('copyof', 'contentbank', $this->content->get_name()),
'data-contentid' => $this->content->get_id(),
];
}
// Add the delete content item to the menu.
if ($this->contenttype->can_delete($this->content)) {
$options[get_string('delete')] = [
'data-action' => 'deletecontent',
'data-contentname' => $this->content->get_name(),
'data-uses' => count($this->content->get_uses()),
'data-contentid' => $this->content->get_id(),
'data-contextid' => $this->content->get_contextid(),
'class' => 'text-danger',
];
}
$dropdown = [];
if ($options) {
foreach ($options as $key => $attribs) {
$url = $attribs['url'] ?? '#';
$extraclasses = $attribs['class'] ?? '';
$dropdown['options'][] = [
'label' => $key,
'url' => $url,
'extraclasses' => $extraclasses,
'attributes' => array_map(function ($key, $value) {
return [
'name' => $key,
'value' => $value
];
}, array_keys($attribs), $attribs)
];
}
}
return $dropdown;
}
/**
* Export this data so it can be used as the context for a mustache template.
*
* @param renderer_base $output
*
* @return stdClass
*/
public function export_for_template(renderer_base $output): stdClass {
$data = new stdClass();
// Get the content type html.
$contenthtml = $this->contenttype->get_view_content($this->content);
$data->contenthtml = $contenthtml;
// Check if the user can edit this content type.
if ($this->contenttype->can_edit($this->content)) {
$data->usercanedit = true;
$urlparams = [
'contextid' => $this->content->get_contextid(),
'plugin' => $this->contenttype->get_plugin_name(),
'id' => $this->content->get_id()
];
$editcontenturl = new moodle_url('/contentbank/edit.php', $urlparams);
$data->editcontenturl = $editcontenturl->out(false);
}
// Close/exit link for those users who can access that context.
$context = context::instance_by_id($this->content->get_contextid());
if (has_capability('moodle/contentbank:access', $context)) {
$closeurl = new moodle_url('/contentbank/index.php', ['contextid' => $context->id]);
$data->closeurl = $closeurl->out(false);
}
$data->actionmenu = $this->get_edit_actions_dropdown();
$data->heading = $this->content->get_name();
if ($this->content->get_visibility() == content::VISIBILITY_UNLISTED) {
$data->heading = get_string('visibilitytitleunlisted', 'contentbank', $data->heading);
}
return $data;
}
}
+303
View File
@@ -0,0 +1,303 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Privacy provider implementation for core_contentbank.
*
* @package core_contentbank
* @copyright 2020 Amaia Anabitarte <amaia@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_contentbank\privacy;
use core_privacy\local\metadata\collection;
use core_privacy\local\request\approved_contextlist;
use core_privacy\local\request\contextlist;
use core_privacy\local\request\transform;
use core_privacy\local\request\writer;
use core_privacy\local\request\userlist;
use core_privacy\local\request\approved_userlist;
use context_system;
use context_coursecat;
use context_course;
/**
* Privacy provider implementation for core_contentbank.
*
* @copyright 2020 Amaia Anabitarte <amaia@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements
\core_privacy\local\metadata\provider,
\core_privacy\local\request\core_userlist_provider,
\core_privacy\local\request\plugin\provider,
\core_privacy\local\request\user_preference_provider {
/**
* Returns meta data about this system.
*
* @param collection $collection The initialised collection to add items to.
* @return collection A listing of user data stored through this system.
*/
public static function get_metadata(collection $collection): collection {
$collection->add_database_table('contentbank_content', [
'name' => 'privacy:metadata:content:name',
'contenttype' => 'privacy:metadata:content:contenttype',
'usercreated' => 'privacy:metadata:content:usercreated',
'usermodified' => 'privacy:metadata:content:usermodified',
'timecreated' => 'privacy:metadata:content:timecreated',
'timemodified' => 'privacy:metadata:content:timemodified',
], 'privacy:metadata:contentbankcontent');
return $collection;
}
/**
* Export all user preferences for the contentbank
*
* @param int $userid The userid of the user whose data is to be exported.
*/
public static function export_user_preferences(int $userid) {
$preference = get_user_preferences('core_contentbank_view_list', null, $userid);
if (isset($preference)) {
writer::export_user_preference(
'core_contentbank',
'core_contentbank_view_list',
$preference,
get_string('privacy:request:preference:set', 'core_contentbank', (object) [
'name' => 'core_contentbank_view_list',
'value' => $preference,
])
);
}
}
/**
* Get the list of contexts that contain user information for the specified user.
*
* @param int $userid The user to search.
* @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin.
*/
public static function get_contexts_for_userid(int $userid): contextlist {
$sql = "SELECT DISTINCT ctx.id
FROM {context} ctx
JOIN {contentbank_content} cb
ON cb.contextid = ctx.id
WHERE cb.usercreated = :userid
AND (ctx.contextlevel = :contextlevel1
OR ctx.contextlevel = :contextlevel2
OR ctx.contextlevel = :contextlevel3)";
$params = [
'userid' => $userid,
'contextlevel1' => CONTEXT_SYSTEM,
'contextlevel2' => CONTEXT_COURSECAT,
'contextlevel3' => CONTEXT_COURSE,
];
$contextlist = new contextlist();
$contextlist->add_from_sql($sql, $params);
return $contextlist;
}
/**
* Get the list of users within a specific context.
*
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
*/
public static function get_users_in_context(userlist $userlist) {
$context = $userlist->get_context();
$allowedcontextlevels = [
CONTEXT_SYSTEM,
CONTEXT_COURSECAT,
CONTEXT_COURSE,
];
if (!in_array($context->contextlevel, $allowedcontextlevels)) {
return;
}
$sql = "SELECT cb.usercreated as userid
FROM {contentbank_content} cb
WHERE cb.contextid = :contextid";
$params = [
'contextid' => $context->id
];
$userlist->add_from_sql('userid', $sql, $params);
}
/**
* Export all user data for the specified user, in the specified contexts.
*
* @param approved_contextlist $contextlist The approved contexts to export information for.
*/
public static function export_user_data(approved_contextlist $contextlist) {
global $DB;
// Remove contexts different from SYSTEM, COURSECAT or COURSE.
$contextids = array_reduce($contextlist->get_contexts(), function($carry, $context) {
if ($context->contextlevel == CONTEXT_SYSTEM || $context->contextlevel == CONTEXT_COURSECAT
|| $context->contextlevel == CONTEXT_COURSE) {
$carry[] = $context->id;
}
return $carry;
}, []);
if (empty($contextids)) {
return;
}
$userid = $contextlist->get_user()->id;
list($contextsql, $contextparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED);
// Retrieve the contentbank_content records created for the user.
$sql = "SELECT cb.id,
cb.name,
cb.contenttype,
cb.usercreated,
cb.usermodified,
cb.timecreated,
cb.timemodified,
cb.contextid
FROM {contentbank_content} cb
WHERE cb.usercreated = :userid
AND cb.contextid {$contextsql}
ORDER BY cb.contextid";
$params = ['userid' => $userid] + $contextparams;
$contents = $DB->get_recordset_sql($sql, $params);
$data = [];
$lastcontextid = null;
$subcontext = [
get_string('name', 'core_contentbank'),
];
foreach ($contents as $content) {
// The core_contentbank data export is organised in:
// {Sytem|Course Category|Course Context Level}/Content/data.json.
if ($lastcontextid && $lastcontextid != $content->contextid) {
$context = \context::instance_by_id($lastcontextid);
writer::with_context($context)->export_data($subcontext, (object)$data);
$data = [];
}
$data[] = (object) [
'name' => $content->name,
'contenttype' => $content->contenttype,
'usercreated' => transform::user($content->usercreated),
'usermodified' => transform::user($content->usermodified),
'timecreated' => transform::datetime($content->timecreated),
'timemodified' => transform::datetime($content->timemodified)
];
$lastcontextid = $content->contextid;
// The core_contentbank files export is organised in:
// {Sytem|Course Category|Course Context Level}/Content/_files/public/_itemid/filename.
$context = \context::instance_by_id($lastcontextid);
writer::with_context($context)->export_area_files($subcontext, 'contentbank', 'public', $content->id);
}
if (!empty($data)) {
$context = \context::instance_by_id($lastcontextid);
writer::with_context($context)->export_data($subcontext, (object)$data);
}
$contents->close();
}
/**
* Delete all data for all users in the specified context.
*
* @param context $context The specific context to delete data for.
*/
public static function delete_data_for_all_users_in_context(\context $context) {
global $DB;
if (!$context instanceof context_system && !$context instanceof context_coursecat
&& !$context instanceof context_course) {
return;
}
static::delete_data($context, []);
}
/**
* Delete multiple users within a single context.
*
* @param approved_userlist $userlist The approved context and user information to delete information for.
*/
public static function delete_data_for_users(approved_userlist $userlist) {
$context = $userlist->get_context();
if (!$context instanceof context_system && !$context instanceof context_coursecat
&& !$context instanceof context_course) {
return;
}
static::delete_data($context, $userlist->get_userids());
}
/**
* Delete all user data for the specified user, in the specified contexts.
*
* @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
*/
public static function delete_data_for_user(approved_contextlist $contextlist) {
if (empty($contextlist->count())) {
return;
}
$userid = $contextlist->get_user()->id;
foreach ($contextlist->get_contexts() as $context) {
if (!$context instanceof context_system && !$context instanceof context_coursecat
&& !$context instanceof context_course) {
continue;
}
static::delete_data($context, [$userid]);
}
}
/**
* Delete data related to a context and users (if defined).
*
* @param context $context A context.
* @param array $userids The user IDs.
*/
protected static function delete_data(\context $context, array $userids) {
global $DB;
$params = ['contextid' => $context->id];
$select = 'contextid = :contextid';
// Delete the Content Bank files.
if (!empty($userids)) {
list($insql, $inparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
$params += $inparams;
$select .= ' AND usercreated '.$insql;
}
$fs = get_file_storage();
$contents = $DB->get_records_select('contentbank_content',
$select, $params);
foreach ($contents as $content) {
$fs->delete_area_files($content->contextid, 'contentbank', 'public', $content->id);
}
// Delete all the contents.
$DB->delete_records_select('contentbank_content', $select, $params);
}
}
@@ -0,0 +1,108 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* H5P Content manager class
*
* @package contenttype_h5p
* @copyright 2020 Amaia Anabitarte <amaia@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace contenttype_h5p;
use core\notification;
use core_h5p\factory;
/**
* H5P Content manager class
*
* @package contenttype_h5p
* @copyright 2020 Amaia Anabitarte <amaia@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class content extends \core_contentbank\content {
/**
* Returns user has access permission for the content itself.
* If the H5P content-type library is disabled, the user won't have access to it.
*
* @return bool True if content could be accessed. False otherwise.
*/
public function is_view_allowed(): bool {
// Force H5P content to be deployed.
$fileurl = $this->get_file_url();
if (empty($fileurl)) {
// This should never happen because H5P contents should have always a file. However, this extra-checked has been added
// to avoid the contentbank stop working if, for any unkonwn/weird reason, the file doesn't exist.
return false;
}
// Skip capability check when creating the H5P content (because it has been created by trusted users).
$h5pplayer = new \core_h5p\player($fileurl, new \stdClass(), true, '', true);
// Flush error messages.
$h5pplayer->get_messages();
// Check if the H5P entry has been created and if the main library is enabled.
$file = $this->get_file();
if (!empty($file)) {
$h5p = \core_h5p\api::get_content_from_pathnamehash($file->get_pathnamehash());
if (empty($h5p)) {
// If there is no H5P entry for this content, it won't be displayed unless the user has the manageanycontent
// capability. Reasons for contents without a proper H5P entry in DB:
// - Invalid H5P package (it won't be never deployed).
// - Disabled content-type library (it can't be deployed so there is no way to know the mainlibraryid).
$context = \context::instance_by_id($this->content->contextid);
if (!has_capability('moodle/contentbank:manageanycontent', $context)) {
return false;
}
} else if (!\core_h5p\api::is_library_enabled((object) ['id' => $h5p->mainlibraryid])) {
// If the main library is disabled, it won't be displayed.
return false;
}
}
return parent::is_view_allowed();
}
/**
* Import a file as a valid content.
* Before importing the file, this method will check if the file is a valid H5P package. If it's not valid, it will thrown
* an exception.
*
* @throws \moodle_exception If file operations fail
* @param \stored_file $file File to store in the content file area.
* @return \stored_file|null the stored content file or null if the file is discarted.
*/
public function import_file(\stored_file $file): ?\stored_file {
// The H5P content can be only deployed if the author of the .h5p file can update libraries or if all the
// content-type libraries exist, to avoid users without the h5p:updatelibraries capability upload malicious content.
$onlyupdatelibs = !\core_h5p\helper::can_update_library($file);
if (!\core_h5p\api::is_valid_package($file, $onlyupdatelibs)) {
$factory = new factory();
$errors = $factory->get_framework()->getMessages('error');
foreach ($errors as $error) {
notification::error($error->message);
}
if (empty($errors) || count($errors) > 1) {
throw new \moodle_exception('notvalidpackage', 'h5p');
}
throw new \moodle_exception($errors[0]->code, 'h5p');
}
return parent::import_file($file);
}
}
@@ -0,0 +1,174 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* H5P content type manager class
*
* @package contenttype_h5p
* @copyright 2020 Amaia Anabitarte <amaia@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace contenttype_h5p;
use core\event\contentbank_content_viewed;
use stdClass;
use core_h5p\editor_ajax;
use core_h5p\file_storage;
use core_h5p\local\library\autoloader;
use Moodle\H5PCore;
/**
* H5P content bank manager class
*
* @package contenttype_h5p
* @copyright 2020 Amaia Anabitarte <amaia@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class contenttype extends \core_contentbank\contenttype {
/**
* Delete this content from the content_bank and remove all the H5P related information.
*
* @param content $content The content to delete.
* @return boolean true if the content has been deleted; false otherwise.
*/
public function delete_content(\core_contentbank\content $content): bool {
// Delete the H5P content.
$factory = new \core_h5p\factory();
if (!empty($content->get_file_url())) {
\core_h5p\api::delete_content_from_pluginfile_url($content->get_file_url(), $factory);
}
// Delete the content from the content_bank.
return parent::delete_content($content);
}
/**
* Returns the HTML content to add to view.php visualizer.
*
* @param content $content The content to be displayed.
* @return string HTML code to include in view.php.
*/
public function get_view_content(\core_contentbank\content $content): string {
// Trigger an event for viewing this content.
$event = contentbank_content_viewed::create_from_record($content->get_content());
$event->trigger();
$fileurl = $content->get_file_url();
$html = \core_h5p\player::display($fileurl, new \stdClass(), true);
return $html;
}
/**
* Returns the HTML code to render the icon for H5P content types.
*
* @param content $content The content to be displayed.
* @return string HTML code to render the icon
*/
public function get_icon(\core_contentbank\content $content): string {
global $OUTPUT, $DB;
$iconurl = $OUTPUT->image_url('f/h5p')->out(false);
$file = $content->get_file();
if (!empty($file)) {
$h5p = \core_h5p\api::get_content_from_pathnamehash($file->get_pathnamehash());
if (!empty($h5p)) {
\core_h5p\local\library\autoloader::register();
if ($h5plib = $DB->get_record('h5p_libraries', ['id' => $h5p->mainlibraryid])) {
$h5pfilestorage = new \core_h5p\file_storage();
$h5picon = $h5pfilestorage->get_icon_url(
$h5plib->id,
$h5plib->machinename,
$h5plib->majorversion,
$h5plib->minorversion);
if (!empty($h5picon)) {
$iconurl = $h5picon;
}
}
}
}
return $iconurl;
}
/**
* Return an array of implemented features by this plugin.
*
* @return array
*/
protected function get_implemented_features(): array {
return [self::CAN_UPLOAD, self::CAN_EDIT, self::CAN_DOWNLOAD, self::CAN_COPY];
}
/**
* Return an array of extensions this contenttype could manage.
*
* @return array
*/
public function get_manageable_extensions(): array {
return ['.h5p'];
}
/**
* Returns user has access capability for the content itself.
*
* @return bool True if content could be accessed. False otherwise.
*/
protected function is_access_allowed(): bool {
return true;
}
/**
* Returns the list of different H5P content types the user can create.
*
* @return array An object for each H5P content type:
* - string typename: descriptive name of the H5P content type.
* - string typeeditorparams: params required by the H5P editor.
* - url typeicon: H5P content type icon.
*/
public function get_contenttype_types(): array {
// Get the H5P content types available.
autoloader::register();
$editorajax = new editor_ajax();
$h5pcontenttypes = $editorajax->getLatestLibraryVersions();
$types = [];
$h5pfilestorage = new file_storage();
foreach ($h5pcontenttypes as $h5pcontenttype) {
if ($h5pcontenttype->enabled) {
// Only enabled content-types will be displayed.
$library = [
'name' => $h5pcontenttype->machine_name,
'majorVersion' => $h5pcontenttype->major_version,
'minorVersion' => $h5pcontenttype->minor_version,
];
$key = H5PCore::libraryToString($library);
$type = new stdClass();
$type->key = $key;
$type->typename = $h5pcontenttype->title;
$type->typeeditorparams = 'library=' . $key;
$type->typeicon = $h5pfilestorage->get_icon_url(
$h5pcontenttype->id,
$h5pcontenttype->machine_name,
$h5pcontenttype->major_version,
$h5pcontenttype->minor_version);
$types[] = $type;
}
}
return $types;
}
}
@@ -0,0 +1,179 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Provides the class that defines the form for the H5P authoring tool.
*
* @package contenttype_h5p
* @copyright 2020 Victor Deniz <victor@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace contenttype_h5p\form;
use contenttype_h5p\content;
use contenttype_h5p\contenttype;
use core_contentbank\form\edit_content;
use core_h5p\api;
use core_h5p\editor as h5peditor;
use core_h5p\factory;
use core_h5p\helper;
use stdClass;
/**
* Defines the form for editing an H5P content.
*
* @copyright 2020 Victor Deniz <victor@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class editor extends edit_content {
/** @var $h5peditor H5P editor object */
private $h5peditor;
/** @var $content The content being edited */
private $content;
/**
* Defines the form fields.
*/
protected function definition() {
global $DB, $OUTPUT;
$mform = $this->_form;
$errors = [];
$notifications = [];
// Id of the content to edit.
$id = $this->_customdata['id'];
// H5P content type to create.
$library = optional_param('library', null, PARAM_TEXT);
if (empty($id) && empty($library)) {
$returnurl = new \moodle_url('/contentbank/index.php', ['contextid' => $this->_customdata['contextid']]);
throw new \moodle_exception('invalidcontentid', 'error', $returnurl);
}
$this->h5peditor = new h5peditor();
$this->set_display_vertical();
$mform->addElement('html', $OUTPUT->heading($this->_customdata['heading'], 2));
if ($id) {
// The H5P editor needs the H5P content id (h5p table).
$record = $DB->get_record('contentbank_content', ['id' => $id]);
$this->content = new content($record);
$file = $this->content->get_file();
$h5p = api::get_content_from_pathnamehash($file->get_pathnamehash());
if (!$h5p) {
// H5P content has not been deployed yet. Let's check why.
$factory = new \core_h5p\factory();
$factory->get_framework()->set_file($file);
$h5pid = helper::save_h5p($factory, $file, new stdClass());
$errors = $factory->get_framework()->getMessages('error');
$notifications = $factory->get_framework()->getMessages('info');
} else {
$h5pid = $h5p->id;
}
if ($h5pid) {
$mform->addElement('hidden', 'h5pid', $h5pid);
$mform->setType('h5pid', PARAM_INT);
$this->h5peditor->set_content($h5pid);
}
} else {
// The H5P editor needs the H5P content type library name for a new content.
$mform->addElement('hidden', 'library', $library);
$mform->setType('library', PARAM_TEXT);
$this->h5peditor->set_library($library, $this->_customdata['contextid'], 'contentbank', 'public');
}
$mformid = 'coolh5peditor';
$mform->setAttributes(array('id' => $mformid) + $mform->getAttributes());
if ($errors || $notifications) {
// Show the error messages and a Cancel button.
foreach ($errors as $error) {
$mform->addElement('warning', $error->code, 'notify', $error->message);
}
foreach ($notifications as $key => $notification) {
$mform->addElement('warning', 'notification_'.$key, 'notify', $notification);
}
$mform->addElement('cancel', 'cancel', get_string('back'));
} else {
$this->h5peditor->add_editor_to_form($mform);
$this->add_action_buttons();
}
}
/**
* Modify or create an H5P content from the form data.
*
* @param stdClass $data Form data to create or modify an H5P content.
*
* @return int The id of the edited or created content.
*/
public function save_content(stdClass $data): int {
global $DB;
// The H5P libraries expect data->id as the H5P content id.
// The method H5PCore::saveContent throws an error if id is set but empty.
if (empty($data->id)) {
unset($data->id);
} else {
// The H5P libraries save in $data->id the H5P content id (h5p table), so the content id is saved in another var.
$contentid = $data->id;
}
$h5pcontentid = $this->h5peditor->save_content($data);
$factory = new factory();
$h5pfs = $factory->get_framework();
// Needs the H5P file id to create or update the content bank record.
$h5pcontent = $h5pfs->loadContent($h5pcontentid);
$fs = get_file_storage();
$file = $fs->get_file_by_hash($h5pcontent['pathnamehash']);
// Creating new content.
if (!isset($data->h5pid)) {
// The initial name of the content is the title of the H5P content.
$cbrecord = new stdClass();
$cbrecord->name = json_decode($data->h5pparams)->metadata->title;
$context = \context::instance_by_id($data->contextid, MUST_EXIST);
// Create entry in content bank.
$contenttype = new contenttype($context);
$newcontent = $contenttype->create_content($cbrecord);
if ($file && $newcontent) {
$updatedfilerecord = new stdClass();
$updatedfilerecord->id = $file->get_id();
$updatedfilerecord->itemid = $newcontent->get_id();
// As itemid changed, the pathnamehash has to be updated in the file table.
$pathnamehash = \file_storage::get_pathname_hash($file->get_contextid(), $file->get_component(),
$file->get_filearea(), $updatedfilerecord->itemid, $file->get_filepath(), $file->get_filename());
$updatedfilerecord->pathnamehash = $pathnamehash;
$DB->update_record('files', $updatedfilerecord);
// The pathnamehash in the h5p table must match the file pathnamehash.
$h5pfs->updateContentFields($h5pcontentid, ['pathnamehash' => $pathnamehash]);
}
} else {
// Update content.
$this->content->update_content();
}
return $contentid ?? $newcontent->get_id();
}
}
@@ -0,0 +1,44 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Privacy provider implementation for core_contentbank.
*
* @package contenttype_h5p
* @copyright 2020 Amaia Anabitarte <amaia@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace contenttype_h5p\privacy;
/**
* Privacy provider implementation for contenttype_h5p.
*
* @copyright 2020 Amaia Anabitarte <amaia@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements \core_privacy\local\metadata\null_provider {
/**
* Get the language string identifier with the component's language
* file to explain why this plugin stores no data.
*
* @return string
*/
public static function get_reason(): string {
return 'privacy:metadata';
}
}
+57
View File
@@ -0,0 +1,57 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* H5P content bank contenttype capabilities.
*
* @package contenttype_h5p
* @copyright 2020 Amaia Anabitarte <amaia@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$capabilities = [
'contenttype/h5p:access' => array(
'captype' => 'read',
'contextlevel' => CONTEXT_COURSE,
'archetypes' => array(
'manager' => CAP_ALLOW,
'coursecreator' => CAP_ALLOW,
'editingteacher' => CAP_ALLOW,
)
),
'contenttype/h5p:upload' => [
'riskbitmask' => RISK_SPAM,
'captype' => 'write',
'contextlevel' => CONTEXT_COURSE,
'archetypes' => [
'manager' => CAP_ALLOW,
'coursecreator' => CAP_ALLOW,
'editingteacher' => CAP_ALLOW,
]
],
'contenttype/h5p:useeditor' => [
'riskbitmask' => RISK_SPAM,
'captype' => 'write',
'contextlevel' => CONTEXT_COURSE,
'archetypes' => [
'manager' => CAP_ALLOW,
'coursecreator' => CAP_ALLOW,
'editingteacher' => CAP_ALLOW,
]
],
];
@@ -0,0 +1,31 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Strings for plugin 'contenttype_h5p'
*
* @package contenttype_h5p
* @copyright 2020 Amaia Anabitarte <amaia@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['description'] = 'H5P interactive content';
$string['pluginname'] = 'H5P';
$string['pluginname_help'] = 'Content bank to upload and share H5P content';
$string['privacy:metadata'] = 'The H5P content bank plugin does not store any personal data.';
$string['h5p:access'] = 'Access H5P content in the content bank';
$string['h5p:upload'] = 'Upload new H5P content';
$string['h5p:useeditor'] = 'Create or edit content using the H5P editor';
@@ -0,0 +1,32 @@
@core @core_contentbank @core_h5p @contenttype_h5p @_file_upload @_switch_iframe @javascript
Feature: Replace H5P file from an existing content
In order to replace an H5P content from the content bank
As an admin
I need to be able to replace the content with a new .h5p file
Background:
Given the following "contentbank content" exist:
| contextlevel | reference | contenttype | user | contentname | filepath |
| System | | contenttype_h5p | admin | filltheblanks.h5p | /h5p/tests/fixtures/filltheblanks.h5p |
And I log in as "admin"
And I turn editing mode on
And the following config values are set as admin:
| unaddableblocks | | theme_boost|
And I add the "Navigation" block if not present
And I expand "Site pages" node
And I click on "Content bank" "link"
Scenario: Admins can replace the original .h5p file with a new one
Given I click on "filltheblanks.h5p" "link"
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
And I should see "Of which countries"
And I switch to the main frame
When I click on "More" "button"
And I click on "Replace with file" "link"
And I upload "h5p/tests/fixtures/ipsums.h5p" file to "Upload content" filemanager
And I click on "Save changes" "button"
Then I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
And I should see "Lorum ipsum"
And I switch to the main frame
@@ -0,0 +1,124 @@
@core @core_contentbank @core_h5p @contenttype_h5p @_switch_iframe @javascript
Feature: H5P file upload to content bank for admins
In order import new H5P content to content bank
As an admin
I need to be able to upload a new .h5p file to content bank
Background:
Given the following "user private file" exists:
| user | admin |
| filepath | h5p/tests/fixtures/filltheblanks.h5p |
And I log in as "admin"
And I turn editing mode on
And the following config values are set as admin:
| unaddableblocks | | theme_boost|
And I add the "Navigation" block if not present
And I expand "Site pages" node
And I click on "Content bank" "link"
Scenario: Admins can upload .h5p extension files to content bank
Given I should not see "filltheblanks.h5p"
When 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 click on "Select this file" "button"
And I click on "Save changes" "button"
And I wait until the page is ready
Then I should see "filltheblanks.h5p"
Scenario: Admins can see uploaded H5P contents
Given I should not see "filltheblanks.h5p"
When 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 click on "Select this file" "button"
And I click on "Save changes" "button"
And I wait until the page is ready
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
Then I should see "Of which countries"
Scenario: Users can't see content managed by disabled plugins
Given 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 click on "Select this file" "button"
And I click on "Save changes" "button"
And I wait until the page is ready
And I should see "filltheblanks.h5p"
And I navigate to "Plugins > Content bank > Manage content types" in site administration
And I click on "Disable" "icon" in the "H5P" "table_row"
And I wait until the page is ready
When I navigate to "Plugins > Content bank" in site administration
Then I should not see "filltheblanks.h5p"
Scenario: Contents in a context are not available from other contexts
Given the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
When 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 click on "Select this file" "button"
And I click on "Save changes" "button"
And I wait until the page is ready
Then I should see "filltheblanks.h5p"
And I am on "Course 1" course homepage
And the following config values are set as admin:
| unaddableblocks | | theme_boost|
And I add the "Navigation" block if not present
And I expand "Site pages" node
And I click on "Content bank" "link"
And I should not see "filltheblanks.h5p"
Scenario: Admins can upload and deployed content types when libraries are not installed
Given I navigate to "H5P > Manage H5P content types" in site administration
And I should not see "Fill in the Blanks"
And I follow "Dashboard"
And I expand "Site pages" node
And I click on "Content bank" "link"
And I should not see "filltheblanks.h5p"
When 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 click on "Select this file" "button"
And I click on "Save changes" "button"
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
Then I should see "Of which countries"
And I switch to the main frame
And I navigate to "H5P > Manage H5P content types" in site administration
And I should see "Fill in the Blanks"
Scenario: Uploading invalid packages throws error
Given the following "user private files" exist:
| user | filepath |
| admin | h5p/tests/fixtures/no-json-file.h5p |
| admin | h5p/tests/fixtures/unzippable.h5p |
And I follow "Dashboard"
And I expand "Site pages" node
And I click on "Content bank" "link"
When 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 "no-json-file.h5p" "link"
And I click on "Select this file" "button"
And I click on "Save changes" "button"
And I wait until the page is ready
Then I should see "A valid main h5p.json file is missing"
And I should see "Only files with the following extensions are allowed"
And I should not see "Sorry, this file is not valid"
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 "unzippable.h5p" "link"
And I click on "Select this file" "button"
And I click on "Save changes" "button"
And I wait until the page is ready
And I should see "It is not possible to unzip it"
And I should not see "Sorry, this file is not valid"
@@ -0,0 +1,103 @@
@core @core_contentbank @core_h5p @contenttype_h5p @_file_upload @javascript
Feature: Disable H5P content-types from the content bank
In order to disable H5P content-types
As an admin
I need to be able to check they are not displayed in the content bank
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following "contentbank contents" exist:
| contextlevel | reference | contenttype | user | contentname | filepath |
| Course | C1 | contenttype_h5p | admin | filltheblanks | /h5p/tests/fixtures/filltheblanks.h5p |
| Course | C1 | contenttype_h5p | admin | accordion | /h5p/tests/fixtures/ipsums.h5p |
| Course | C1 | contenttype_h5p | admin | invalidh5p | /h5p/tests/fixtures/h5ptest.zip |
And I log in as "admin"
And I am on "Course 1" course homepage with editing mode on
And the following config values are set as admin:
| unaddableblocks | | theme_boost|
And I add the "Navigation" block if not present
And I log out
Scenario: Teachers cannot view disabled or invalid content-types
Given I log in as "teacher1"
And I am on "Course 1" course homepage
And I expand "Site pages" node
And I click on "Content bank" "link"
And I should see "accordion"
And I should see "filltheblanks"
And I should not see "invalidh5p"
And I log out
And I log in as "admin"
And I navigate to "H5P > Manage H5P content types" in site administration
And I click on "Disable" "link" in the "Accordion" "table_row"
And I log out
When I log in as "teacher1"
And I am on "Course 1" course homepage
And I expand "Site pages" node
And I click on "Content bank" "link"
Then I should not see "accordion"
And I should see "filltheblanks"
And I should not see "invalidh5p"
Scenario: Admins cannot view disabled content-types
Given I log in as "admin"
And I am on "Course 1" course homepage
And I expand "Site pages" node
And I click on "Content bank" "link"
And I should see "accordion"
And I should see "filltheblanks"
And I should see "invalidh5p"
And I navigate to "H5P > Manage H5P content types" in site administration
And I click on "Disable" "link" in the "Accordion" "table_row"
When I am on "Course 1" course homepage
And I expand "Site pages" node
And I click on "Content bank" "link"
Then I should not see "accordion"
And I should see "filltheblanks"
And I should see "invalidh5p"
Scenario: Teachers cannot create disabled content-types
Given I log in as "teacher1"
And I am on "Course 1" course homepage
And I expand "Site pages" node
And I click on "Content bank" "link"
And I click on "[data-action=Add-content]" "css_element"
And I should see "Accordion"
And I should see "Fill in the Blanks"
And I log out
And I log in as "admin"
And I navigate to "H5P > Manage H5P content types" in site administration
And I click on "Disable" "link" in the "Accordion" "table_row"
And I log out
When I log in as "teacher1"
And I am on "Course 1" course homepage
And I expand "Site pages" node
And I click on "Content bank" "link"
And I click on "[data-action=Add-content]" "css_element"
Then I should not see "Accordion"
And I should see "Fill in the Blanks"
Scenario: Admins cannot create disabled content-types
Given I log in as "admin"
And I am on "Course 1" course homepage
And I expand "Site pages" node
And I click on "Content bank" "link"
And I click on "[data-action=Add-content]" "css_element"
And I should see "Accordion"
And I should see "Fill in the Blanks"
And I navigate to "H5P > Manage H5P content types" in site administration
And I click on "Disable" "link" in the "Accordion" "table_row"
When I am on "Course 1" course homepage
And I expand "Site pages" node
And I click on "Content bank" "link"
And I click on "[data-action=Add-content]" "css_element"
Then I should not see "Accordion"
And I should see "Fill in the Blanks"
@@ -0,0 +1,55 @@
@core @core_contentbank @core_h5p @contenttype_h5p @_file_upload @javascript
Feature: Manage H5P content from the content bank
In order to manage H5P content in the content bank
As an admin
I need to be able to edit any H5P content in the content bank
Background:
Given I log in as "admin"
And I navigate to "H5P > Manage H5P content types" in site administration
And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "H5P content type" filemanager
And I click on "Upload H5P content types" "button" in the "#fitem_id_uploadlibraries" "css_element"
And I upload "h5p/tests/fixtures/ipsums.h5p" file to "H5P content type" filemanager
And I click on "Upload H5P content types" "button" in the "#fitem_id_uploadlibraries" "css_element"
And the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following "contentbank content" exist:
| contextlevel | reference | contenttype | user | contentname | filepath |
| Course | C1 | contenttype_h5p | admin | filltheblanks.h5p | /h5p/tests/fixtures/filltheblanks.h5p |
| Course | C1 | contenttype_h5p | teacher1 | ipsums.h5p | /h5p/tests/fixtures/ipsums.h5p |
And I am on "Course 1" course homepage with editing mode on
And the following config values are set as admin:
| unaddableblocks | | theme_boost|
And I add the "Navigation" block if not present
And I log out
Scenario: Teachers can rename their own content in the content bank
Given I log in as "teacher1"
And I am on "Course 1" course homepage
And I expand "Site pages" node
And I click on "Content bank" "link"
And I follow "ipsums.h5p"
When I click on "More" "button"
And I should see "Rename"
And I click on "Rename" "link"
And I set the field "Content name" to "New name"
And I click on "Rename" "button"
And I wait until the page is ready
Then I should not see "ipsums.h5p"
And I should see "New name"
Scenario: Teachers can't rename content created by other users in the content bank
Given I log in as "teacher1"
And I am on "Course 1" course homepage
And I expand "Site pages" node
And I click on "Content bank" "link"
When I follow "filltheblanks.h5p"
And I click on "More" "button"
Then I should not see "Rename"
@@ -0,0 +1,109 @@
@core @core_contentbank @core_h5p @contenttype_h5p @_file_upload @_switch_iframe @javascript
Feature: Replace H5P file from an existing content requires special capabilities
In order replace an H5P content from the content bank
As a teacher
I need to be able to replace the content only if certain capabilities are allowed
Background:
Given I log in as "admin"
And I navigate to "H5P > Manage H5P content types" in site administration
And I upload "h5p/tests/fixtures/ipsums.h5p" file to "H5P content type" filemanager
And I click on "Upload H5P content types" "button" in the "#fitem_id_uploadlibraries" "css_element"
And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "H5P content type" filemanager
And I click on "Upload H5P content types" "button" in the "#fitem_id_uploadlibraries" "css_element"
And the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "categories" exist:
| name | category | idnumber |
| Cat 1 | 0 | CAT1 |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | CAT1 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following "contentbank content" exist:
| contextlevel | reference | contenttype | user | contentname | filepath |
| Course | C1 | contenttype_h5p | admin | admincontent | /h5p/tests/fixtures/ipsums.h5p |
| Course | C1 | contenttype_h5p | teacher1 | teachercontent | /h5p/tests/fixtures/filltheblanks.h5p |
And I log out
And I log in as "teacher1"
And I am on "Course 1" course homepage with editing mode on
And the following config values are set as admin:
| unaddableblocks | | theme_boost|
And I add the "Navigation" block if not present
And I expand "Site pages" node
And I click on "Content bank" "link"
# Force the content deploy
And I click on "admincontent" "link"
And I am on "Course 1" course homepage
And I expand "Site pages" node
And I click on "Content bank" "link"
Scenario: Teacher can replace its own H5P files
Given I click on "teachercontent" "link"
When I click on "More" "button"
And I click on "Replace with file" "link"
And I upload "h5p/tests/fixtures/ipsums.h5p" file to "Upload content" filemanager
And I click on "Save changes" "button"
Then I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
And I should see "Lorum ipsum"
And I switch to the main frame
Scenario: Teacher cannot replace another user's H5P files
When I click on "admincontent" "link"
And I click on "More" "button"
Then I should not see "Replace with file"
Scenario: Teacher cannot replace a content without having upload capability
Given the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| moodle/contentbank:upload | Prevent | editingteacher | Course | C1 |
When I click on "teachercontent" "link"
And I click on "More" "button"
Then I should not see "Replace with file"
Scenario: Teacher cannot replace a content without having the H5P upload capability
Given the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| contenttype/h5p:upload | Prevent | editingteacher | Course | C1 |
When I click on "teachercontent" "link"
And I click on "More" "button"
Then I should not see "Replace with file"
Scenario: Teacher cannot replace a content without having the manage own content capability
Given the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| moodle/contentbank:manageowncontent | Prevent | editingteacher | Course | C1 |
When I click on "teachercontent" "link"
And I click on "More" "button"
Then I should not see "Replace with file"
Scenario: Replacing with invalid packages throws error
Given I log in as "admin"
And I navigate to "H5P > Manage H5P content types" in site administration
And I upload "h5p/tests/fixtures/no-json-file.h5p" file to "H5P content type" filemanager
And I upload "h5p/tests/fixtures/unzippable.h5p" file to "H5P content type" filemanager
And I log out
And I log in as "teacher1"
And I am on "Course 1" course homepage
And I expand "Site pages" node
And I click on "Content bank" "link"
And I click on "teachercontent" "link"
When I click on "More" "button"
And I click on "Replace with file" "link"
And I upload "h5p/tests/fixtures/no-json-file.h5p" file to "Upload content" filemanager
And I click on "Save changes" "button"
And I wait until the page is ready
Then I should see "A valid main h5p.json file is missing"
And I should see "Only files with the following extensions are allowed"
And I should not see "Sorry, this file is not valid"
And I click on "More" "button"
And I click on "Replace with file" "link"
And I upload "h5p/tests/fixtures/unzippable.h5p" file to "Upload content" filemanager
And I click on "Save changes" "button"
And I wait until the page is ready
And I should see "It is not possible to unzip it"
And I should not see "Sorry, this file is not valid"
@@ -0,0 +1,177 @@
@core @core_contentbank @core_h5p @contenttype_h5p @_file_upload @_switch_iframe @javascript
Feature: H5P file upload to content bank for non admins
In order import new H5P content to content bank
As an admin
I need to be able to upload a new .h5p file to content bank
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
| teacher2 | Teacher | 2 | teacher2@example.com |
And the following "categories" exist:
| name | category | idnumber |
| Cat 1 | 0 | CAT1 |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | CAT1 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| teacher2 | C1 | editingteacher |
And the following "user private file" exists:
| user | teacher1 |
| filepath | h5p/tests/fixtures/filltheblanks.h5p |
And I log in as "teacher1"
Scenario: Teachers can not access system level content bank
Given I turn editing mode on
And the following config values are set as admin:
| unaddableblocks | | theme_boost|
And I add the "Navigation" block if not present
When I expand "Site pages" node
Then I should not see "Content bank"
Scenario: Teachers can access course level content bank
Given I am on "Course 1" course homepage with editing mode on
And the following config values are set as admin:
| unaddableblocks | | theme_boost|
And I add the "Navigation" block if not present
When I expand "Site pages" node
Then I should see "Content bank"
Scenario: Teachers can upload .h5p extension files to course content bank
Given I log out
And I log in as "admin"
And I navigate to "H5P > Manage H5P content types" in site administration
And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "H5P content type" filemanager
And I click on "Upload H5P content types" "button" in the "#fitem_id_uploadlibraries" "css_element"
And I log out
And I log in as "teacher1"
And I am on "Course 1" course homepage with editing mode on
And the following config values are set as admin:
| unaddableblocks | | theme_boost|
And I add the "Navigation" block if not present
When I expand "Site pages" node
And I click on "Content bank" "link"
Then I should not see "filltheblanks.h5p"
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 click on "Select this file" "button"
And I click on "Save changes" "button"
And I wait until the page is ready
And I should see "filltheblanks.h5p"
Scenario: Other teachers can see uploaded H5P contents
Given I log out
And I log in as "admin"
And I navigate to "H5P > Manage H5P content types" in site administration
And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "H5P content type" filemanager
And I click on "Upload H5P content types" "button" in the "#fitem_id_uploadlibraries" "css_element"
And I log out
And I log in as "teacher1"
And I am on "Course 1" course homepage with editing mode on
And the following config values are set as admin:
| unaddableblocks | | theme_boost|
And I add the "Navigation" block if not present
When 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 click on "Select this file" "button"
And I click on "Save changes" "button"
And I wait until the page is ready
And I should see "filltheblanks.h5p"
And I log out
When I log in as "teacher2"
And I am on "Course 1" course homepage with editing mode on
And the following config values are set as admin:
| unaddableblocks | | theme_boost|
And I add the "Navigation" block if not present
And I expand "Site pages" node
And I click on "Content bank" "link"
Then I should see "filltheblanks.h5p"
Scenario: Teachers can not upload and deploy content types when libraries are not installed
Given I log out
And I log in as "admin"
And I navigate to "H5P > Manage H5P content types" in site administration
And I should not see "Fill in the Blanks"
And I log out
And I log in as "teacher1"
And I am on "Course 1" course homepage with editing mode on
And the following config values are set as admin:
| unaddableblocks | | theme_boost|
And I add the "Navigation" block if not present
And I expand "Site pages" node
And I click on "Content bank" "link"
When 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 click on "Select this file" "button"
And I click on "Save changes" "button"
Then I should see "Missing required library"
And I should not see "filltheblanks.h5p"
And I log out
And I log in as "admin"
And I am on "Course 1" course homepage
And I expand "Site pages" node
And I click on "Content bank" "link"
And I should not see "filltheblanks.h5p"
Scenario: Teachers can not see existing contents when libraries are not installed
Given the following "user private file" exists:
| user | admin |
| filepath | h5p/tests/fixtures/filltheblanks.h5p |
And I log in as "admin"
And I navigate to "H5P > Manage H5P content types" in site administration
And I should not see "Fill in the Blanks"
When I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "H5P content type" filemanager
And I click on "Upload H5P content types" "button" in the "#fitem_id_uploadlibraries" "css_element"
And I wait until the page is ready
And I should see "Fill in the Blanks"
And I log out
And I log in as "teacher1"
Given I am on "Course 1" course homepage with editing mode on
And the following config values are set as admin:
| unaddableblocks | | theme_boost|
And I add the "Navigation" block if not present
When 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 click on "Select this file" "button"
And I click on "Save changes" "button"
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
Then I should see "Of which countries"
Then I should not see "missing-main-library"
And I switch to the main frame
Given I log out
And I log in as "admin"
And I navigate to "H5P > Manage H5P content types" in site administration
When I click on "Delete version" "link" in the "Fill in the Blanks" "table_row"
And I press "Continue"
Then I should not see "Fill in the Blanks"
And I log out
And I log in as "teacher1"
Given I am on "Course 1" course homepage
When I expand "Site pages" node
And I click on "Content bank" "link"
Then I should not see "filltheblanks.h5p"
And I log out
And I log in as "admin"
And I am on "Course 1" course homepage
And I expand "Site pages" node
And I click on "Content bank" "link"
And I should see "filltheblanks.h5p"
And I click on "filltheblanks.h5p" "link"
And I switch to "h5p-player" class iframe
And I should see "missing-main-library"
@@ -0,0 +1,190 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace contenttype_h5p;
/**
* Test for H5P content bank plugin.
*
* @package contenttype_h5p
* @category test
* @copyright 2020 Amaia Anabitarte <amaia@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @coversDefaultClass \contenttype_h5p\content
*/
class content_h5p_test extends \advanced_testcase {
/**
* Tests for uploaded file.
*
* @covers ::get_file
*/
public function test_upload_file(): void {
$this->resetAfterTest();
// Create content.
$record = new \stdClass();
$record->name = 'Test content';
$record->configdata = '';
$contenttype = new \contenttype_h5p\contenttype(\context_system::instance());
$content = $contenttype->create_content($record);
// Create a dummy file.
$filename = 'content.h5p';
$dummy = [
'contextid' => \context_system::instance()->id,
'component' => 'contentbank',
'filearea' => 'public',
'itemid' => $content->get_id(),
'filepath' => '/',
'filename' => $filename
];
$fs = get_file_storage();
$fs->create_file_from_string($dummy, 'dummy content');
$file = $content->get_file();
$this->assertInstanceOf(\stored_file::class, $file);
$this->assertEquals($filename, $file->get_filename());
}
/**
* Tests for is view allowed content.
*
* @covers ::is_view_allowed
* @dataProvider is_view_allowed_provider
*
* @param string $role User role to use for create and view contents.
* @param array $disabledlibraries Library names to disable.
* @param array $expected Array with the expected values for the contents in the following order:
* ['H5P.Blanks deployed', 'H5P.Accordion deployed', 'H5P.Accordion undeployed', 'Invalid content'].
*/
public function test_is_view_allowed(string $role, array $disabledlibraries, array $expected): void {
global $CFG, $USER, $DB;
$this->resetAfterTest();
// Create a course.
$course = $this->getDataGenerator()->create_course();
$coursecontext = \context_course::instance($course->id);
// Set user.
if ($role == 'admin') {
$this->setAdminUser();
} else {
// Enrol user to the course.
$user = $this->getDataGenerator()->create_and_enrol($course, $role);
$this->setUser($user);
}
// Add contents to the content bank.
$generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank');
$filepath = $CFG->dirroot . '/h5p/tests/fixtures/filltheblanks.h5p';
$contents = $generator->generate_contentbank_data('contenttype_h5p', 1, $USER->id, $coursecontext, true, $filepath);
$filltheblanks = array_shift($contents);
$filepath = $CFG->dirroot . '/h5p/tests/fixtures/ipsums.h5p';
$contents = $generator->generate_contentbank_data('contenttype_h5p', 2, $USER->id, $coursecontext, true, $filepath);
$accordion1 = array_shift($contents);
$accordion2 = array_shift($contents);
$filepath = $CFG->dirroot . '/h5p/tests/fixtures/invalid.zip';
$contents = $generator->generate_contentbank_data('contenttype_h5p', 1, $USER->id, $coursecontext, true, $filepath);
$invalid = array_shift($contents);
// Load some of these H5P files though the player to create the H5P DB entries.
$h5pplayer = new \core_h5p\player($filltheblanks->get_file_url(), new \stdClass(), true);
$h5pplayer = new \core_h5p\player($accordion1->get_file_url(), new \stdClass(), true);
// Check the expected H5P content has been created.
$this->assertEquals(2, $DB->count_records('h5p'));
$this->assertEquals(4, $DB->count_records('contentbank_content'));
// Disable libraries.
foreach ($disabledlibraries as $libraryname) {
$libraryid = $DB->get_field('h5p_libraries', 'id', ['machinename' => $libraryname]);
\core_h5p\api::set_library_enabled((int) $libraryid, false);
}
$this->assertEquals($expected[0], $filltheblanks->is_view_allowed());
$this->assertEquals($expected[1], $accordion1->is_view_allowed());
$this->assertEquals($expected[2], $accordion2->is_view_allowed());
$this->assertEquals($expected[3], $invalid->is_view_allowed());
// Check that after enabling libraries again, all the content return true (but the invalid package).
foreach ($disabledlibraries as $libraryname) {
$libraryid = $DB->get_field('h5p_libraries', 'id', ['machinename' => $libraryname]);
\core_h5p\api::set_library_enabled((int) $libraryid, true);
}
$this->assertEquals(true, $filltheblanks->is_view_allowed());
$this->assertEquals(true, $accordion1->is_view_allowed());
$this->assertEquals(true, $accordion2->is_view_allowed()); // It will be deployed, so now it will always return true.
$this->assertEquals($expected[3], $invalid->is_view_allowed());
}
/**
* Data provider for test_is_view_allowed.
*
* @return array
*/
public function is_view_allowed_provider(): array {
return [
'Editing teacher with all libraries enabled' => [
'role' => 'editingteacher',
'disabledlibraries' => [],
'expected' => [true, true, true, false],
],
'Manager with all libraries enabled' => [
'role' => 'manager',
'disabledlibraries' => [],
'expected' => [true, true, true, true],
],
'Admin with all libraries enabled' => [
'role' => 'admin',
'disabledlibraries' => [],
'expected' => [true, true, true, true],
],
'Editing teacher with H5P.Accordion disabled' => [
'role' => 'editingteacher',
'disabledlibraries' => ['H5P.Accordion'],
'expected' => [true, false, false, false],
],
'Manager with H5P.Accordion disabled' => [
'role' => 'manager',
'disabledlibraries' => ['H5P.Accordion'],
'expected' => [true, false, true, true],
],
'Admin with H5P.Accordion disabled' => [
'role' => 'admin',
'disabledlibraries' => ['H5P.Accordion'],
'expected' => [true, false, true, true],
],
'Editing teacher with all libraries disabled' => [
'role' => 'editingteacher',
'disabledlibraries' => ['H5P.Accordion', 'H5P.Blanks'],
'expected' => [false, false, false, false],
],
'Manager with all libraries disabled' => [
'role' => 'manager',
'disabledlibraries' => ['H5P.Accordion', 'H5P.Blanks'],
'expected' => [false, false, true, true],
],
'Admin with all libraries disabled' => [
'role' => 'admin',
'disabledlibraries' => ['H5P.Accordion', 'H5P.Blanks'],
'expected' => [false, false, true, true],
],
];
}
}
@@ -0,0 +1,179 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace contenttype_h5p;
/**
* Test for H5P content bank plugin.
*
* @package contenttype_h5p
* @category test
* @copyright 2020 Amaia Anabitarte <amaia@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @coversDefaultClass \contenttype_h5p\contenttype
*/
class contenttype_h5p_test extends \advanced_testcase {
/**
* Test the behaviour of delete_content().
*/
public function test_delete_content(): void {
global $CFG, $USER, $DB;
$this->resetAfterTest();
$systemcontext = \context_system::instance();
// Create users.
$roleid = $DB->get_field('role', 'id', array('shortname' => 'manager'));
$manager = $this->getDataGenerator()->create_user();
$this->getDataGenerator()->role_assign($roleid, $manager->id);
$this->setUser($manager);
// Add an H5P file to the content bank.
$filepath = $CFG->dirroot . '/h5p/tests/fixtures/filltheblanks.h5p';
$generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank');
$contents = $generator->generate_contentbank_data('contenttype_h5p', 2, $USER->id, $systemcontext, true, $filepath);
$content1 = array_shift($contents);
$content2 = array_shift($contents);
// Load this H5P file though the player to create the H5P DB entries.
$h5pplayer = new \core_h5p\player($content1->get_file_url(), new \stdClass(), true);
$h5pplayer->add_assets_to_page();
$h5pplayer->output();
$h5pplayer = new \core_h5p\player($content2->get_file_url(), new \stdClass(), true);
$h5pplayer->add_assets_to_page();
$h5pplayer->output();
// Check the H5P content has been created.
$this->assertEquals(2, $DB->count_records('h5p'));
$this->assertEquals(2, $DB->count_records('contentbank_content'));
// Check the H5P content is removed after calling this method.
$contenttype = new \contenttype_h5p\contenttype($systemcontext);
$contenttype->delete_content($content1);
$this->assertEquals(1, $DB->count_records('h5p'));
$this->assertEquals(1, $DB->count_records('contentbank_content'));
}
/**
* Tests can_upload behavior.
*
* @covers ::can_upload
*/
public function test_can_upload(): void {
$this->resetAfterTest();
$systemcontext = \context_system::instance();
$systemtype = new \contenttype_h5p\contenttype($systemcontext);
// Admins can upload.
$this->setAdminUser();
$this->assertTrue($systemtype->can_upload());
// Teacher can upload in the course but not at system level.
$course = $this->getDataGenerator()->create_course();
$teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
$coursecontext = \context_course::instance($course->id);
$coursetype = new \contenttype_h5p\contenttype($coursecontext);
$this->setUser($teacher);
$this->assertTrue($coursetype->can_upload());
$this->assertFalse($systemtype->can_upload());
// Users can't upload.
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
$this->assertFalse($coursetype->can_upload());
$this->assertFalse($systemtype->can_upload());
}
/**
* Tests get_icon result.
*
* @covers ::get_icon
*/
public function test_get_icon(): void {
global $CFG;
$this->resetAfterTest();
$systemcontext = \context_system::instance();
$this->setAdminUser();
$contenttype = new contenttype($systemcontext);
// Add an H5P fill the blanks file to the content bank.
$filepath = $CFG->dirroot . '/h5p/tests/fixtures/filltheblanks.h5p';
$generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank');
$contents = $generator->generate_contentbank_data('contenttype_h5p', 1, 0, $systemcontext, true, $filepath);
$filltheblanks = array_shift($contents);
// Add an H5P find the words file to the content bank.
$filepath = $CFG->dirroot . '/h5p/tests/fixtures/find-the-words.h5p';
$generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank');
$contents = $generator->generate_contentbank_data('contenttype_h5p', 1, 0, $systemcontext, true, $filepath);
$findethewords = array_shift($contents);
// Check before deploying the icon for both contents is the same: default one.
// Because we don't know specific H5P content type yet.
$defaulticon = $contenttype->get_icon($filltheblanks);
$this->assertEquals($defaulticon, $contenttype->get_icon($findethewords));
$this->assertStringContainsString('h5p', $defaulticon);
// Deploy one of the contents though the player to create the H5P DB entries and know specific content type.
$h5pplayer = new \core_h5p\player($findethewords->get_file_url(), new \stdClass(), true);
$h5pplayer->add_assets_to_page();
$h5pplayer->output();
// Once the H5P has been deployed, we know the specific H5P content type, so the icon returned is not default one.
$findicon = $contenttype->get_icon($findethewords);
$this->assertNotEquals($defaulticon, $findicon);
$this->assertStringContainsStringIgnoringCase('find', $findicon);
}
/**
* Tests get_download_url result.
*
* @covers ::get_download_url
*/
public function test_get_download_url(): void {
global $CFG;
$this->resetAfterTest();
$systemcontext = \context_system::instance();
$this->setAdminUser();
$contenttype = new contenttype($systemcontext);
// Add an H5P fill the blanks file to the content bank.
$filename = 'filltheblanks.h5p';
$filepath = $CFG->dirroot . '/h5p/tests/fixtures/' . $filename;
$generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank');
$contents = $generator->generate_contentbank_data('contenttype_h5p', 1, 0, $systemcontext, true, $filepath);
$filltheblanks = array_shift($contents);
// Check before deploying the URL is returned OK.
$url1 = $contenttype->get_download_url($filltheblanks);
$this->assertNotEmpty($url1);
$this->assertStringContainsString($filename, $url1);
// Deploy the contents though the player to create the H5P DB entries and know specific content type.
$h5pplayer = new \core_h5p\player($filltheblanks->get_file_url(), new \stdClass(), true);
$h5pplayer->add_assets_to_page();
$h5pplayer->output();
// Once the H5P has been deployed, the URL is still the same.
$url2 = $contenttype->get_download_url($filltheblanks);
$this->assertNotEmpty($url2);
$this->assertEquals($url1, $url2);
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Version details
*
* @package contenttype_h5p
* @copyright 2020 Amaia Anabitarte <amaia@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2024042200; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2024041600; // Requires this Moodle version.
$plugin->component = 'contenttype_h5p'; // Full name of the plugin (used for diagnostics).
+139
View File
@@ -0,0 +1,139 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Create or update contents through the specific content type editor
*
* @package core_contentbank
* @copyright 2020 Victor Deniz <victor@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require('../config.php');
require_login();
$contextid = required_param('contextid', PARAM_INT);
$pluginname = required_param('plugin', PARAM_PLUGIN);
$id = optional_param('id', null, PARAM_INT);
$library = optional_param('library', null, PARAM_RAW);
$context = context::instance_by_id($contextid, MUST_EXIST);
$cb = new \core_contentbank\contentbank();
if (!$cb->is_context_allowed($context)) {
throw new \moodle_exception('contextnotallowed', 'core_contentbank');
}
require_capability('moodle/contentbank:access', $context);
$returnurl = new \moodle_url('/contentbank/view.php', ['id' => $id]);
if (!empty($id)) {
$record = $DB->get_record('contentbank_content', ['id' => $id], '*', MUST_EXIST);
$contentclass = "$record->contenttype\\content";
$content = new $contentclass($record);
// Set the heading title.
$heading = $content->get_name();
// The content type of the content overwrites the pluginname param value.
$contenttypename = $content->get_content_type();
$breadcrumbtitle = get_string('edit');
} else {
$contenttypename = "contenttype_$pluginname";
$heading = get_string('addinganew', 'moodle', get_string('description', $contenttypename));
$content = null;
$breadcrumbtitle = get_string('add');
}
// Check plugin is enabled.
$plugin = core_plugin_manager::instance()->get_plugin_info($contenttypename);
if (!$plugin || !$plugin->is_enabled()) {
throw new \moodle_exception('unsupported', 'core_contentbank', $returnurl);
}
// Create content type instance.
$contenttypeclass = "$contenttypename\\contenttype";
if (class_exists($contenttypeclass)) {
$contenttype = new $contenttypeclass($context);
} else {
throw new \moodle_exception('unsupported', 'core_contentbank', $returnurl);
}
// Checks the user can edit this content and content type.
if (!$contenttype->can_edit($content)) {
throw new \moodle_exception('contenttypenoedit', 'core_contentbank', $returnurl);
}
$values = [
'contextid' => $contextid,
'plugin' => $pluginname,
'id' => $id,
'heading' => $heading,
'library' => $library
];
$title = get_string('contentbank');
\core_contentbank\helper::get_page_ready($context, $title, true);
if ($PAGE->course) {
require_login($PAGE->course->id);
}
if ($context->contextlevel == CONTEXT_COURSECAT) {
$PAGE->set_primary_active_tab('home');
}
$PAGE->set_url(new \moodle_url('/contentbank/edit.php', $values));
if ($context->id == \context_system::instance()->id) {
$PAGE->set_context(context_course::instance($context->id));
} else {
$PAGE->set_context($context);
}
if ($content) {
$PAGE->navbar->add($content->get_name(), new \moodle_url('/contentbank/view.php', ['id' => $id]));
}
$PAGE->navbar->add($breadcrumbtitle);
$PAGE->set_title($title);
$PAGE->set_pagelayout('incourse');
$PAGE->set_secondary_active_tab('contentbank');
// Instantiate the content type form.
$editorclass = "$contenttypename\\form\\editor";
if (!class_exists($editorclass)) {
throw new \moodle_exception('noformdesc');
}
$editorform = new $editorclass(null, $values);
if ($editorform->is_cancelled()) {
if (empty($id)) {
$returnurl = new \moodle_url('/contentbank/index.php', ['contextid' => $context->id]);
}
redirect($returnurl);
} else if ($data = $editorform->get_data()) {
if (empty($id)) {
$msg = get_string('contentcreated', 'contentbank');
} else {
$msg = get_string('contentupdated', 'contentbank');
}
$id = $editorform->save_content($data);
// Just in case we've created a new content.
$returnurl->param('id', $id);
redirect($returnurl, $msg, null, \core\output\notification::NOTIFY_SUCCESS);
}
echo $OUTPUT->header();
$editorform->display();
echo $OUTPUT->footer();
+137
View File
@@ -0,0 +1,137 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* List content in content bank.
*
* @package core_contentbank
* @copyright 2020 Amaia Anabitarte <amaia@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require('../config.php');
require_login();
$contextid = optional_param('contextid', \context_system::instance()->id, PARAM_INT);
$search = optional_param('search', '', PARAM_CLEAN);
$context = context::instance_by_id($contextid, MUST_EXIST);
$cb = new \core_contentbank\contentbank();
if (!$cb->is_context_allowed($context)) {
throw new \moodle_exception('contextnotallowed', 'core_contentbank');
}
require_capability('moodle/contentbank:access', $context);
// If notifications had been sent we don't pay attention to message parameter.
if (empty($SESSION->notifications)) {
$statusmsg = optional_param('statusmsg', '', PARAM_ALPHANUMEXT);
$errormsg = optional_param('errormsg', '', PARAM_ALPHANUMEXT);
}
$title = get_string('contentbank');
\core_contentbank\helper::get_page_ready($context, $title);
if ($PAGE->course) {
require_login($PAGE->course->id);
}
$PAGE->set_url('/contentbank/index.php', ['contextid' => $contextid]);
if ($contextid == \context_system::instance()->id) {
$PAGE->set_context(context_course::instance($contextid));
} else {
$PAGE->set_context($context);
}
if ($context->contextlevel == CONTEXT_COURSECAT) {
$PAGE->set_primary_active_tab('home');
}
$PAGE->set_title($title);
$PAGE->add_body_class('limitedwidth');
$PAGE->set_pagetype('contentbank');
$PAGE->set_secondary_active_tab('contentbank');
// Get all contents managed by active plugins where the user has permission to render them.
$contenttypes = [];
$enabledcontenttypes = $cb->get_enabled_content_types();
foreach ($enabledcontenttypes as $contenttypename) {
$contenttypeclass = "\\contenttype_$contenttypename\\contenttype";
$contenttype = new $contenttypeclass($context);
if ($contenttype->can_access()) {
$contenttypes[] = $contenttypename;
}
}
$foldercontents = $cb->search_contents($search, $contextid, $contenttypes);
// Get the toolbar ready.
$toolbar = array ();
// Place the Add button in the toolbar.
if (has_capability('moodle/contentbank:useeditor', $context)) {
// Get the content types for which the user can use an editor.
$editabletypes = $cb->get_contenttypes_with_capability_feature(\core_contentbank\contenttype::CAN_EDIT, $context);
if (!empty($editabletypes)) {
// Editor base URL.
$editbaseurl = new moodle_url('/contentbank/edit.php', ['contextid' => $contextid]);
$toolbar[] = [
'name' => get_string('add'),
'link' => $editbaseurl, 'dropdown' => true,
'contenttypes' => $editabletypes,
'action' => 'add'
];
}
}
// Place the Upload button in the toolbar.
if (has_capability('moodle/contentbank:upload', $context)) {
// Don' show upload button if there's no plugin to support any file extension.
$accepted = $cb->get_supported_extensions_as_string($context);
if (!empty($accepted)) {
$importurl = new moodle_url('/contentbank/index.php', ['contextid' => $contextid]);
$toolbar[] = [
'name' => get_string('upload', 'contentbank'),
'link' => $importurl->out(false),
'icon' => 'i/upload',
'action' => 'upload'
];
$PAGE->requires->js_call_amd(
'core_contentbank/upload',
'initModal',
['[data-action=upload]', \core_contentbank\form\upload_files::class, $contextid]
);
}
}
echo $OUTPUT->header();
echo $OUTPUT->heading($title, 2);
echo $OUTPUT->box_start('generalbox');
// If needed, display notifications.
if (!empty($errormsg) && get_string_manager()->string_exists($errormsg, 'core_contentbank')) {
$errormsg = get_string($errormsg, 'core_contentbank');
echo $OUTPUT->notification($errormsg);
} else if (!empty($statusmsg) && get_string_manager()->string_exists($statusmsg, 'core_contentbank')) {
$statusmsg = get_string($statusmsg, 'core_contentbank');
echo $OUTPUT->notification($statusmsg, 'notifysuccess');
}
// Render the contentbank contents.
$folder = new \core_contentbank\output\bankcontent($foldercontents, $toolbar, $context, $cb);
echo $OUTPUT->render($folder);
echo $OUTPUT->box_end();
echo $OUTPUT->footer();
+49
View File
@@ -0,0 +1,49 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Library functions for contentbank
*
* @package core_contentbank
* @copyright 2020 Bas Brands
* @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
*
* @uses core_user::is_current_user
*
* @return array[] preferences configuration
*/
function core_contentbank_user_preferences(): array {
return [
'core_contentbank_view_list' => [
'choices' => array(0, 1),
'type' => PARAM_INT,
'null' => NULL_NOT_ALLOWED,
'default' => 0,
'permissioncallback' => [core_user::class, 'is_current_user'],
],
'core_contentbank_visibility' => [
'choices' => [content::VISIBILITY_UNLISTED, content::VISIBILITY_PUBLIC],
'type' => PARAM_INT,
'null' => NULL_NOT_ALLOWED
]
];
}
+231
View File
@@ -0,0 +1,231 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template core_contentbank/bankcontent
Example context (json):
{
"contents": [
{
"name": "Accordion.h5p",
"title": "accordion.h5p",
"timemodified": 1589792272,
"size": "699.3KB",
"bytes": 716126,
"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",
"visibilityunlisted": true
},
{
"name": "resume.pdf",
"title": "resume",
"timemodified": 1589792039,
"size": "699.3KB",
"uses": 3,
"bytes": 716126,
"type": "Archive (PDF)",
"author": "Admin user",
"icon": "http://something/theme/image.php/boost/core/1584597850/f/pdf-64",
"visibilityunlisted": false
}
],
"tools": [
{
"name": "Add",
"dropdown": true,
"link": "http://something/contentbank/edit.php?contextid=1",
"contenttypes": [
{
"name": "H5P Interactive Content",
"baseurl": "http://something/contentbank/edit.php?contextid=1&plugin=h5p",
"types": [
{
"typename": "H5P Interactive Content"
},
{
"typename": "Accordion",
"typeeditorparams": "library=Accordion-1.4",
"typeicon": "http://something/pluginfile.php/1/core_h5p/libraries/13/H5P.Accordion-1.4/icon.svg"
}
]
}
]
},
{
"name": "Upload",
"link": "http://something/contentbank/contenttype/h5p/view.php?url=http://something/pluginfile.php/1/contentbank/public/accordion.h5p",
"icon" : "i/upload"
},
{
"icon": "i/export"
}
],
"allowedcontexts": [
{
"name": "contextid",
"method": "get",
"action": "http://localhost/stable_master/contentbank/index.php",
"options": [
{
"value": "1",
"name": "System",
"selected": true,
"optgroup": false
},
{
"value": "32",
"name": "Category 1",
"selected": false,
"optgroup": false
}
]
}
]
}
}}
<div class="content-bank-container {{#viewlist}}view-list{{/viewlist}} {{^viewlist}}view-grid{{/viewlist}}"
data-region="contentbank">
<div class="d-flex justify-content-between">
<div class="cb-search-container mb-2 mr-2">
{{>core_contentbank/bankcontent/search}}
</div>
<div class="cb-toolbar-container mb-2 d-flex">
{{>core_contentbank/bankcontent/toolbar}}
</div>
</div>
<div class="cb-navigation-container mb-2">
{{>core_contentbank/bankcontent/navigation}}
</div>
<div class="pb-3 border">
<div class="content-bank">
<div class="cb-navbar bg-light p-2 border-bottom">
<div class="cb-navbar-breadbrumb">
{{#pix}} i/folder {{/pix}}
</div>
<div class="cb-navbar-totalsearch d-none">
</div>
</div>
{{#contents.0}}
<div role="{{#viewlist}}table{{/viewlist}}{{^viewlist}}list{{/viewlist}}" aria-label="{{#str}} contentbank, contentbank {{/str}}"
class="cb-content-wrapper d-flex px-2" data-region="filearea">
<div {{#viewlist}}role="row"{{/viewlist}} class="cb-heading bg-white">
<div {{#viewlist}}role="columnheader"{{/viewlist}} aria-sort="none" class="cb-file cb-column d-flex">
<button class="btn btn-block cb-btnsort dir-none text-left" data-string="contentname" data-action="sortname"
title="{{#str}} sortbyx, core, {{#str}} contentname, contentbank {{/str}} {{/str}}">
<span class="title">{{#str}} contentname, contentbank {{/str}}</span>
<span class="default">{{#pix}} t/sort, core {{/pix}}</span>
<span class="desc">{{#pix}} t/sort_desc, core {{/pix}}</span>
<span class="asc">{{#pix}} t/sort_asc, core {{/pix}}</span>
</button>
</div>
<div {{#viewlist}}role="columnheader"{{/viewlist}} aria-sort="none" class="cb-uses cb-column d-flex">
<button class="btn btn-block cb-btnsort dir-none text-left" data-string="uses" data-action="sortuses"
title="{{#str}} sortbyx, core, {{#str}} uses, contentbank {{/str}} {{/str}}">
<span class="title">{{#str}} uses, contentbank {{/str}}</span>
<span class="default">{{#pix}} t/sort, core {{/pix}}</span>
<span class="desc">{{#pix}} t/sort_desc, core {{/pix}}</span>
<span class="asc">{{#pix}} t/sort_asc, core {{/pix}}</span>
</button>
</div>
<div {{#viewlist}}role="columnheader"{{/viewlist}} aria-sort="none" class="cb-date cb-column d-flex">
<button class="btn btn-block cb-btnsort dir-none text-left" data-string="lastmodified" data-action="sortdate"
title="{{#str}} sortbyx, core, {{#str}} lastmodified, contentbank {{/str}} {{/str}}">
<span class="title">{{#str}} lastmodified, contentbank {{/str}}</span>
<span class="default">{{#pix}} t/sort, core {{/pix}}</span>
<span class="desc">{{#pix}} t/sort_desc, core {{/pix}}</span>
<span class="asc">{{#pix}} t/sort_asc, core {{/pix}}</span>
</button>
</div>
<div {{#viewlist}}role="columnheader"{{/viewlist}} aria-sort="none" class="cb-size cb-column d-flex">
<button class="btn btn-block cb-btnsort dir-none text-left" data-string="size" data-action="sortsize"
title="{{#str}} sortbyx, core, {{#str}} size, contentbank {{/str}} {{/str}}">
<span class="title">{{#str}} size, contentbank {{/str}}</span>
<span class="default">{{#pix}} t/sort, core {{/pix}}</span>
<span class="desc">{{#pix}} t/sort_desc, core {{/pix}}</span>
<span class="asc">{{#pix}} t/sort_asc, core {{/pix}}</span>
</button>
</div>
<div {{#viewlist}}role="columnheader"{{/viewlist}} aria-sort="none" class="cb-type cb-column d-flex">
<button class="btn btn-block cb-btnsort dir-none text-left" data-string="type" data-action="sorttype"
title="{{#str}} sortbyx, core, {{#str}} type, contentbank {{/str}} {{/str}}">
<span class="title">{{#str}} type, contentbank {{/str}}</span>
<span class="default">{{#pix}} t/sort, core {{/pix}}</span>
<span class="desc">{{#pix}} t/sort_desc, core {{/pix}}</span>
<span class="asc">{{#pix}} t/sort_asc, core {{/pix}}</span>
</button>
</div>
<div {{#viewlist}}role="columnheader"{{/viewlist}} aria-sort="none" class="cb-author cb-column d-flex last">
<button class="btn btn-block cb-btnsort dir-none text-left" data-string="author" data-action="sortauthor"
title="{{#str}} sortbyx, core, {{#str}} author, contentbank {{/str}} {{/str}}">
<span class="title">{{#str}} author, contentbank {{/str}}</span>
<span class="default">{{#pix}} t/sort, core {{/pix}}</span>
<span class="desc">{{#pix}} t/sort_desc, core {{/pix}}</span>
<span class="asc">{{#pix}} t/sort_asc, core {{/pix}}</span>
</button>
</div>
</div>
{{#contents}}
<div role="{{#viewlist}}row{{/viewlist}}{{^viewlist}}listitem{{/viewlist}}"
class="cb-listitem {{#visibilityunlisted}}cb-unlisted{{/visibilityunlisted}}"
data-file="{{ title }}"
data-name="{{ name }}"
data-bytes="{{ bytes }}"
data-uses="{{ uses }}"
data-timemodified="{{ timemodified }}"
data-type="{{ type }}"
data-author="{{ author }}">
<div {{#viewlist}}role="cell"{{/viewlist}} class="cb-file cb-column position-relative">
<div class="cb-thumbnail" style="background-image: url('{{{ icon }}}');">
</div>
<a href="{{{ link }}}" class="cb-link stretched-link">
<span class="cb-name word-break-all clamp-2" data-region="cb-content-name">
{{{ name }}}
</span>
</a>
</div>
<div {{#viewlist}}role="cell"{{/viewlist}} class="cb-uses cb-column small">
{{ uses }}
</div>
<div {{#viewlist}}role="cell"{{/viewlist}} class="cb-date cb-column small">
{{#userdate}} {{ timemodified }}, {{#str}} strftimedatetimeshort, core_langconfig {{/str}} {{/userdate}}
</div>
<div {{#viewlist}}role="cell"{{/viewlist}} class="cb-size cb-column small">
{{ size }}
</div>
<div {{#viewlist}}role="cell"{{/viewlist}} class="cb-type cb-column small">
{{{ type }}}
</div>
<div {{#viewlist}}role="cell"{{/viewlist}} class="cb-type cb-column last small">
{{{ author }}}
</div>
</div>
{{/contents}}
</div>
{{/contents.0}}
{{^contents.0}}
<div class="cb-content-wrapper d-flex flex-wrap p-2" data-region="filearea">
<div class="w-100 p-3 text-center text-muted">
{{#str}} nocontentavailable, core_contentbank {{/str}}
</div>
</div>
{{/contents.0}}
</div>
</div>
</div>
@@ -0,0 +1,27 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template core_contentbank/bankcontent/navigation
Example context (json):
{
}
}}
{{#allowedcontexts}}
{{> core/single_select }}
{{/allowedcontexts}}
@@ -0,0 +1,31 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template core_contentbank/bankcontent/search
Example context (json):
{
}
}}
{{< core/search_input_auto }}
{{$label}}{{#str}}
searchcontentbankbyname, contentbank
{{/str}}{{/label}}
{{$placeholder}}{{#str}} search, core {{/str}}{{/placeholder}}
{{/ core/search_input_auto }}
@@ -0,0 +1,76 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template core_contentbank/bankcontent/toolbar
Example context (json):
{
"tools": [
{
"name": "Add",
"dropdown": true,
"link": "http://something/contentbank/edit.php?contextid=1",
"contenttypes": [
{
"name": "h5p",
"baseurl": "http://something/contentbank/edit.php?contextid=1&plugin=h5p",
"types": [
{
"typename": "H5P Interactive Content"
},
{
"typename": "Accordion",
"typeeditorparams": "library=Accordion-1.4",
"typeicon": "http://something/pluginfile.php/1/core_h5p/libraries/13/H5P.Accordion-1.4/icon.svg"
}
]
}
]
},
{
"name": "Upload",
"link": "http://something/contentbank/contenttype/h5p/view.php?url=http://something/pluginfile.php/1/contentbank/public/accordion.h5p",
"icon" : "i/upload"
},
{
"icon": "i/export"
}
]
}
}}
{{#tools}}
{{#dropdown}}
{{>core_contentbank/bankcontent/toolbar_dropdown}}
{{/dropdown}}
{{^dropdown}}
<a href="{{ link }}" class="icon-no-margin btn btn-secondary text-nowrap" title="{{ name }}" data-action="{{ action }}">
{{#pix}} {{{ icon }}} {{/pix}} {{{ name }}}
</a>
{{/dropdown}}
{{/tools}}
<button class="icon-no-margin btn btn-secondary {{^viewlist}}active{{/viewlist}} ml-2"
title="{{#str}} displayicons, contentbank {{/str}}"
data-action="viewgrid">
{{#pix}}a/view_icon_active, core, {{#str}} displayicons, contentbank {{/str}} {{/pix}}
</button>
<button class="icon-no-margin btn btn-secondary {{#viewlist}}active{{/viewlist}}"
title="{{#str}} displaydetails, contentbank {{/str}}"
data-action="viewlist">
{{#pix}}t/viewdetails, core, {{#str}} displaydetails, contentbank {{/str}} {{/pix}}
</button>
@@ -0,0 +1,69 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template core_contentbank/bankcontent/toolbar_dropdown
Example context (json):
{
"name": "Add",
"dropdown": true,
"link": "http://something/contentbank/edit.php?contextid=1",
"contenttypes": [
{
"name": "h5p",
"baseurl": "http://something/contentbank/edit.php?contextid=1&plugin=h5p",
"types": [
{
"typename": "H5P Interactive Content"
},
{
"typename": "Accordion",
"typeeditorparams": "library=Accordion-1.4",
"typeicon": "http://something/pluginfile.php/1/core_h5p/libraries/13/H5P.Accordion-1.4/icon.svg"
}
]
}
]
}
}}
<div class="btn-group mr-1" role="group">
<button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" data-action="{{name}}-content"
aria-haspopup="true" aria-expanded="false" {{^contenttypes}}title="{{#str}}nocontenttypes, core_contentbank{{/str}}"
disabled{{/contenttypes}}>
{{#name}} {{name}} {{/name}}
</button>
<div class="dropdown-menu dropdown-scrollable dropdown-menu-right">
{{#contenttypes}}
{{#types}}
{{^typeeditorparams}}
<h6 class="dropdown-header">{{ typename }}</h6>
{{/typeeditorparams}}
{{#typeeditorparams}}
<a class="dropdown-item icon-size-4" href="{{{ baseurl }}}&{{{ typeeditorparams }}}">
{{#typeicon}}
<img alt="" class="icon" src="{{{ typeicon }}}">
{{/typeicon}}
{{^typeicon}}
{{#pix}} b/h5p_library, core {{/pix}}
{{/typeicon}} {{ typename }}
</a>
{{/typeeditorparams}}
{{/types}}
{{/contenttypes}}
</div>
</div>
@@ -0,0 +1,57 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more comments.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template core_contentbank/contentbankmenu
Contentbank view toolbar.
Classes required for JS:
* none
Data attributes required for JS:
* none
Context variables required for this template:
* options - JSON object - the options available in the dropdown
Example context (json):
{
"options": [
{
"url": "www.google.com",
"label": "Google",
"attributes": [
{
"name": "data-attrib",
"value": "1"
}
]
}
]
}
}}
<div class="dropdown d-flex">
<button aria-label="{{#str}}actionsmenu{{/str}}" class="btn btn-secondary dropdown-toggle" id="dropdown-actions" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{#str}}more, core_contentbank{{/str}}
</button>
{{! dropdown-menu-right style to fix RTL placement }}
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdown-actions">
{{#options}}
<a class="dropdown-item {{extraclasses}}" {{#attributes}}{{name}}="{{value}}"{{/attributes}} href="{{url}}">{{label}}</a>
{{/options}}
</div>
</div>
@@ -0,0 +1,30 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template core_contentbank/copycontent
Example context (json):
{
"contentid": 17,
"name": "Accordion"
}
}}
<div class="form-check w-100 justify-content-start">
<label for="newname">{{#str}}contentname, core_contentbank{{/str}}</label>
<input type="text" size="5" id="newname" name="newname" value="{{ name }}" class="form-control">
</div>
@@ -0,0 +1,30 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template core_contentbank/renamecontent
Example context (json):
{
"contentid": 17,
"name": "Accordion"
}
}}
<div class="form-check w-100 justify-content-start">
<label for="newname">{{#str}}contentname, core_contentbank{{/str}}</label>
<input type="text" size="5" id="newname" name="newname" value="{{ name }}" class="form-control text-ltr">
</div>
@@ -0,0 +1,47 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more comments.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template core_contentbank/viewcontent
View content page.
Classes required for JS:
* none
Data attributes required for JS:
* none
Context variables required for this template:
* contenthtml - string - content html.
* usercanedit - boolean - whether the user has permission to edit the content.
* editcontenturl - string - edit page URL.
* closeurl - string - close landing page.
Example context (json):
{
"contenthtml" : "<iframe src=\"http://something/h5p/embed.php?url=h5pfileurl\"></iframe>",
"usercanedit" : true,
"editcontenturl" : "http://something/contentbank/edit.php?contextid=1&plugin=h5p&id=1",
"closeurl" : "http://moodle.test/h5pcb/moodle/contentbank/index.php"
}
}}
<div class="core_contentbank_viewcontent">
{{>core_contentbank/viewcontent/toolbarview}}
<div class="mt-1 mb-1" data-region="viewcontent-content">
{{{ contenthtml }}}
</div>
</div>
@@ -0,0 +1,81 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more comments.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template core_contentbank/viewcontent/toolbarview
Contentbank view toolbar.
Classes required for JS:
* none
Data attributes required for JS:
* none
Context variables required for this template:
* contenthtml - string - content html.
* usercanedit - boolean - whether the user has permission to edit the content.
* editcontenturl - string - edit page URL.
* closeurl - string - close landing page.
Example context (json):
{
"usercanedit" : true,
"heading" : "This is a heading",
"editcontenturl" : "http://something/contentbank/edit.php?contextid=1&plugin=h5p&id=1",
"closeurl" : "http://moodle.test/h5pcb/moodle/contentbank/index.php",
"actionmenu": {
"options": [
{
"url": "www.google.com",
"label": "Google",
"attributes": [
{
"name": "data-attrib",
"value": "1"
}
]
}
]
}
}
}}
{{#heading}}<h2>{{heading}}</h2>{{/heading}}
<div class="cb-toolbar-container container-fluid my-2">
<div class="row">
{{#usercanedit}}
<div class="d-flex mr-3">
<a href="{{editcontenturl}}" class="btn btn-primary" data-action="edit-content">
{{#str}}edit{{/str}}
</a>
</div>
{{/usercanedit}}
{{#actionmenu}}
<div>
{{> core_contentbank/contentbankmenu }}
</div>
{{/actionmenu}}
{{#closeurl}}
<div class="ml-auto">
<a href="{{closeurl}}" class="btn btn-secondary" data-action="close-content">
{{#str}}exit, core_contentbank{{/str}}
</a>
</div>
{{/closeurl}}
</div>
</div>
@@ -0,0 +1,30 @@
@core @core_contentbank
Feature: Access permission to content Bank
In order to control access to content bank
As an admin
I need to be able to configure users' permissions
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | user1@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
Scenario: Admins access content bank
Given I log in as "admin"
And I am on "Course 1" course homepage
Then "Content bank" "link" should exist
Scenario: Editing teachers can access content bank at course level
Given I log in as "teacher1"
And I am on "Course 1" course homepage with editing mode on
Then "Content bank" "link" should exist
Scenario: Editing teachers can't access content bank at system level
Given I log in as "teacher1"
Then "Content bank" "link" should not exist
@@ -0,0 +1,175 @@
@core @core_contentbank @core_h5p @contentbank_h5p @_file_upload @javascript
Feature: Copy content from the content bank
In order copy content from the content bank
As an admin
I need to be able to copy any content from the content bank
Background:
Given the following "contentbank content" exist:
| contextlevel | reference | contenttype | user | contentname | filepath |
| System | | contenttype_h5p | admin | content2copy.h5p | /h5p/tests/fixtures/filltheblanks.h5p |
And I log in as "admin"
And I am on site homepage
And I turn editing mode on
And the following config values are set as admin:
| unaddableblocks | | theme_boost|
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"
Scenario: Admins can copy content from the content bank
Given I log in as "admin"
And I am on site homepage
And I click on "Site pages" "list_item" in the "Navigation" "block"
And I click on "Content bank" "link"
And I click on "content2copy.h5p" "link"
And I click on "More" "button"
And I click on "Copy content" "link" in the ".cb-toolbar-container" "css_element"
And I set the following fields to these values:
| Content name | |
And I click on "Save changes" "button"
Then I should see "Empty name is not allowed"
And I click on "OK" "button" in the "Error" "dialogue"
And I set the following fields to these values:
| Content name | Fill the blanks copy 1 |
And I click on "Save changes" "button"
Then I should see "Fill the blanks copy 1"
And I click on "Edit" "link"
And I switch to "h5p-editor-iframe" class iframe
Then the field "Title" matches value "Geography"
Scenario: Users without the required capability cannot copy content
Given the following "users" exist:
| username | firstname | lastname | email |
| manager | Max | Manager | man@example.com |
And the following "role assigns" exist:
| user | role | contextlevel | reference |
| manager | manager | System | |
And the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| moodle/contentbank:copycontent | Prohibit | manager | System | |
| moodle/contentbank:copyanycontent | Prohibit | manager | System | |
And I log out
And I log in as "manager"
And 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 "content2copy.h5p"
And I follow "content2copy.h5p"
And I click on "More" "button"
Then I should not see "Copy content"
Scenario: Users can't copy content if they don't have the required permission
Given I log in as "admin"
And I am on site homepage
Given I navigate to "H5P > Manage H5P content types" in site administration
And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "H5P content type" filemanager
And I click on "Upload H5P content types" "button" in the "#fitem_id_uploadlibraries" "css_element"
And I upload "h5p/tests/fixtures/ipsums.h5p" file to "H5P content type" filemanager
And I click on "Upload H5P content types" "button" in the "#fitem_id_uploadlibraries" "css_element"
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | user1@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following "contentbank content" exist:
| contextlevel | reference | contenttype | user | contentname | filepath |
| Course | C1 | contenttype_h5p | teacher1 | filltheblanks.h5p | /h5p/tests/fixtures/filltheblanks.h5p |
When I log out
And I log in as "teacher1"
And I am on "Course 1" course homepage
And 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 "filltheblanks.h5p"
And I click on "More" "button"
Then I should see "Copy content"
And the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| moodle/contentbank:copycontent | Prohibit | editingteacher | System | |
And I reload the page
And I click on "More" "button"
Then I should not see "Copy content"
Scenario: Teachers can copy their own content in the content bank
Given I log in as "admin"
And I am on site homepage
Given I navigate to "H5P > Manage H5P content types" in site administration
And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "H5P content type" filemanager
And I click on "Upload H5P content types" "button" in the "#fitem_id_uploadlibraries" "css_element"
And I upload "h5p/tests/fixtures/ipsums.h5p" file to "H5P content type" filemanager
And I click on "Upload H5P content types" "button" in the "#fitem_id_uploadlibraries" "css_element"
And the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following "contentbank content" exist:
| contextlevel | reference | contenttype | user | contentname | filepath |
| Course | C1 | contenttype_h5p | admin | filltheblanks.h5p | /h5p/tests/fixtures/filltheblanks.h5p |
| Course | C1 | contenttype_h5p | teacher1 | ipsums.h5p | /h5p/tests/fixtures/ipsums.h5p |
When I log out
And I log in as "teacher1"
And I am on "Course 1" course homepage
And I click on "Site pages" "list_item" in the "Navigation" "block"
And I click on "Content bank" "link" in the "Navigation" "block"
And I follow "ipsums.h5p"
And I click on "More" "button"
Then I should see "Copy content"
Scenario: Teachers can't copy content created by other users in the content bank
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following "contentbank content" exist:
| contextlevel | reference | contenttype | user | contentname | filepath |
| Course | C1 | contenttype_h5p | admin | filltheblanks.h5p | /h5p/tests/fixtures/filltheblanks.h5p |
| Course | C1 | contenttype_h5p | teacher1 | ipsums.h5p | /h5p/tests/fixtures/ipsums.h5p |
When I log out
And I log in as "teacher1"
And I am on "Course 1" course homepage
And I click on "Site pages" "list_item" in the "Navigation" "block"
And I click on "Content bank" "link" in the "Navigation" "block"
And I follow "filltheblanks.h5p"
And I click on "More" "button"
Then I should not see "Copy content"
Scenario: Teachers can copy any content created by other users in the content bank if allowed
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following "contentbank content" exist:
| contextlevel | reference | contenttype | user | contentname | filepath |
| Course | C1 | contenttype_h5p | admin | filltheblanks.h5p | /h5p/tests/fixtures/filltheblanks.h5p |
| Course | C1 | contenttype_h5p | teacher1 | ipsums.h5p | /h5p/tests/fixtures/ipsums.h5p |
And the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| moodle/contentbank:copyanycontent | Allow | editingteacher | System | |
When I log out
And I log in as "teacher1"
And I am on "Course 1" course homepage
And I click on "Site pages" "list_item" in the "Navigation" "block"
And I click on "Content bank" "link"
And I follow "filltheblanks.h5p"
And I click on "More" "button"
Then I should see "Copy content"
@@ -0,0 +1,84 @@
@core @core_contentbank @core_h5p @contentbank_h5p @javascript
Feature: Delete H5P file from the content bank
In order remove H5P content from the content bank
As an admin
I need to be able to delete any H5P content from the content bank
Background:
Given the following "user private file" exists:
| user | admin |
| filepath | h5p/tests/fixtures/filltheblanks.h5p |
And I am on the "Content bank" page logged in as "admin"
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 "content2delete.h5p"
And I click on "Select this file" "button"
And I click on "Save changes" "button"
Scenario: Admins can delete content from the content bank
Given I wait "2" seconds
And I click on "More" "button"
And I should see "Delete"
And I click on "Delete" "link"
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 "content2delete.h5p"
And I wait "2" seconds
And I click on "More" "button"
And I click on "Delete" "link" in the ".cb-toolbar-container" "css_element"
And I click on "Delete" "button" in the "Delete content" "dialogue"
And I wait until the page is ready
And I should see "Content deleted."
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:
| capability | permission | role | contextlevel | reference |
| moodle/contentbank:deleteanycontent | Prohibit | manager | System | |
And the following "users" exist:
| username | firstname | lastname | email |
| manager | Max | Manager | man@example.com |
And the following "role assigns" exist:
| user | role | contextlevel | reference |
| manager | manager | System | |
And the following "user private file" exists:
| user | manager |
| filepath | h5p/tests/fixtures/find-the-words.h5p |
When I am on the "Content bank" page logged in as "manager"
And I should see "content2delete.h5p"
And I follow "content2delete.h5p"
And I wait "2" seconds
And I click on "More" "button"
Then I should not see "Delete"
And I am on the "Content bank" page
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 "find-the-words.h5p" "link"
And I click on "Select this file" "button"
And I click on "Save changes" "button"
And I wait "2" seconds
And I click on "More" "button"
And I should see "Delete"
Scenario: The number of times a content is used is displayed before removing it
Given I am on the "My private files" page
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"
And I am on the "Content bank" page
And I follow "content2delete.h5p"
And I wait "2" seconds
And I click on "More" "button"
And I click on "Delete" "link" in the ".cb-toolbar-container" "css_element"
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 "Content deleted."
And I should not see "content2delete.h5p"
@@ -0,0 +1,63 @@
@core @core_contentbank @core_h5p @contentbank_h5p @_file_upload @javascript
Feature: Download H5P content from the content bank
In order export H5P content from the content bank
As an admin
I need to be able to download any H5P content from the content bank
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| manager | Max | Manager | man@example.com |
And the following "role assigns" exist:
| user | role | contextlevel | reference |
| manager | manager | System | |
And the following "contentbank content" exist:
| contextlevel | reference | contenttype | user | contentname | filepath |
| System | | contenttype_h5p | admin | filltheblanksadmin.h5p | /h5p/tests/fixtures/filltheblanks.h5p |
| System | | contenttype_h5p | manager | filltheblanksmanager.h5p | /h5p/tests/fixtures/filltheblanks.h5p |
And I log in as "admin"
And I am on site homepage
And I turn editing mode on
And the following config values are set as admin:
| unaddableblocks | | theme_boost|
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"
Scenario: Admins can download content from the content bank
Given 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 "filltheblanksmanager.h5p"
And I click on "More" "button"
And I should see "Download"
When I click on "Download" "link"
Then I should see "filltheblanksmanager.h5p"
Scenario: Users can download content created by different users
Given the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| moodle/contentbank:manageanycontent | Prohibit | manager | System | |
And I log out
And I log in as "manager"
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 "filltheblanksadmin.h5p"
And I follow "filltheblanksadmin.h5p"
And I click on "More" "button"
Then I should see "Download"
And I should not see "Rename"
Scenario: Users without the required capability cannot download content
Given the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| moodle/contentbank:downloadcontent | Prohibit | manager | System | |
And I log out
And I log in as "manager"
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 "filltheblanksmanager.h5p"
And I follow "filltheblanksmanager.h5p"
And I click on "More" "button"
Then I should not see "Download"
@@ -0,0 +1,199 @@
@core @core_contentbank @core_h5p @contentbank_h5p @_file_upload @javascript
Feature: Content bank use editor feature
In order to add/edit content
As a user
I need to be able to access the edition options
Background:
Given I log in as "admin"
And I am on site homepage
And I turn editing mode on
And the following config values are set as admin:
| unaddableblocks | | theme_boost|
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"
Scenario: Users see the Add button disabled if there is no content type available for creation
Given I click on "Site pages" "list_item" in the "Navigation" "block"
When I click on "Content bank" "link"
Then the "[data-action=Add-content]" "css_element" should be disabled
Scenario: Users can see the Add button if there is content type available for creation
Given the following "user private file" exists:
| user | admin |
| filepath | h5p/tests/fixtures/filltheblanks.h5p |
And I click on "Site pages" "list_item" in the "Navigation" "block"
And I click on "Content bank" "link" in the "Navigation" "block"
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 click on "Select this file" "button"
And I click on "Save changes" "button"
When I click on "Content bank" "link"
And I click on "filltheblanks.h5p" "link"
And I click on "Exit" "link"
Then I click on "[data-action=Add-content]" "css_element"
And I should see "Fill in the Blanks"
Scenario: Users can edit content if they have the required permission
Given the following "user private file" exists:
| user | admin |
| filepath | h5p/tests/fixtures/filltheblanks.h5p |
And I click on "Site pages" "list_item" in the "Navigation" "block"
And I click on "Content bank" "link" in the "Navigation" "block"
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 click on "Select this file" "button"
And I click on "Save changes" "button"
When I click on "Content bank" "link"
And I click on "filltheblanks.h5p" "link"
Then I click on "Edit" "link"
And I switch to "h5p-editor-iframe" class iframe
And I switch to the main frame
And I click on "Cancel" "button"
And "filltheblanks.h5p" "heading" should exist
Scenario: Users can create new content if they have the required permission
Given I navigate to "H5P > Manage H5P content types" in site administration
And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "H5P content type" filemanager
And I click on "Upload H5P content types" "button" in the "#fitem_id_uploadlibraries" "css_element"
And I should see "H5P content types uploaded successfully"
And I click on "Site pages" "list_item" in the "Navigation" "block"
When I click on "Content bank" "link" in the "Navigation" "block"
And I click on "[data-action=Add-content]" "css_element"
Then I click on "Fill in the Blanks" "link"
And I switch to "h5p-editor-iframe" class iframe
And I switch to the main frame
And I click on "Cancel" "button"
Scenario: Users can't edit content if they don't have the required permission
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | user1@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And I navigate to "H5P > Manage H5P content types" in site administration
And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "H5P content type" filemanager
And I click on "Upload H5P content types" "button" in the "#fitem_id_uploadlibraries" "css_element"
And I should see "H5P content types uploaded successfully"
And I log out
And I log in as "teacher1"
And I am on "Course 1" course homepage
And I click on "Site pages" "list_item" in the "Navigation" "block"
And I click on "Content bank" "link"
And "[data-action=Add-content]" "css_element" should exist
When the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| moodle/contentbank:useeditor | Prohibit | editingteacher | System | |
And I reload the page
Then "[data-action=Add-content]" "css_element" should not exist
Scenario: Users can edit content and save changes
Given the following "contentbank content" exist:
| contextlevel | reference | contenttype | user | contentname | filepath |
| System | | contenttype_h5p | admin | filltheblanks.h5p | /h5p/tests/fixtures/filltheblanks.h5p |
And I click on "Site pages" "list_item" in the "Navigation" "block"
And I click on "Content bank" "link" in the "Navigation" "block"
And I click on "filltheblanks.h5p" "link"
And I click on "Edit" "link"
And I switch to "h5p-editor-iframe" class iframe
And the field "Title" matches value "Geography"
And I set the field "Title" to "New title"
And I switch to the main frame
When I click on "Save" "button"
And "filltheblanks.h5p" "heading" should exist
And I click on "Edit" "link"
And I switch to "h5p-editor-iframe" class iframe
Then the field "Title" matches value "New title"
Scenario: Teachers can edit their own content in the content bank
Given I navigate to "H5P > Manage H5P content types" in site administration
And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "H5P content type" filemanager
And I click on "Upload H5P content types" "button" in the "#fitem_id_uploadlibraries" "css_element"
And I upload "h5p/tests/fixtures/ipsums.h5p" file to "H5P content type" filemanager
And I click on "Upload H5P content types" "button" in the "#fitem_id_uploadlibraries" "css_element"
And the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following "contentbank content" exist:
| contextlevel | reference | contenttype | user | contentname | filepath |
| Course | C1 | contenttype_h5p | admin | filltheblanks.h5p | /h5p/tests/fixtures/filltheblanks.h5p |
| Course | C1 | contenttype_h5p | teacher1 | ipsums.h5p | /h5p/tests/fixtures/ipsums.h5p |
When I log out
And I log in as "teacher1"
And I am on "Course 1" course homepage
And I expand "Site pages" node
And I click on "Content bank" "link"
And I follow "ipsums.h5p"
Then "Edit" "link" should exist in the "region-main" "region"
Scenario: Teachers can't edit content created by other users in the content bank
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following "contentbank content" exist:
| contextlevel | reference | contenttype | user | contentname | filepath |
| Course | C1 | contenttype_h5p | admin | filltheblanks.h5p | /h5p/tests/fixtures/filltheblanks.h5p |
| Course | C1 | contenttype_h5p | teacher1 | ipsums.h5p | /h5p/tests/fixtures/ipsums.h5p |
When I log out
And I log in as "teacher1"
And I am on "Course 1" course homepage
And I expand "Site pages" node
And I click on "Content bank" "link"
And I follow "filltheblanks.h5p"
Then "Edit" "link" should not exist in the "region-main" "region"
Scenario: Teachers keep their content authoring in copied courses
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following "contentbank content" exist:
| contextlevel | reference | contenttype | user | contentname | filepath |
| Course | C1 | contenttype_h5p | admin | filltheblanks.h5p | /h5p/tests/fixtures/filltheblanks.h5p |
| Course | C1 | contenttype_h5p | teacher1 | ipsums.h5p | /h5p/tests/fixtures/ipsums.h5p |
And I am on the "Course 1" "course copy" page
And I set the following fields to these values:
| Course full name | Copy |
| Course short name | Copy |
| Teacher | 1 |
When I press "Copy and view"
And I trigger cron
And I am on homepage
And I log out
And I log in as "teacher1"
And I am on "Copy" course homepage
And I expand "Site pages" node
And I click on "Content bank" "link"
And I follow "ipsums.h5p"
Then "Edit" "link" should exist in the "region-main" "region"
And I navigate to "Content bank" in current page administration
And I follow "filltheblanks.h5p"
Then "Edit" "link" should not exist in the "region-main" "region"
+75
View File
@@ -0,0 +1,75 @@
@core @core_contentbank @core_h5p @contentbank_h5p @_switch_iframe @javascript
Feature: Confirm content bank events are triggered
In order to log content bank actions
As an admin
I need to be able to check triggered events
Background:
Given the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "contentbank content" exist:
| contextlevel | reference | contenttype | user | contentname | filepath |
| Course | C1 | contenttype_h5p | admin | Existing | /h5p/tests/fixtures/filltheblanks.h5p |
And the following "user private file" exists:
| user | admin |
| filepath | h5p/tests/fixtures/filltheblanks.h5p |
And the following config values are set as admin:
| unaddableblocks | | theme_boost|
And I log in as "admin"
And I am on "Course 1" course homepage with editing mode on
And I add the "Navigation" block if not present
Scenario: Content created and uploaded events when uploading a content file
Given I navigate to "Reports > Live logs" in site administration
And I should not see "Content uploaded"
And I should not see "Content created"
And I am on "Course 1" course homepage
And I expand "Site pages" node
And I click on "Content bank" "link"
When 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 click on "Select this file" "button"
And I click on "Save changes" "button"
And I navigate to "Reports > Live logs" in site administration
Then I should see "Content uploaded"
And I should see "Content created"
Scenario: Content viewed event
Given I navigate to "Reports > Live logs" in site administration
And I should not see "Content viewed"
And I am on "Course 1" course homepage
And I expand "Site pages" node
And I click on "Content bank" "link"
When I click on "Existing" "link"
And I navigate to "Reports > Live logs" in site administration
Then I should see "Content viewed"
Scenario: Content deleted event
Given I navigate to "Reports > Live logs" in site administration
And I should not see "Content deleted"
And I am on "Course 1" course homepage
And I expand "Site pages" node
And I click on "Content bank" "link"
And I click on "Existing" "link"
And I click on "More" "button"
When I click on "Delete" "link"
And I click on "Delete" "button" in the "Delete content" "dialogue"
And I navigate to "Reports > Live logs" in site administration
Then I should see "Content deleted"
Scenario: Content updated event when renaming
Given I navigate to "Reports > Live logs" in site administration
And I should not see "Content updated"
And I am on "Course 1" course homepage
And I expand "Site pages" node
And I click on "Content bank" "link"
And I click on "Existing" "link"
And I click on "More" "button"
When I click on "Rename" "link"
And I set the field "Content name" to "New name"
And I click on "Rename" "button"
And I navigate to "Reports > Live logs" in site administration
Then I should see "Content updated"
@@ -0,0 +1,101 @@
@core @core_contentbank @core_h5p @contentbank_h5p @_file_upload @javascript
Feature: Navigate to different contexts in the content bank
In order to navigate easily in the content bank
I need to be able to view dropdown with all allowed contexts in the content bank
Background:
Given I log in as "admin"
And the following "categories" exist:
| name | category | idnumber |
| Cat 1 | 0 | CAT1 |
| Cat 2 | 0 | CAT2 |
And the following "courses" exist:
| fullname | shortname | category |
| Course 0 | C0 | |
| Course 1 | C1 | CAT1 |
| Course 2 | C2 | CAT2 |
And I navigate to "H5P > Manage H5P content types" in site administration
And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "H5P content type" filemanager
And I click on "Upload H5P content types" "button" in the "#fitem_id_uploadlibraries" "css_element"
And the following "contentbank content" exist:
| contextlevel | reference | contenttype | user | contentname | filepath |
| System | | contenttype_h5p | admin | santjordi.h5p | /h5p/tests/fixtures/filltheblanks.h5p |
| Category | CAT1 | contenttype_h5p | admin | santjordi_rose.h5p | /h5p/tests/fixtures/filltheblanks.h5p |
| Category | CAT2 | contenttype_h5p | admin | SantJordi_book | /h5p/tests/fixtures/filltheblanks.h5p |
| Course | C0 | contenttype_h5p | admin | Dragon.h5p | /h5p/tests/fixtures/filltheblanks.h5p |
| Course | C1 | contenttype_h5p | admin | princess.h5p | /h5p/tests/fixtures/filltheblanks.h5p |
| Course | C2 | contenttype_h5p | admin | mathsbook.h5p | /h5p/tests/fixtures/filltheblanks.h5p |
Scenario: Admins can view and navigate to all the contexts in the content bank
Given I am on site homepage
And I turn editing mode on
And the following config values are set as admin:
| unaddableblocks | | theme_boost|
And I add the "Navigation" block if not present
And I expand "Site pages" node
When I click on "Content bank" "link"
And the "Choose course or category" select box should contain "System"
And the "Choose course or category" select box should contain "Cat 1"
And the "Choose course or category" select box should contain "Cat 2"
And the "Choose course or category" select box should contain "Course 0"
And the "Choose course or category" select box should contain "Course 1"
And the "Choose course or category" select box should contain "Course 2"
And I should see "santjordi.h5p"
And I should not see "santjordi_rose.h5p"
And I should not see "Dragon.h5p"
And I set the field "Choose course or category" to "Cat 1"
Then I should not see "santjordi.h5p"
And I should see "santjordi_rose.h5p"
And I should not see "Dragon.h5p"
And I set the field "Choose course or category" to "Course 0"
And I should not see "santjordi.h5p"
And I should not see "santjordi_rose.h5p"
And I should see "Dragon.h5p"
Scenario: Teachers can view and navigate to contexts in the content bank based on their permissions
Given the following "users" exist:
| username | firstname | lastname |
| teacher | Joseba | Cilarte |
And the following "course enrolments" exist:
| user | course | role |
| teacher | C0 | editingteacher |
| teacher | C1 | editingteacher |
And I log out
And I am on the "C0" "Course" page logged in as "teacher"
And I turn editing mode on
And the following config values are set as admin:
| unaddableblocks | | theme_boost|
And I add the "Navigation" block if not present
And I expand "Site pages" node
When I click on "Content bank" "link"
And the "Choose course or category" select box should contain "Course 0"
And the "Choose course or category" select box should contain "Course 1"
And the "Choose course or category" select box should not contain "System"
And the "Choose course or category" select box should not contain "Cat 1"
And the "Choose course or category" select box should not contain "Cat 2"
And the "Choose course or category" select box should not contain "Course 2"
And I should see "Dragon.h5p"
And I should not see "princess.h5p"
And I should not see "santjordi.h5p"
And I should not see "santjordi_rose.h5p"
And I set the field "Choose course or category" to "Course 1"
Then I should not see "Dragon.h5p"
And I should see "princess.h5p"
And I should not see "santjordi.h5p"
And I should not see "santjordi_rose.h5p"
And the following "role assigns" exist:
| user | role | contextlevel | reference |
| teacher | manager | Category | CAT1 |
And I am on the "C0" "Course" page logged in as "teacher"
And I expand "Site pages" node
When I click on "Content bank" "link"
And the "Choose course or category" select box should contain "Course 0"
And the "Choose course or category" select box should contain "Course 1"
And the "Choose course or category" select box should contain "Cat 1"
And the "Choose course or category" select box should not contain "System"
And the "Choose course or category" select box should not contain "Cat 2"
And the "Choose course or category" select box should not contain "Course 2"
And I should see "Dragon.h5p"
And I set the field "Choose course or category" to "Cat 1"
And I should not see "Dragon.h5p"
And I should see "santjordi_rose.h5p"
@@ -0,0 +1,62 @@
@core @core_contentbank @core_h5p @contentbank_h5p @_file_upload @javascript
Feature: Search content in the content bank
In order to find easily content in the content bank
As an admin
I need to be able to search content in the content bank
Background:
Given I log in as "admin"
And I navigate to "H5P > Manage H5P content types" in site administration
And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "H5P content type" filemanager
And I click on "Upload H5P content types" "button" in the "#fitem_id_uploadlibraries" "css_element"
And the following "contentbank content" exist:
| contextlevel | reference | contenttype | user | contentname | filepath |
| System | | contenttype_h5p | admin | santjordi.h5p | /h5p/tests/fixtures/filltheblanks.h5p |
| System | | contenttype_h5p | admin | santjordi_rose.h5p | /h5p/tests/fixtures/filltheblanks.h5p |
| System | | contenttype_h5p | admin | SantJordi_book | /h5p/tests/fixtures/filltheblanks.h5p |
| System | | contenttype_h5p | admin | Dragon_santjordi.h5p | /h5p/tests/fixtures/filltheblanks.h5p |
| System | | contenttype_h5p | admin | princess.h5p | /h5p/tests/fixtures/filltheblanks.h5p |
| System | | contenttype_h5p | admin | mathsbook.h5p | /h5p/tests/fixtures/filltheblanks.h5p |
| System | | contenttype_h5p | admin | historybook.h5p | /h5p/tests/fixtures/filltheblanks.h5p |
| System | | contenttype_h5p | admin | santvicenc.h5p | /h5p/tests/fixtures/filltheblanks.h5p |
Scenario: Admins can search content in the content bank
Given I am on site homepage
And I turn editing mode on
And the following config values are set as admin:
| unaddableblocks | | theme_boost|
And I add the "Navigation" block if not present
And I expand "Site pages" node
And I click on "Content bank" "link"
And I should see "santjordi.h5p"
And "Clear search input" "button" should not be visible
And I should not see "items found"
When I set the field "Search" to "book"
# Waiting for the animation to show the button to finish.
And I wait "1" seconds
Then "Clear search input" "button" should be visible
And I should see "3 items found"
And I should see "SantJordi_book"
And I should see "mathsbook.h5p"
And I should see "historybook.h5p"
And I set the field "Search" to "sant"
And "Clear search input" "button" should be visible
And I should see "5 items found"
And I set the field "Search" to "santjordi"
And I should see "4 items found"
And I should see "santjordi.h5p"
And I should see "santjordi_rose.h5p"
And I should see "SantJordi_book"
And I should see "Dragon_santjordi.h5p"
And I click on "Clear search input" "button"
# Waiting for the animation to hide the button to finish.
And I wait "1" seconds
And "Clear search input" "button" should not be visible
And I should not see "items found"
And I set the field "Search" to ".h5p"
# Waiting for the animation to show the button to finish.
And I wait "1" seconds
And "Clear search input" "button" should be visible
And I should see "7 items found"
And I set the field "Search" to "friend"
And I should see "0 items found"
@@ -0,0 +1,59 @@
@core @core_contentbank @core_h5p @contentbank_h5p @_file_upload @javascript
Feature: Sort content in the content bank
In order to temporarily organise the content of the content bank
As an admin
I need to be able to sort the content bank in various ways
Background:
Given I log in as "admin"
And I navigate to "H5P > Manage H5P content types" in site administration
And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "H5P content type" filemanager
And I click on "Upload H5P content types" "button" in the "#fitem_id_uploadlibraries" "css_element"
And the following "users" exist:
| username | firstname | lastname | email |
| manager | Max | Manager | man@example.com |
And the following "role assigns" exist:
| user | role | contextlevel | reference |
| manager | manager | System | |
And the following "contentbank content" exist:
| contextlevel | reference | contenttype | user | contentname | filepath |
| System | | contenttype_h5p | admin | Dragon_santjordi.h5p | /h5p/tests/fixtures/filltheblanks.h5p |
| System | | contenttype_h5p | admin | mathsbook.h5p | /h5p/tests/fixtures/filltheblanks.h5p |
| System | | contenttype_h5p | manager | historybook.h5p | /h5p/tests/fixtures/filltheblanks.h5p |
| System | | contenttype_h5p | admin | santjordi.h5p | /h5p/tests/fixtures/filltheblanks.h5p |
| System | | contenttype_h5p | admin | santjordi_rose.h5p | /h5p/tests/fixtures/filltheblanks.h5p |
| System | | contenttype_h5p | admin | SantJordi_book | /h5p/tests/fixtures/filltheblanks.h5p |
Scenario: Admins can order content in the content bank
Given I am on site homepage
And I turn editing mode on
And the following config values are set as admin:
| unaddableblocks | | theme_boost|
And I add the "Navigation" block if not present
And I expand "Site pages" node
And I click on "Content bank" "link"
When I click on "Display content bank with file details" "button"
And I click on "Sort by Content name ascending" "button"
And "Dragon_santjordi.h5p" "text" should appear before "historybook.h5p" "text"
And "historybook.h5p" "text" should appear before "mathsbook.h5p" "text"
And "SantJordi_book" "text" should appear before "santjordi_rose.h5p" "text"
And I click on "Sort by Content name descending" "button"
And "historybook.h5p" "text" should appear before "Dragon_santjordi.h5p" "text"
And "mathsbook.h5p" "text" should appear before "historybook.h5p" "text"
Then "santjordi_rose.h5p" "text" should appear before "SantJordi_book" "text"
Scenario: Admins can order content depending on the author
Given I am on site homepage
And I turn editing mode on
And the following config values are set as admin:
| unaddableblocks | | theme_boost|
And I add the "Navigation" block if not present
And I expand "Site pages" node
And I click on "Content bank" "link"
When I click on "Display content bank with file details" "button"
Then I click on "Sort by Author ascending" "button"
And "Dragon_santjordi.h5p" "text" should appear before "historybook.h5p" "text"
And "santjordi_rose.h5p" "text" should appear before "historybook" "text"
And I click on "Sort by Author descending" "button"
And "historybook.h5p" "text" should appear before "Dragon_santjordi.h5p" "text"
And "historybook.h5p" "text" should appear before "santjordi_rose" "text"
@@ -0,0 +1,67 @@
@core @core_contentbank @core_h5p @contentbank_h5p @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 "user private files" exist:
| user | filepath |
| admin | h5p/tests/fixtures/filltheblanks.h5p |
| admin | h5p/tests/fixtures/greeting-card.h5p |
And I log in as "admin"
And I am on site homepage
And I turn editing mode on
And the following config values are set as admin:
| unaddableblocks | | theme_boost|
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.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 expand "Site pages" node
And I click on "Content bank" "link"
When I click on "Display content bank with file details" "button"
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 "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 am on the "My private files" page
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"
+153
View File
@@ -0,0 +1,153 @@
@core @core_contentbank @core_h5p @contentbank_h5p @_file_upload @javascript
Feature: Make content public or unlisted
In order to make content public or unlisted
As a user
I need to be able to access the edition options
Background:
Given I log in as "admin"
And I am on site homepage
And I turn editing mode on
And the following config values are set as admin:
| unaddableblocks | | theme_boost|
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 navigate to "H5P > Manage H5P content types" in site administration
And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "H5P content type" filemanager
And I click on "Upload H5P content types" "button" in the "#fitem_id_uploadlibraries" "css_element"
Scenario: Users can make their content public or unlisted
Given the following "contentbank content" exist:
| contextlevel | reference | contenttype | user | contentname | filepath | visibility |
| System | | contenttype_h5p | admin | filltheblanks.h5p | /h5p/tests/fixtures/filltheblanks.h5p | 1 |
And I click on "Site pages" "list_item" in the "Navigation" "block"
And I click on "Content bank" "link" in the "Navigation" "block"
And I click on "filltheblanks.h5p" "link"
And I wait until the page is ready
And "filltheblanks.h5p (Unlisted)" "heading" should not exist
And I click on "More" "button"
And I should see "Make unlisted"
And I click on "Make unlisted" "link"
And I wait until the page is ready
Then "filltheblanks.h5p (Unlisted)" "heading" should exist
And I click on "More" "button"
And I should see "Make public"
Scenario: Unlisted content cannot be seen by other users
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
| teacher2 | Teacher | 2 | teacher2@example.com |
And the following "courses" exist:
| fullname | shortname |
| Course 1 | C1 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| teacher2 | C1 | editingteacher |
And the following "contentbank content" exist:
| contextlevel | reference | contenttype | user | contentname | filepath | visibility |
| Course | C1 | contenttype_h5p | teacher1 | filltheblanks.h5p | /h5p/tests/fixtures/filltheblanks.h5p | 2 |
And I log out
And I log in as "teacher1"
And I am on "Course 1" course homepage
And I click on "Site pages" "list_item" in the "Navigation" "block"
And I click on "Content bank" "link" in the "Navigation" "block"
Then I should see "filltheblanks.h5p (Unlisted)"
And I log out
And I log in as "teacher2"
And I am on "Course 1" course homepage
And I click on "Site pages" "list_item" in the "Navigation" "block"
And I click on "Content bank" "link" in the "Navigation" "block"
Then I should not see "filltheblanks.h5p"
Scenario: Unlisted content is not found through search by other users
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
| teacher2 | Teacher | 2 | teacher2@example.com |
And the following "courses" exist:
| fullname | shortname |
| Course 1 | C1 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| teacher2 | C1 | editingteacher |
And the following "contentbank content" exist:
| contextlevel | reference | contenttype | user | contentname | filepath | visibility |
| Course | C1 | contenttype_h5p | teacher1 | filltheblanks.h5p | /h5p/tests/fixtures/filltheblanks.h5p | 2 |
And I log out
And I log in as "teacher1"
And I am on "Course 1" course homepage
And I click on "Site pages" "list_item" in the "Navigation" "block"
And I click on "Content bank" "link" in the "Navigation" "block"
And I set the field "Search" to "filltheblanks.h5p"
And I should see "filltheblanks.h5p"
And I log out
And I log in as "teacher2"
And I am on "Course 1" course homepage
And I click on "Site pages" "list_item" in the "Navigation" "block"
And I click on "Content bank" "link" in the "Navigation" "block"
When I set the field "Search" to "filltheblanks.h5p"
Then I should not see "filltheblanks.h5p"
Scenario: Managers can see other users' unlisted content
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
| manager1 | Manager | 1 | manager1@example.com |
And the following "courses" exist:
| fullname | shortname |
| Course 1 | C1 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| manager1 | C1 | manager |
And the following "contentbank content" exist:
| contextlevel | reference | contenttype | user | contentname | filepath | visibility |
| Course | C1 | contenttype_h5p | teacher1 | filltheblanks.h5p | /h5p/tests/fixtures/filltheblanks.h5p | 2 |
And I log out
And I log in as "manager1"
And I am on "Course 1" course homepage
And 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 (Unlisted)"
And I set the field "Search" to "filltheblanks.h5p"
And I should see "filltheblanks.h5p (Unlisted)"
@_file_upload
Scenario: Default content visibility can be set to unlisted
Given the following "courses" exist:
| fullname | shortname |
| Course 1 | C1 |
And I set the following administration settings values:
| Default content visibility | 2 |
And I am on "Course 1" course homepage
And I click on "Site pages" "list_item" in the "Navigation" "block"
And I click on "Content bank" "link" in the "Navigation" "block"
And I click on "Upload" "link"
And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "Upload content" filemanager
And I click on "Save changes" "button"
Then "filltheblanks.h5p (Unlisted)" "heading" should exist
@_file_upload
Scenario: User preference concerning content visibility overrides site-wide default content visibility
Given the following "courses" exist:
| fullname | shortname |
| Course 1 | C1 |
And I set the following administration settings values:
| Default content visibility | 2 |
And the following "user preferences" exist:
| user | preference | value |
| admin | core_contentbank_visibility | 1 |
And I am on "Course 1" course homepage
And I click on "Site pages" "list_item" in the "Navigation" "block"
And I click on "Content bank" "link" in the "Navigation" "block"
And I click on "Upload" "link"
And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "Upload content" filemanager
And I click on "Save changes" "button"
Then "filltheblanks.h5p" "heading" should exist
And "filltheblanks.h5p (Unlisted)" "heading" should not exist
+424
View File
@@ -0,0 +1,424 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Test for content bank contenttype class.
*
* @package core_contentbank
* @category test
* @copyright 2020 Amaia Anabitarte <amaia@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_contentbank;
use stdClass;
use context_system;
use contenttype_testable\contenttype as contenttype;
/**
* Test for content bank contenttype class.
*
* @package core_contentbank
* @category test
* @copyright 2020 Amaia Anabitarte <amaia@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @coversDefaultClass \core_contentbank\content
*
*/
class content_test extends \advanced_testcase {
/**
* Setup to ensure that fixtures are loaded.
*/
public static function setupBeforeClass(): void {
global $CFG;
require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_contenttype.php');
require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_content.php');
}
/**
* Tests for behaviour of get_name().
*
* @covers ::get_name
*/
public function test_get_name(): void {
$this->resetAfterTest();
// Create content.
$record = new stdClass();
$record->name = 'Test content';
$record->configdata = '';
$contenttype = new contenttype(context_system::instance());
$content = $contenttype->create_content($record);
$this->assertEquals($record->name, $content->get_name());
}
/**
* Data provider for test_set_name.
*
* @return array
*/
public function set_name_provider() {
return [
'Standard name' => ['New name', 'New name'],
'Name with digits' => ['Today is 17/04/2017', 'Today is 17/04/2017'],
'Name with symbols' => ['Follow us: @moodle', 'Follow us: @moodle'],
'Name with tags' => ['This is <b>bold</b>', 'This is bold'],
'Long name' => [str_repeat('a', 100), str_repeat('a', 100)],
'Too long name' => [str_repeat('a', 300), str_repeat('a', 255)],
'Empty name' => ['', 'Old name'],
'Blanks only' => [' ', 'Old name'],
];
}
/**
* Tests for 'set_name' behaviour.
*
* @dataProvider set_name_provider
* @param string $newname The name to set
* @param string $expected The name result
*
* @covers ::set_name
*/
public function test_set_name(string $newname, string $expected): void {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
$oldname = "Old name";
$context = context_system::instance();
// Create content.
$record = new stdClass();
$record->name = $oldname;
$contenttype = new contenttype($context);
$content = $contenttype->create_content($record);
$this->assertEquals($oldname, $content->get_name());
$content->set_name($newname);
$this->assertEquals($expected, $content->get_name());
$record = $DB->get_record('contentbank_content', ['id' => $content->get_id()]);
$this->assertEquals($expected, $record->name);
}
/**
* Tests for behaviour of get_content_type().
*
* @covers ::get_content_type
*/
public function test_get_content_type(): void {
$this->resetAfterTest();
// Create content.
$record = new stdClass();
$record->name = 'Test content';
$record->configdata = '';
$contenttype = new contenttype(context_system::instance());
$content = $contenttype->create_content($record);
$this->assertEquals('contenttype_testable', $content->get_content_type());
}
/**
* Tests for 'configdata' behaviour.
*
* @covers ::set_configdata
*/
public function test_configdata_changes(): void {
$this->resetAfterTest();
$configdata = "{img: 'icon.svg'}";
// Create content.
$record = new stdClass();
$record->configdata = $configdata;
$contenttype = new contenttype(context_system::instance());
$content = $contenttype->create_content($record);
$this->assertEquals($configdata, $content->get_configdata());
$configdata = "{alt: 'Name'}";
$content->set_configdata($configdata);
$this->assertEquals($configdata, $content->get_configdata());
}
/**
* Tests for 'set_contextid' behaviour.
*
* @covers ::set_contextid
*/
public function test_set_contextid(): void {
$this->resetAfterTest();
$this->setAdminUser();
$context = context_system::instance();
$course = $this->getDataGenerator()->create_course();
$newcontext = \context_course::instance($course->id);
// 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);
$content = reset($contents);
$oldcontextid = $content->get_contextid();
$file = $content->get_file();
$this->assertEquals($oldcontextid, $file->get_contextid());
$this->assertEquals($context->id, $oldcontextid);
$this->assertNotEquals($newcontext->id, $oldcontextid);
$content->set_contextid($newcontext->id);
$file = $content->get_file();
$this->assertEquals($newcontext->id, $content->get_contextid());
$this->assertEquals($newcontext->id, $file->get_contextid());
}
/**
* Tests for set_visibility behaviour
*
* @covers ::set_visibility
*/
public function test_set_visibility(): void {
$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.
*
* @covers ::import_file
*/
public function test_import_file_replace(): void {
global $USER;
$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);
$content = reset($contents);
$originalfile = $content->get_file();
// Create a dummy file.
$filerecord = array(
'contextid' => $context->id,
'component' => 'contentbank',
'filearea' => 'draft',
'itemid' => $content->get_id(),
'filepath' => '/',
'filename' => 'example.txt'
);
$fs = get_file_storage();
$file = $fs->create_file_from_string($filerecord, 'Dummy content ');
$importedfile = $content->import_file($file);
$this->assertEquals($originalfile->get_filename(), $importedfile->get_filename());
$this->assertEquals($originalfile->get_filearea(), $importedfile->get_filearea());
$this->assertEquals($originalfile->get_filepath(), $importedfile->get_filepath());
$this->assertEquals($originalfile->get_mimetype(), $importedfile->get_mimetype());
$this->assertEquals($file->get_userid(), $importedfile->get_userid());
$this->assertEquals($file->get_contenthash(), $importedfile->get_contenthash());
}
/**
* Tests for 'import_file' behaviour when uploading a new file.
*
* @covers ::import_file
*/
public function test_import_file_upload(): void {
global $USER;
$this->resetAfterTest();
$this->setAdminUser();
$context = context_system::instance();
$type = new contenttype($context);
$record = (object)[
'name' => 'content name',
'usercreated' => $USER->id,
];
$content = $type->create_content($record);
// Create a dummy file.
$filerecord = array(
'contextid' => $context->id,
'component' => 'contentbank',
'filearea' => 'draft',
'itemid' => $content->get_id(),
'filepath' => '/',
'filename' => 'example.txt'
);
$fs = get_file_storage();
$file = $fs->create_file_from_string($filerecord, 'Dummy content ');
$importedfile = $content->import_file($file);
$this->assertEquals($file->get_filename(), $importedfile->get_filename());
$this->assertEquals($file->get_userid(), $importedfile->get_userid());
$this->assertEquals($file->get_mimetype(), $importedfile->get_mimetype());
$this->assertEquals($file->get_contenthash(), $importedfile->get_contenthash());
$this->assertEquals('public', $importedfile->get_filearea());
$this->assertEquals('/', $importedfile->get_filepath());
$contentfile = $content->get_file($file);
$this->assertEquals($importedfile->get_id(), $contentfile->get_id());
}
/**
* Tests for 'get_content_type_instance'
*
* @covers ::get_content_type_instance
*/
public function test_get_content_type_instance(): void {
global $USER;
$this->resetAfterTest();
$this->setAdminUser();
$context = context_system::instance();
$type = new contenttype($context);
$record = (object)[
'name' => 'content name',
'usercreated' => $USER->id,
];
$content = $type->create_content($record);
$contenttype = $content->get_content_type_instance();
$this->assertInstanceOf(get_class($type), $contenttype);
}
/**
* Tests for 'is_view_allowed'.
*
* @covers ::is_view_allowed
*/
public function test_is_view_allowed(): void {
$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.
*
* @covers ::get_uses
*/
public function test_get_uses(): void {
$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());
}
}
+672
View File
@@ -0,0 +1,672 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Test for extensions manager.
*
* @package core_contentbank
* @category test
* @copyright 2020 Amaia Anabitarte <amaia@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_contentbank;
defined('MOODLE_INTERNAL') || die();
use advanced_testcase;
use context_block;
use context_course;
use context_coursecat;
use context_module;
use context_system;
use context_user;
use Exception;
global $CFG;
require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_contenttype.php');
require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_content.php');
/**
* Test for extensions manager.
*
* @package core_contentbank
* @category test
* @copyright 2020 Amaia Anabitarte <amaia@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @coversDefaultClass \core_contentbank\contentbank
*/
class contentbank_test extends advanced_testcase {
/**
* Setup to ensure that fixtures are loaded.
*/
public static function setupBeforeClass(): void {
global $CFG;
require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_contenttype.php');
}
/**
* Data provider for test_get_extension_supporter.
*
* @return array
*/
public function get_extension_provider() {
return [
'H5P file' => ['something.h5p', '.h5p'],
'PDF file' => ['something.pdf', '.pdf']
];
}
/**
* Tests for get_extension() function.
*
* @dataProvider get_extension_provider
* @param string $filename The filename given
* @param string $expected The extension of the file
*
* @covers ::get_extension
*/
public function test_get_extension(string $filename, string $expected): void {
$this->resetAfterTest();
$cb = new contentbank();
$extension = $cb->get_extension($filename);
$this->assertEquals($expected, $extension);
}
/**
* Data provider for test_load_context_supported_extensions.
*
* @return array
*/
public function get_extension_supporters_provider() {
return [
'H5P first' => [['.h5p' => ['h5p', 'testable']], '.h5p', 'h5p'],
'Testable first (but upload not implemented)' => [['.h5p' => ['testable', 'h5p']], '.h5p', 'h5p'],
];
}
/**
* Tests for get_extension_supporter() function with admin permissions.
*
* @dataProvider get_extension_supporters_provider
* @param array $supporters The content type plugin supporters for each extension
* @param string $extension The extension of the file given
* @param string $expected The supporter contenttype of the file
*
* @covers ::load_context_supported_extensions
*/
public function test_get_extension_supporter_for_admins(array $supporters, string $extension, string $expected): void {
$this->resetAfterTest();
$cb = new contentbank();
$systemcontext = context_system::instance();
// All contexts allowed for admins.
$this->setAdminUser();
$contextsupporters = $cb->load_context_supported_extensions($systemcontext);
$this->assertArrayHasKey($extension, $contextsupporters);
$this->assertEquals($expected, $contextsupporters[$extension]);
}
/**
* Tests for get_extension_supporter() function with user default permissions.
*
* @dataProvider get_extension_supporters_provider
* @param array $supporters The content type plugin supporters for each extension
* @param string $extension The extension of the file given
* @param string $expected The supporter contenttype of the file
*
* @covers ::load_context_supported_extensions
*/
public function test_get_extension_supporter_for_users(array $supporters, string $extension, string $expected): void {
$this->resetAfterTest();
$cb = new contentbank();
$systemcontext = context_system::instance();
// Set a user with no permissions.
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
// Users with no capabilities can't upload content.
$contextsupporters = $cb->load_context_supported_extensions($systemcontext);
$this->assertEquals([], $contextsupporters);
}
/**
* Tests for get_extension_supporter() function with teacher defaul permissions.
*
* @dataProvider get_extension_supporters_provider
* @param array $supporters The content type plugin supporters for each extension
* @param string $extension The extension of the file given
* @param string $expected The supporter contenttype of the file
*
* @covers ::load_context_supported_extensions
*/
public function test_get_extension_supporter_for_teachers(array $supporters, string $extension, string $expected): void {
$this->resetAfterTest();
$cb = new contentbank();
$course = $this->getDataGenerator()->create_course();
$teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
$this->setUser($teacher);
$coursecontext = context_course::instance($course->id);
// Teachers has permission in their context to upload supported by H5P content type.
$contextsupporters = $cb->load_context_supported_extensions($coursecontext);
$this->assertArrayHasKey($extension, $contextsupporters);
$this->assertEquals($expected, $contextsupporters[$extension]);
}
/**
* Tests for get_extension_supporter() function.
*
* @dataProvider get_extension_supporters_provider
* @param array $supporters The content type plugin supporters for each extension
* @param string $extension The extension of the file given
* @param string $expected The supporter contenttype of the file
*
* @covers ::get_extension_supporter
*/
public function test_get_extension_supporter(array $supporters, string $extension, string $expected): void {
$this->resetAfterTest();
$cb = new contentbank();
$systemcontext = context_system::instance();
$this->setAdminUser();
$supporter = $cb->get_extension_supporter($extension, $systemcontext);
$this->assertEquals($expected, $supporter);
}
/**
* Test the behaviour of search_contents().
*
* @dataProvider search_contents_provider
* @param string $search String to search.
* @param string $where Context where to search.
* @param int $expectedresult Expected result.
* @param array $contexts List of contexts where to create content.
*/
public function test_search_contents(?string $search, string $where, int $expectedresult, array $contexts = [],
array $contenttypes = null): void {
global $DB, $CFG;
$this->resetAfterTest();
$this->setAdminUser();
// Create users.
$managerroleid = $DB->get_field('role', 'id', ['shortname' => 'manager']);
$manager = $this->getDataGenerator()->create_user();
$this->getDataGenerator()->role_assign($managerroleid, $manager->id);
// Create a category and a course.
$coursecat = $this->getDataGenerator()->create_category();
$course = $this->getDataGenerator()->create_course();
$existingcontexts = [];
$existingcontexts['system'] = \context_system::instance();
$existingcontexts['category'] = \context_coursecat::instance($coursecat->id);
$existingcontexts['course'] = \context_course::instance($course->id);
if (empty($where)) {
$contextid = 0;
} else {
$contextid = $existingcontexts[$where]->id;
}
// Add some content to the content bank.
$filepath = $CFG->dirroot . '/h5p/tests/fixtures/filltheblanks.h5p';
$generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank');
foreach ($contexts as $context) {
$contextinstance = $existingcontexts[$context];
$records = $generator->generate_contentbank_data('contenttype_h5p', 3,
$manager->id, $contextinstance, false, $filepath);
}
// Search for some content.
$cb = new contentbank();
$contents = $cb->search_contents($search, $contextid, $contenttypes);
$this->assertCount($expectedresult, $contents);
if (!empty($contents) && !empty($search)) {
foreach ($contents as $content) {
$this->assertStringContainsString($search, $content->get_name());
}
}
}
/**
* Data provider for test_search_contents().
*
* @return array
*/
public function search_contents_provider(): array {
return [
'Search all content in all contexts' => [
null,
'',
9,
['system', 'category', 'course']
],
'Search in all contexts for existing string in all contents' => [
'content',
'',
9,
['system', 'category', 'course']
],
'Search in all contexts for unexisting string in all contents' => [
'chocolate',
'',
0,
['system', 'category', 'course']
],
'Search in all contexts for existing string in some contents' => [
'1',
'',
3,
['system', 'category', 'course']
],
'Search in all contexts for existing string in some contents (create only 1 context)' => [
'1',
'',
1,
['system']
],
'Search in system context for existing string in all contents' => [
'content',
'system',
3,
['system', 'category', 'course']
],
'Search in category context for unexisting string in all contents' => [
'chocolate',
'category',
0,
['system', 'category', 'course']
],
'Search in course context for existing string in some contents' => [
'1',
'course',
1,
['system', 'category', 'course']
],
'Search in system context' => [
null,
'system',
3,
['system', 'category', 'course']
],
'Search in course context with existing content' => [
null,
'course',
3,
['system', 'category', 'course']
],
'Search in course context without existing content' => [
null,
'course',
0,
['system', 'category']
],
'Search in an empty contentbank' => [
null,
'',
0,
[]
],
'Search in a context in an empty contentbank' => [
null,
'system',
0,
[]
],
'Search for a string in an empty contentbank' => [
'content',
'',
0,
[]
],
'Search with unexisting content-type' => [
null,
'course',
0,
['system', 'category', 'course'],
['contenttype_unexisting'],
],
];
}
/**
* Test create_content_from_file function.
*
* @covers ::create_content_from_file
*/
public function test_create_content_from_file(): void {
global $USER, $CFG;
$this->resetAfterTest();
$this->setAdminUser();
$systemcontext = \context_system::instance();
$name = 'greeting-card.h5p';
// Create a dummy H5P file.
$dummyh5p = array(
'contextid' => $systemcontext->id,
'component' => 'contentbank',
'filearea' => 'public',
'itemid' => 1,
'filepath' => '/',
'filename' => $name,
'userid' => $USER->id
);
$path = $CFG->dirroot . '/h5p/tests/fixtures/' . $name;
$dummyh5pfile = \core_h5p\helper::create_fake_stored_file_from_path($path);
$cb = new contentbank();
$content = $cb->create_content_from_file($systemcontext, $USER->id, $dummyh5pfile);
$this->assertEquals('contenttype_h5p', $content->get_content_type());
$this->assertInstanceOf('\\contenttype_h5p\\content', $content);
$this->assertEquals($name, $content->get_name());
}
/**
* Test the behaviour of delete_contents().
*
* @covers ::delete_contents
*/
public function test_delete_contents(): void {
global $DB;
$this->resetAfterTest();
$cb = new \core_contentbank\contentbank();
// Create a category and two courses.
$systemcontext = context_system::instance();
$coursecat = $this->getDataGenerator()->create_category();
$coursecatcontext = context_coursecat::instance($coursecat->id);
$course1 = $this->getDataGenerator()->create_course();
$course1context = context_course::instance($course1->id);
$course2 = $this->getDataGenerator()->create_course();
$course2context = context_course::instance($course2->id);
// Add some content to the content bank.
$generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank');
$systemcontent = $generator->generate_contentbank_data(null, 3, 0, $systemcontext);
$categorycontent = $generator->generate_contentbank_data(null, 3, 0, $coursecatcontext);
$course1content = $generator->generate_contentbank_data(null, 3, 0, $course1context);
$course2content = $generator->generate_contentbank_data(null, 3, 0, $course2context);
// Check the content has been created as expected.
$this->assertEquals(12, $DB->count_records('contentbank_content'));
// Check the system content is deleted as expected and the rest of the content is not.
$this->assertTrue($cb->delete_contents($systemcontext));
$this->assertEquals(0, $DB->count_records('contentbank_content', ['contextid' => $systemcontext->id]));
// And the rest of the context content exists.
$this->assertEquals(9, $DB->count_records('contentbank_content'));
// Check the course category content is deleted as expected and the rest of the content is not.
$this->assertTrue($cb->delete_contents($coursecatcontext));
$this->assertEquals(0, $DB->count_records('contentbank_content', ['contextid' => $coursecatcontext->id]));
// And the rest of the context content exists.
$this->assertEquals(6, $DB->count_records('contentbank_content'));
// Check the course content is deleted as expected and the rest of the content is not.
$this->assertTrue($cb->delete_contents($course1context));
$this->assertEquals(0, $DB->count_records('contentbank_content', ['contextid' => $course1context->id]));
// And the rest of the context content exists.
$this->assertEquals(3, $DB->count_records('contentbank_content'));
}
/**
* Test the behaviour of delete_contents() for empty content bank.
*
* @covers ::delete_contents
*/
public function test_delete_contents_for_empty_contentbank(): void {
$this->resetAfterTest();
$cb = new \core_contentbank\contentbank();
// Create a category and two courses.
$systemcontext = \context_system::instance();
$coursecat = $this->getDataGenerator()->create_category();
$coursecatcontext = \context_coursecat::instance($coursecat->id);
$course = $this->getDataGenerator()->create_course();
$coursecontext = \context_course::instance($course->id);
// Check there's no error when trying to delete content from an empty content bank.
$this->assertTrue($cb->delete_contents($systemcontext));
$this->assertTrue($cb->delete_contents($coursecatcontext));
$this->assertTrue($cb->delete_contents($coursecontext));
}
/**
* Test the behaviour of move_contents().
*
* @covers ::move_contents
*/
public function test_move_contents(): void {
global $DB;
$this->resetAfterTest();
$cb = new \core_contentbank\contentbank();
// Create a category and two courses.
$course1 = $this->getDataGenerator()->create_course();
$course1context = context_course::instance($course1->id);
$course2 = $this->getDataGenerator()->create_course();
$course2context = context_course::instance($course2->id);
// Add some content to the content bank.
$generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank');
$course1content = $generator->generate_contentbank_data(null, 3, 0, $course1context);
$course2content = $generator->generate_contentbank_data(null, 3, 0, $course2context);
// Check the content has been created as expected.
$this->assertEquals(6, $DB->count_records('contentbank_content'));
$this->assertEquals(3, $DB->count_records('contentbank_content', ['contextid' => $course1context->id]));
// Check the content is moved to another context as expected and the rest of the content is not.
$this->assertTrue($cb->move_contents($course1context, $course2context));
$this->assertEquals(6, $DB->count_records('contentbank_content'));
$this->assertEquals(0, $DB->count_records('contentbank_content', ['contextid' => $course1context->id]));
$this->assertEquals(6, $DB->count_records('contentbank_content', ['contextid' => $course2context->id]));
}
/**
* Test the behaviour of move_contents() for empty content bank.
*
* @covers ::move_contents
*/
public function test_move_contents_for_empty_contentbank(): void {
$this->resetAfterTest();
$cb = new \core_contentbank\contentbank();
// Create a category and two courses.
$systemcontext = \context_system::instance();
$course = $this->getDataGenerator()->create_course();
$coursecontext = \context_course::instance($course->id);
// Check there's no error when trying to move content context from an empty content bank.
$this->assertTrue($cb->delete_contents($systemcontext, $coursecontext));
}
/**
* Data provider for get_contenttypes_with_capability_feature.
*
* @return array
*/
public function get_contenttypes_with_capability_feature_provider(): array {
return [
'no-contenttypes_enabled' => [
'contenttypesenabled' => [],
'contenttypescanfeature' => [],
],
'contenttype_enabled_noeditable' => [
'contenttypesenabled' => ['testable'],
'contenttypescanfeature' => [],
],
'contenttype_enabled_editable' => [
'contenttypesenabled' => ['testable'],
'contenttypescanfeature' => ['testable'],
],
'no-contenttype_enabled_editable' => [
'contenttypesenabled' => [],
'contenttypescanfeature' => ['testable'],
],
];
}
/**
* Tests for get_contenttypes_with_capability_feature() function.
*
* @dataProvider get_contenttypes_with_capability_feature_provider
* @param array $contenttypesenabled Content types enabled.
* @param array $contenttypescanfeature Content types the user has the permission to use the feature.
*
* @covers ::get_contenttypes_with_capability_feature
*/
public function test_get_contenttypes_with_capability_feature(array $contenttypesenabled, array $contenttypescanfeature): void {
$this->resetAfterTest();
$cb = new contentbank();
$plugins = [];
// Content types not enabled where the user has permission to use a feature.
if (empty($contenttypesenabled) && !empty($contenttypescanfeature)) {
$enabled = false;
// Mock core_plugin_manager class and the method get_plugins_of_type.
$pluginmanager = $this->getMockBuilder(\core_plugin_manager::class)
->disableOriginalConstructor()
->onlyMethods(['get_plugins_of_type'])
->getMock();
// Replace protected singletoninstance reference (core_plugin_manager property) with mock object.
$ref = new \ReflectionProperty(\core_plugin_manager::class, 'singletoninstance');
$ref->setValue(null, $pluginmanager);
// Return values of get_plugins_of_type method.
foreach ($contenttypescanfeature as $contenttypepluginname) {
$contenttypeplugin = new \stdClass();
$contenttypeplugin->name = $contenttypepluginname;
$contenttypeplugin->type = 'contenttype';
// Add the feature to the fake content type.
$classname = "\\contenttype_$contenttypepluginname\\contenttype";
$classname::$featurestotest = ['test2'];
$plugins[] = $contenttypeplugin;
}
// Set expectations and return values.
$pluginmanager->expects($this->once())
->method('get_plugins_of_type')
->with('contenttype')
->willReturn($plugins);
} else {
$enabled = true;
// Get access to private property enabledcontenttypes.
$rc = new \ReflectionClass(\core_contentbank\contentbank::class);
$rcp = $rc->getProperty('enabledcontenttypes');
foreach ($contenttypesenabled as $contenttypename) {
$plugins["\\contenttype_$contenttypename\\contenttype"] = $contenttypename;
// Add to the testable contenttype the feature to test.
if (in_array($contenttypename, $contenttypescanfeature)) {
$classname = "\\contenttype_$contenttypename\\contenttype";
$classname::$featurestotest = ['test2'];
}
}
// Set as enabled content types only those in the test.
$rcp->setValue($cb, $plugins);
}
$actual = $cb->get_contenttypes_with_capability_feature('test2', null, $enabled);
$this->assertEquals($contenttypescanfeature, array_values($actual));
}
/**
* Test the behaviour of get_content_from_id()
*
* @covers ::get_content_from_id
*/
public function test_get_content_from_id(): void {
$this->resetAfterTest();
$cb = new \core_contentbank\contentbank();
// Create a category and two courses.
$systemcontext = context_system::instance();
// Add some content to the content bank.
$generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank');
$contents = $generator->generate_contentbank_data(null, 3, 0, $systemcontext);
$content = reset($contents);
// Get the content instance form id.
$newinstance = $cb->get_content_from_id($content->get_id());
$this->assertEquals($content->get_id(), $newinstance->get_id());
// Now produce and exception with an innexistent id.
$this->expectException(Exception::class);
$cb->get_content_from_id(0);
}
/**
* Test the behaviour of is_context_allowed().
*
* @covers ::is_context_allowed
*/
public function test_is_context_allowed(): void {
$this->resetAfterTest();
$cb = new contentbank();
// System context.
$this->assertTrue($cb->is_context_allowed(context_system::instance()));
// User context.
$user = $this->getDataGenerator()->create_user();
$this->assertFalse($cb->is_context_allowed(context_user::instance($user->id)));
// Category context.
$category = $this->getDataGenerator()->create_category();
$this->assertTrue($cb->is_context_allowed(context_coursecat::instance($category->id)));
// Course context.
$course = $this->getDataGenerator()->create_course(['category' => $category->id]);
$coursecontext = context_course::instance($course->id);
$this->assertTrue($cb->is_context_allowed($coursecontext));
// Module context.
$module = $this->getDataGenerator()->create_module('page', ['course' => $course->id]);
$this->assertFalse($cb->is_context_allowed(context_module::instance($module->cmid)));
// Block context.
$block = $this->getDataGenerator()->create_block('online_users', ['parentcontextid' => $coursecontext->id]);
$this->assertFalse($cb->is_context_allowed(context_block::instance($block->id)));
}
}
+669
View File
@@ -0,0 +1,669 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_contentbank;
use stdClass;
use context_system;
use context_user;
use Exception;
use contenttype_testable\contenttype as contenttype;
/**
* Test for content bank contenttype class.
*
* @package core_contentbank
* @category test
* @copyright 2020 Amaia Anabitarte <amaia@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @coversDefaultClass \core_contentbank\contenttype
*
*/
class contenttype_test extends \advanced_testcase {
/** @var int Identifier for the manager role. */
protected $managerroleid;
/** @var stdClass Manager user. */
protected $manager1;
/** @var stdClass Manager user. */
protected $manager2;
/** @var stdClass User. */
protected $user;
/** @var array List of contents created (every user has a key with contents created by her). */
protected $contents = [];
/** @var contenttype The contenttype instance. */
protected $contenttype;
/**
* Setup to ensure that fixtures are loaded.
*/
public static function setupBeforeClass(): void {
global $CFG;
require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_contenttype.php');
require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_content.php');
}
/**
* Tests get_contenttype_name result.
*
* @covers ::get_contenttype_name
*/
public function test_get_contenttype_name(): void {
$this->resetAfterTest();
$systemcontext = \context_system::instance();
$testable = new contenttype($systemcontext);
$this->assertEquals('contenttype_testable', $testable->get_contenttype_name());
}
/**
* Tests get_plugin_name result.
*
* @covers ::get_plugin_name
*/
public function test_get_plugin_name(): void {
$this->resetAfterTest();
$systemcontext = \context_system::instance();
$testable = new contenttype($systemcontext);
$this->assertEquals('testable', $testable->get_plugin_name());
}
/**
* Tests get_icon result.
*
* @covers ::get_icon
*/
public function test_get_icon(): void {
global $CFG;
$this->resetAfterTest();
$systemcontext = \context_system::instance();
$testable = new contenttype($systemcontext);
$record = new stdClass();
$record->name = 'New content';
$content = $testable->create_content($record);
$this->assertEquals(
"{$CFG->wwwroot}/theme/image.php/boost/core/1/f/unknown",
$testable->get_icon($content),
);
}
/**
* Tests is_feature_supported behavior .
*
* @covers ::is_feature_supported
*/
public function test_is_feature_supported(): void {
$this->resetAfterTest();
$systemcontext = \context_system::instance();
$testable = new contenttype($systemcontext);
$this->assertTrue($testable->is_feature_supported(contenttype::CAN_TEST));
$this->assertFalse($testable->is_feature_supported(contenttype::CAN_UPLOAD));
}
/**
* Tests can_upload behavior with no implemented upload feature.
*
* @covers ::can_upload
*/
public function test_no_upload_feature_supported(): void {
$this->resetAfterTest();
$systemcontext = \context_system::instance();
$testable = new contenttype($systemcontext);
$this->setAdminUser();
$this->assertFalse($testable->is_feature_supported(contenttype::CAN_UPLOAD));
$this->assertFalse($testable->can_upload());
}
/**
* Test create_content() with empty data.
*
* @covers ::create_content
*/
public function test_create_empty_content(): void {
$this->resetAfterTest();
// Create empty content.
$record = new stdClass();
$contenttype = new contenttype(context_system::instance());
$content = $contenttype->create_content($record);
$this->assertEquals('contenttype_testable', $content->get_content_type());
$this->assertInstanceOf('\\contenttype_testable\\content', $content);
}
/**
* Tests for behaviour of create_content() with data.
*
* @covers ::create_content
*/
public function test_create_content(): void {
$this->resetAfterTest();
// Create content.
$record = new stdClass();
$record->name = 'Test content';
$record->configdata = '';
$record->contenttype = '';
$contenttype = new contenttype(context_system::instance());
$content = $contenttype->create_content($record);
$this->assertEquals('contenttype_testable', $content->get_content_type());
$this->assertInstanceOf('\\contenttype_testable\\content', $content);
}
/**
* Tests for behaviour of upload_content() with a file and a record.
*
* @dataProvider upload_content_provider
* @param bool $userecord if a predefined record has to be used.
*
* @covers ::upload_content
*/
public function test_upload_content(bool $userecord): void {
global $USER;
$this->resetAfterTest();
$this->setAdminUser();
$dummy = [
'contextid' => context_user::instance($USER->id)->id,
'component' => 'user',
'filearea' => 'draft',
'itemid' => 1,
'filepath' => '/',
'filename' => 'file.h5p',
'userid' => $USER->id,
];
$fs = get_file_storage();
$dummyfile = $fs->create_file_from_string($dummy, 'Dummy content');
// Create content.
if ($userecord) {
$record = new stdClass();
$record->name = 'Test content';
$record->configdata = '';
$record->contenttype = '';
$checkname = $record->name;
} else {
$record = null;
$checkname = $dummyfile->get_filename();
}
$contenttype = new contenttype(context_system::instance());
$content = $contenttype->upload_content($dummyfile, $record);
$this->assertEquals('contenttype_testable', $content->get_content_type());
$this->assertEquals($checkname, $content->get_name());
$this->assertInstanceOf('\\contenttype_testable\\content', $content);
$file = $content->get_file();
$this->assertEquals($dummyfile->get_filename(), $file->get_filename());
$this->assertEquals($dummyfile->get_userid(), $file->get_userid());
$this->assertEquals($dummyfile->get_mimetype(), $file->get_mimetype());
$this->assertEquals($dummyfile->get_contenthash(), $file->get_contenthash());
$this->assertEquals('contentbank', $file->get_component());
$this->assertEquals('public', $file->get_filearea());
$this->assertEquals('/', $file->get_filepath());
}
/**
* Data provider for test_rename_content.
*
* @return array
*/
public function upload_content_provider() {
return [
'With record' => [true],
'Without record' => [false],
];
}
/**
* Tests for behaviour of upload_content() with a file wrong file.
*
* @covers ::upload_content
*/
public function test_upload_content_exception(): void {
global $USER, $DB;
$this->resetAfterTest();
$this->setAdminUser();
// The testing contenttype thows exception if filename is "error.*".
$dummy = [
'contextid' => context_user::instance($USER->id)->id,
'component' => 'user',
'filearea' => 'draft',
'itemid' => 1,
'filepath' => '/',
'filename' => 'error.txt',
'userid' => $USER->id,
];
$fs = get_file_storage();
$dummyfile = $fs->create_file_from_string($dummy, 'Dummy content');
$contenttype = new contenttype(context_system::instance());
$cbcontents = $DB->count_records('contentbank_content');
// We need to capture the exception to check no content is created.
try {
$content = $contenttype->upload_content($dummyfile);
$this->assertTrue(false);
} catch (Exception $e) {
$this->assertTrue(true);
}
$this->assertEquals($cbcontents, $DB->count_records('contentbank_content'));
$this->assertEquals(1, $DB->count_records('files', ['contenthash' => $dummyfile->get_contenthash()]));
}
/**
* Tests for behaviour of replace_content() using a dummy file.
*
* @covers ::replace_content
*/
public function test_replace_content(): void {
global $USER;
$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);
$content = reset($contents);
$dummy = [
'contextid' => context_user::instance($USER->id)->id,
'component' => 'user',
'filearea' => 'draft',
'itemid' => 1,
'filepath' => '/',
'filename' => 'file.h5p',
'userid' => $USER->id,
];
$fs = get_file_storage();
$dummyfile = $fs->create_file_from_string($dummy, 'Dummy content');
$contenttype = new contenttype(context_system::instance());
$content = $contenttype->replace_content($dummyfile, $content);
$this->assertEquals('contenttype_testable', $content->get_content_type());
$this->assertInstanceOf('\\contenttype_testable\\content', $content);
$file = $content->get_file();
$this->assertEquals($dummyfile->get_userid(), $file->get_userid());
$this->assertEquals($dummyfile->get_contenthash(), $file->get_contenthash());
$this->assertEquals('contentbank', $file->get_component());
$this->assertEquals('public', $file->get_filearea());
$this->assertEquals('/', $file->get_filepath());
}
/**
* Tests for behaviour of replace_content() using an error file.
*
* @covers ::replace_content
*/
public function test_replace_content_exception(): void {
global $USER;
$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);
$content = reset($contents);
$dummy = [
'contextid' => context_user::instance($USER->id)->id,
'component' => 'user',
'filearea' => 'draft',
'itemid' => 1,
'filepath' => '/',
'filename' => 'error.txt',
'userid' => $USER->id,
];
$fs = get_file_storage();
$dummyfile = $fs->create_file_from_string($dummy, 'Dummy content');
$contenttype = new contenttype(context_system::instance());
$this->expectException(Exception::class);
$content = $contenttype->replace_content($dummyfile, $content);
}
/**
* Test the behaviour of can_delete().
*/
public function test_can_delete(): void {
global $DB;
$this->resetAfterTest();
$this->contenttype_setup_scenario_data();
$managercontent = array_shift($this->contents[$this->manager1->id]);
$usercontent = array_shift($this->contents[$this->user->id]);
// Check the content has been created as expected.
$records = $DB->count_records('contentbank_content');
$this->assertEquals(4, $records);
// Check user can only delete records created by her.
$this->setUser($this->user);
$this->assertFalse($this->contenttype->can_delete($managercontent));
$this->assertTrue($this->contenttype->can_delete($usercontent));
// Check manager can delete records all the records created.
$this->setUser($this->manager1);
$this->assertTrue($this->contenttype->can_delete($managercontent));
$this->assertTrue($this->contenttype->can_delete($usercontent));
// Unassign capability to manager role and check not can only delete their own records.
unassign_capability('moodle/contentbank:deleteanycontent', $this->managerroleid);
$this->assertTrue($this->contenttype->can_delete($managercontent));
$this->assertFalse($this->contenttype->can_delete($usercontent));
$this->setUser($this->manager2);
$this->assertFalse($this->contenttype->can_delete($managercontent));
$this->assertFalse($this->contenttype->can_delete($usercontent));
}
/**
* Test the behaviour of delete_content().
*/
public function test_delete_content(): void {
global $DB;
$this->resetAfterTest();
$this->contenttype_setup_scenario_data();
// Check the content has been created as expected.
$this->assertEquals(4, $DB->count_records('contentbank_content'));
// Check the content is deleted as expected.
$this->setUser($this->manager1);
$content = array_shift($this->contents[$this->manager1->id]);
$deleted = $this->contenttype->delete_content($content);
$this->assertTrue($deleted);
$this->assertEquals(3, $DB->count_records('contentbank_content'));
}
/**
* Helper function to setup 3 users (manager1, manager2 and user) and 4 contents (3 created by manager1 and 1 by user).
*/
protected function contenttype_setup_scenario_data(string $contenttype = 'contenttype_testable'): void {
global $DB;
$systemcontext = context_system::instance();
// Create users.
$this->manager1 = $this->getDataGenerator()->create_user();
$this->manager2 = $this->getDataGenerator()->create_user();
$this->managerroleid = $DB->get_field('role', 'id', array('shortname' => 'manager'));
$this->getDataGenerator()->role_assign($this->managerroleid, $this->manager1->id);
$this->getDataGenerator()->role_assign($this->managerroleid, $this->manager2->id);
$editingteacherrolerid = $DB->get_field('role', 'id', ['shortname' => 'editingteacher']);
$this->user = $this->getDataGenerator()->create_user();
$this->getDataGenerator()->role_assign($editingteacherrolerid, $this->user->id);
// Add some content to the content bank.
$generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank');
$this->contents[$this->manager1->id] = $generator->generate_contentbank_data($contenttype, 3, $this->manager1->id);
$this->contents[$this->user->id] = $generator->generate_contentbank_data($contenttype, 1, $this->user->id);
$contenttypeclass = "\\$contenttype\\contenttype";
$this->contenttype = new $contenttypeclass($systemcontext);
}
/**
* Data provider for test_rename_content.
*
* @return array
*/
public function rename_content_provider() {
return [
'Standard name' => ['New name', 'New name', true],
'Name with digits' => ['Today is 17/04/2017', 'Today is 17/04/2017', true],
'Name with symbols' => ['Follow us: @moodle', 'Follow us: @moodle', true],
'Name with tags' => ['This is <b>bold</b>', 'This is bold', true],
'Long name' => [str_repeat('a', 100), str_repeat('a', 100), true],
'Too long name' => [str_repeat('a', 300), str_repeat('a', 255), true],
'Empty name' => ['', 'Test content ', false],
'Blanks only' => [' ', 'Test content ', false],
'Zero name' => ['0', '0', true],
];
}
/**
* Test the behaviour of rename_content().
*
* @dataProvider rename_content_provider
* @param string $newname The name to set
* @param string $expected The name result
* @param bool $result The bolean result expected when renaming
*
* @covers ::rename_content
*/
public function test_rename_content(string $newname, string $expected, bool $result): void {
global $DB;
$this->resetAfterTest();
// Create course and teacher user.
$course = $this->getDataGenerator()->create_course();
$teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
$coursecontext = \context_course::instance($course->id);
$contenttype = new contenttype($coursecontext);
// Add some content to the content bank as teacher.
$this->setUser($teacher);
$generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank');
$contents = $generator->generate_contentbank_data('contenttype_testable', 1, $teacher->id);
$content = array_shift($contents);
$oldname = $content->get_name();
// Check the content is renamed as expected by a user with permission.
$renamed = $contenttype->rename_content($content, $newname);
$this->assertEquals($result, $renamed);
$record = $DB->get_record('contentbank_content', ['id' => $content->get_id()]);
$this->assertEquals($expected, $record->name);
}
/**
* Test the behaviour of move_content().
*/
public function test_move_content(): void {
global $DB;
$this->resetAfterTest();
$systemcontext = context_system::instance();
$course = $this->getDataGenerator()->create_course();
$coursecontext = \context_course::instance($course->id);
// Add some content to the content bank.
$generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank');
$systemcontents = $generator->generate_contentbank_data('contenttype_testable', 3, 0, $systemcontext);
$generator->generate_contentbank_data('contenttype_testable', 3, 0, $coursecontext);
$systemcontent = reset($systemcontents);
// Check the content has been created as expected.
$this->assertEquals(6, $DB->count_records('contentbank_content'));
$this->assertEquals(3, $DB->count_records('contentbank_content', ['contextid' => $systemcontext->id]));
$this->assertEquals(3, $DB->count_records('contentbank_content', ['contextid' => $coursecontext->id]));
// Check the content files has been created as expected.
$this->assertEquals(12, $DB->count_records('files', ['component' => 'contentbank']));
$this->assertEquals(6, $DB->count_records('files', ['component' => 'contentbank', 'contextid' => $systemcontext->id]));
$this->assertEquals(6, $DB->count_records('files', ['component' => 'contentbank', 'contextid' => $coursecontext->id]));
// Check the content is moved as expected.
$contenttype = new contenttype($systemcontext);
$this->assertTrue($contenttype->move_content($systemcontent, $coursecontext));
$this->assertEquals(6, $DB->count_records('contentbank_content'));
$this->assertEquals(2, $DB->count_records('contentbank_content', ['contextid' => $systemcontext->id]));
$this->assertEquals(4, $DB->count_records('contentbank_content', ['contextid' => $coursecontext->id]));
// Check the content files were moved as expected.
$this->assertEquals(12, $DB->count_records('files', ['component' => 'contentbank']));
$this->assertEquals(4, $DB->count_records('files', ['component' => 'contentbank', 'contextid' => $systemcontext->id]));
$this->assertEquals(8, $DB->count_records('files', ['component' => 'contentbank', 'contextid' => $coursecontext->id]));
}
/**
* Test the behaviour of can_manage().
*
* @covers ::can_manage
*/
public function test_can_manage(): void {
global $DB, $USER;
$this->resetAfterTest();
$generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank');
// Create course and teacher user.
$teacherroleid = $DB->get_field('role', 'id', ['shortname' => 'editingteacher']);
$course = $this->getDataGenerator()->create_course();
$teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
$manager = $this->getDataGenerator()->create_and_enrol($course, 'manager');
$coursecontext = \context_course::instance($course->id);
$contenttype = new contenttype($coursecontext);
// Add some content to the content bank as admin.
$this->setAdminUser();
$contentsbyadmin = $generator->generate_contentbank_data('contenttype_testable', 1, $USER->id, $coursecontext);
$contentbyadmin = array_shift($contentsbyadmin);
// Add some content to the content bank as teacher.
$contentsbyteacher = $generator->generate_contentbank_data('contenttype_testable', 1, $teacher->id, $coursecontext);
$contentbyteacher = array_shift($contentsbyteacher);
// Check the content has been created as expected.
$records = $DB->count_records('contentbank_content');
$this->assertEquals(2, $records);
// Check manager can manage by default all the contents created.
$this->setUser($manager);
$this->assertTrue($contenttype->can_manage($contentbyteacher));
$this->assertTrue($contenttype->can_manage($contentbyadmin));
// Check teacher can only edit their own content.
$this->setUser($teacher);
$this->assertTrue($contenttype->can_manage($contentbyteacher));
$this->assertFalse($contenttype->can_manage($contentbyadmin));
// Unassign capability to teacher role and check they not can not edit any content.
unassign_capability('moodle/contentbank:manageowncontent', $teacherroleid);
$this->assertFalse($contenttype->can_manage($contentbyteacher));
$this->assertFalse($contenttype->can_manage($contentbyadmin));
}
/**
* Test the behaviour of can_download().
*
* @covers ::can_download
*/
public function test_can_download(): void {
global $DB;
$this->resetAfterTest();
$this->contenttype_setup_scenario_data('contenttype_h5p');
$managercontent = array_shift($this->contents[$this->manager1->id]);
$usercontent = array_shift($this->contents[$this->user->id]);
// Check the content has been created as expected.
$records = $DB->count_records('contentbank_content');
$this->assertEquals(4, $records);
// Check user can download content created by anybody.
$this->setUser($this->user);
$this->assertTrue($this->contenttype->can_download($usercontent));
$this->assertTrue($this->contenttype->can_download($managercontent));
// Check manager can download all the content too.
$this->setUser($this->manager1);
$this->assertTrue($this->contenttype->can_download($managercontent));
$this->assertTrue($this->contenttype->can_download($usercontent));
// Unassign capability to manager role and check she cannot download content anymore.
unassign_capability('moodle/contentbank:downloadcontent', $this->managerroleid);
$this->assertFalse($this->contenttype->can_download($managercontent));
$this->assertFalse($this->contenttype->can_download($usercontent));
}
/**
* Tests get_download_url result.
*
* @covers ::get_download_url
*/
public function test_get_download_url(): void {
global $CFG;
$this->resetAfterTest();
$this->setAdminUser();
$systemcontext = context_system::instance();
// Add some content to the content bank.
$filename = 'filltheblanks.h5p';
$filepath = $CFG->dirroot . '/h5p/tests/fixtures/' . $filename;
$generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank');
$contents = $generator->generate_contentbank_data('contenttype_testable', 1, 0, $systemcontext, true, $filepath);
$content = array_shift($contents);
// Check the URL is returned OK for a content with file.
$contenttype = new contenttype($systemcontext);
$url = $contenttype->get_download_url($content);
$this->assertNotEmpty($url);
$this->assertStringContainsString($filename, $url);
// Check the URL is empty when the content hasn't any file.
$record = new stdClass();
$content = $contenttype->create_content($record);
$url = $contenttype->get_download_url($content);
$this->assertEmpty($url);
}
/**
* Tests pluginfile result.
*
* @covers ::pluginfile
*/
public function test_pluginfile(): void {
$this->resetAfterTest();
$this->setAdminUser();
$systemcontext = context_system::instance();
$contenttype = new contenttype($systemcontext);
$this->assertIsCallable([$contenttype, 'pluginfile']);
}
}
+146
View File
@@ -0,0 +1,146 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_contentbank\external;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_contenttype.php');
require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_content.php');
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
use core_external\external_api;
/**
* Content bank's copy content external function tests.
*
* @package core_contentbank
* @copyright 2023 Daniel Neis Araujo
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @coversDefaultClass \core_contentbank\external\copy_content
*/
class copy_content_test extends \externallib_advanced_testcase {
/**
* Test the behaviour of copy_content() for users with permission.
*
* @covers ::execute
*/
public function test_copy_content_with_permission(): void {
global $CFG, $DB;
$this->resetAfterTest();
// Create users.
$roleid = $DB->get_field('role', 'id', ['shortname' => 'editingteacher']);
$teacher = $this->getDataGenerator()->create_user();
$this->getDataGenerator()->role_assign($roleid, $teacher->id);
$this->setUser($teacher);
// Add some content to the content bank as teacher.
$filename = 'filltheblanks.h5p';
$filepath = $CFG->dirroot . '/h5p/tests/fixtures/' . $filename;
$generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank');
$contents = $generator->generate_contentbank_data('contenttype_h5p', 1, $teacher->id, null, true, $filepath);
$content = array_shift($contents);
$oldname = $content->get_name();
$newname = 'New name';
// Call the WS and check the content is copied as expected.
$result = copy_content::execute($content->get_id(), $newname);
$result = external_api::clean_returnvalue(copy_content::execute_returns(), $result);
$this->assertNotEmpty($result['id']);
$record = $DB->get_record('contentbank_content', ['id' => $result['id']]);
$this->assertEquals($newname, $record->name);
$record = $DB->get_record('contentbank_content', ['id' => $content->get_id()]);
$this->assertEquals($oldname, $record->name);
// Call the WS using an unexisting contentid and check an error is thrown.
$this->expectException(\invalid_response_exception::class);
$result = copy_content::execute_returns($content->get_id() + 1, $oldname);
$result = external_api::clean_returnvalue(copy_content::execute_returns(), $result);
$this->assertNotEmpty($result['warnings']);
}
/**
* Test the behaviour of copy_content() for users with and without permission.
*
* @covers ::execute
*/
public function test_copy_content(): void {
global $CFG, $DB;
$this->resetAfterTest();
// Create users.
$course = $this->getDataGenerator()->create_course();
$teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
$teacher2 = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
$manager = $this->getDataGenerator()->create_and_enrol($course, 'manager');
$student = $this->getDataGenerator()->create_and_enrol($course, 'student');
// Add some content to the content bank as teacher.
$coursecontext = \context_course::instance($course->id);
$filename = 'filltheblanks.h5p';
$filepath = $CFG->dirroot . '/h5p/tests/fixtures/' . $filename;
$generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank');
$contents = $generator->generate_contentbank_data('contenttype_h5p', 1, $teacher->id, $coursecontext, true, $filepath);
$content = array_shift($contents);
$oldname = $content->get_name();
$newname = 'New name';
// Call the WS and check the teacher can copy his/her own content.
$this->setUser($teacher);
$result = copy_content::execute($content->get_id(), $newname);
$result = external_api::clean_returnvalue(copy_content::execute_returns(), $result);
$this->assertEmpty($result['warnings']);
$record = $DB->get_record('contentbank_content', ['id' => $result['id']]);
$this->assertEquals($newname, $record->name);
// Call the WS and check the content has not been copied by the student.
$this->setUser($student);
$result = copy_content::execute($content->get_id(), $newname);
$result = external_api::clean_returnvalue(copy_content::execute_returns(), $result);
$this->assertNotEmpty($result['warnings']);
$record = $DB->get_record('contentbank_content', ['id' => $content->get_id()]);
$this->assertEquals($oldname, $record->name);
$this->assertNotEquals($newname, $record->name);
// Call the WS an check the content with empty name is not copied by the teacher.
$this->setUser($teacher);
$result = copy_content::execute($content->get_id(), '');
$result = external_api::clean_returnvalue(copy_content::execute_returns(), $result);
$this->assertNotEmpty($result['warnings']);
// Call the WS and check a teacher cannot copy content from another teacher by default.
$this->setUser($teacher2);
$result = copy_content::execute($content->get_id(), 'New name 2');
$result = external_api::clean_returnvalue(copy_content::execute_returns(), $result);
$this->assertNotEmpty($result['warnings']);
// Call the WS and check a manager can copy content from a teacher by default.
$this->setUser($manager);
$result = copy_content::execute($content->get_id(), $newname);
$result = external_api::clean_returnvalue(copy_content::execute_returns(), $result);
$this->assertEmpty($result['warnings']);
$record = $DB->get_record('contentbank_content', ['id' => $result['id']]);
$this->assertEquals($newname, $record->name);
}
}
+112
View File
@@ -0,0 +1,112 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* External function test for delete_content.
*
* @package core_contentbank
* @category external
* @since Moodle 3.9
* @copyright 2020 Sara Arjona <sara@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_contentbank\external;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_content.php');
use dml_missing_record_exception;
use core_external\external_api;
use externallib_advanced_testcase;
/**
* External function test for delete_content.
*
* @package core_contentbank
* @copyright 2020 Sara Arjona <sara@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class delete_content_test extends externallib_advanced_testcase {
/**
* Test the behaviour of delete_content().
*/
public function test_delete_content(): void {
global $DB;
$this->resetAfterTest();
$records = [];
// Create users.
$user = $this->getDataGenerator()->create_user();
$roleid = $DB->get_field('role', 'id', array('shortname' => 'manager'));
$manager = $this->getDataGenerator()->create_user();
$this->getDataGenerator()->role_assign($roleid, $manager->id);
// Add some content to the content bank.
$generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank');
$records[$manager->id] = $generator->generate_contentbank_data('contenttype_testable', 4, $manager->id, null, false);
$records[$user->id] = $generator->generate_contentbank_data('contenttype_testable', 2, $user->id, null, false);
// Check the content has been created as expected.
$this->assertEquals(6, $DB->count_records('contentbank_content'));
// Check the content is deleted as expected by the user when the content has been created by herself.
$this->setUser($user);
$userrecord = array_shift($records[$user->id]);
$result = delete_content::execute([$userrecord->id]);
$result = external_api::clean_returnvalue(delete_content::execute_returns(), $result);
$this->assertTrue($result['result']);
$this->assertCount(0, $result['warnings']);
$this->assertEquals(5, $DB->count_records('contentbank_content'));
// Check the content is not deleted if the user hasn't created it and has only permission to delete her own content.
$userrecord = array_shift($records[$user->id]);
$managerrecord1 = array_shift($records[$manager->id]);
$result = delete_content::execute([$managerrecord1->id, $userrecord->id]);
$result = external_api::clean_returnvalue(delete_content::execute_returns(), $result);
$this->assertFalse($result['result']);
$this->assertCount(1, $result['warnings']);
$warning = array_shift($result['warnings']);
$this->assertEquals('nopermissiontodelete', $warning['warningcode']);
$this->assertEquals($managerrecord1->id, $warning['item']);
$this->assertEquals(4, $DB->count_records('contentbank_content'));
// Check the content is deleted as expected by the manager.
$this->setUser($manager);
$managerrecord2 = array_shift($records[$manager->id]);
$result = delete_content::execute([$managerrecord1->id, $managerrecord2->id]);
$result = external_api::clean_returnvalue(delete_content::execute_returns(), $result);
$this->assertTrue($result['result']);
$this->assertCount(0, $result['warnings']);
$this->assertEquals(2, $DB->count_records('contentbank_content'));
// Check an exception warning is returned if an unexisting contentid is deleted.
// Check also the other content is deleted (so the process continues after the exception is thrown).
$managerrecord3 = array_shift($records[$manager->id]);
$result = delete_content::execute([$managerrecord1->id, $managerrecord3->id]);
$result = external_api::clean_returnvalue(delete_content::execute_returns(), $result);
$this->assertFalse($result['result']);
$this->assertCount(1, $result['warnings']);
$warning = array_shift($result['warnings']);
$this->assertEquals('exception', $warning['warningcode']);
$this->assertEquals($managerrecord1->id, $warning['item']);
$this->assertEquals(1, $DB->count_records('contentbank_content'));
}
}
+140
View File
@@ -0,0 +1,140 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Core content bank external functions tests.
*
* @package core_contentbank
* @category external
* @copyright 2020 Sara Arjona <sara@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since Moodle 3.9
*/
namespace core_contentbank\external;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_contenttype.php');
require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_content.php');
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
use core_external\external_api;
/**
* Core content bank external functions tests.
*
* @package core_contentbank
* @copyright 2020 Sara Arjona <sara@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @coversDefaultClass \core_contentbank\external\rename_content
*/
class rename_content_test extends \externallib_advanced_testcase {
/**
* Data provider for test_rename_content.
*
* @return array
*/
public function rename_content_provider() {
return [
'Standard name' => ['New name', 'New name', true],
'Name with digits' => ['Today is 17/04/2017', 'Today is 17/04/2017', true],
'Name with symbols' => ['Follow us: @moodle', 'Follow us: @moodle', true],
'Name with tags' => ['This is <b>bold</b>', 'This is bold', true],
'Long name' => [str_repeat('a', 100), str_repeat('a', 100), true],
'Too long name' => [str_repeat('a', 300), str_repeat('a', 255), true],
'Empty name' => ['', 'Test content ', false],
'Blanks only' => [' ', 'Test content ', false],
'Zero name' => ['0', '0', true],
];
}
/**
* Test the behaviour of rename_content() for users with permission.
*
* @dataProvider rename_content_provider
* @param string $newname The name to set
* @param string $expectedname The name result
* @param bool $expectedresult The bolean result expected when renaming
*
* @covers ::execute
*/
public function test_rename_content_with_permission(string $newname, string $expectedname, bool $expectedresult): void {
global $DB;
$this->resetAfterTest();
// Create users.
$roleid = $DB->get_field('role', 'id', ['shortname' => 'editingteacher']);
$teacher = $this->getDataGenerator()->create_user();
$this->getDataGenerator()->role_assign($roleid, $teacher->id);
$this->setUser($teacher);
// Add some content to the content bank as teacher.
$generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank');
$contents = $generator->generate_contentbank_data('contenttype_testable', 1, $teacher->id);
$content = array_shift($contents);
$oldname = $content->get_name();
// Call the WS and check the content is renamed as expected.
$result = rename_content::execute($content->get_id(), $newname);
$result = external_api::clean_returnvalue(rename_content::execute_returns(), $result);
$this->assertEquals($expectedresult, $result['result']);
$record = $DB->get_record('contentbank_content', ['id' => $content->get_id()]);
$this->assertEquals($expectedname, $record->name);
// Call the WS using an unexisting contentid and check an error is thrown.
$this->expectException(\invalid_response_exception::class);
$result = rename_content::execute_returns($content->get_id() + 1, $oldname);
$result = external_api::clean_returnvalue(rename_content::execute_returns(), $result);
$this->assertFalse($result['result']);
}
/**
* Test the behaviour of rename_content() for users with permission.
*
* @covers ::execute
*/
public function test_rename_content_without_permission(): void {
global $DB;
$this->resetAfterTest();
// Create users.
$course = $this->getDataGenerator()->create_course();
$teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
$student = $this->getDataGenerator()->create_and_enrol($course, 'student');
// Add some content to the content bank as teacher.
$generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank');
$contents = $generator->generate_contentbank_data('contenttype_testable', 1, $teacher->id);
$content = array_shift($contents);
$oldname = $content->get_name();
$newname = 'New name';
// Call the WS and check the content has not been renamed by the student.
$this->setUser($student);
$result = rename_content::execute($content->get_id(), $newname);
$result = external_api::clean_returnvalue(rename_content::execute_returns(), $result);
$this->assertFalse($result['result']);
$record = $DB->get_record('contentbank_content', ['id' => $content->get_id()]);
$this->assertEquals($oldname, $record->name);
$this->assertNotEquals($newname, $record->name);
}
}
+56
View File
@@ -0,0 +1,56 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Testable content plugin class.
*
* @package core_contentbank
* @category test
* @copyright 2020 Amaia Anabitarte <amaia@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace contenttype_testable;
use file_exception;
use stored_file;
/**
* Testable content plugin class.
*
* @package core_contentbank
* @copyright 2020 Amaia Anabitarte <amaia@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class content extends \core_contentbank\content {
/**
* Import a file as a valid content.
*
* This method will thow an error if the filename is "error.*"
*
* @param stored_file $file File to store in the content file area.
* @return stored_file|null the stored content file or null if the file is discarted.
* @throws file_exception if the filename contains the word "error"
*/
public function import_file(stored_file $file): ?stored_file {
$filename = $file->get_filename();
if (strrpos($filename, 'error') !== false) {
throw new file_exception('yourerrorthanks', 'contenttype_test');
}
return parent::import_file($file);
}
}
+98
View File
@@ -0,0 +1,98 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace contenttype_testable;
/**
* Testable contenttype plugin class.
*
* @package core_contentbank
* @copyright 2020 Sara Arjona <sara@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class contenttype extends \core_contentbank\contenttype {
/** Feature for testing */
const CAN_TEST = 'test';
/** @var array Additional features for testing */
public static $featurestotest;
/**
* Return an array of implemented features by this plugin.
*
* @return array
*/
protected function get_implemented_features(): array {
$features = [self::CAN_TEST];
if (!empty(self::$featurestotest)) {
$features = array_merge($features, self::$featurestotest);
}
return $features;
}
/**
* Return an array of extensions this plugin could manage.
*
* @return array
*/
public function get_manageable_extensions(): array {
return ['.txt', '.png', '.h5p'];
}
/**
* Returns the list of different types of the given content type.
*
* @return array
*/
public function get_contenttype_types(): array {
$type = new \stdClass();
$type->typename = 'testable';
return [$type];
}
/**
* Returns true, so the user has permission on the feature.
*
* @return bool True if content could be edited or created. False otherwise.
*/
final public function can_test2(): bool {
if (!$this->is_feature_supported('test2')) {
return false;
}
return true;
}
/**
* This implements custom file serving.
*
* @param stdClass $course the course object
* @param stdClass $cm the course module object
* @param \context $context the context
* @param string $filearea the name of the file area
* @param array $args extra arguments (itemid, path)
* @param bool $forcedownload whether or not force download
* @param array $options additional options affecting the file serving
* @return void
*/
public static function pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options = []): void {
return;
}
}
+110
View File
@@ -0,0 +1,110 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Generator for the core_contentbank subsystem.
*
* @package core_contentbank
* @category test
* @copyright 2020 Sara Arjona <sara@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use core_contentbank\content;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_contenttype.php');
/**
* Generator for the core_contentbank subsystem.
*
* @package core_contentbank
* @copyright 2020 Sara Arjona <sara@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class core_contentbank_generator extends \component_generator_base {
/**
* Populate contentbank database tables with relevant data to simulate the process of adding items to the content bank.
*
* @param string $contenttype Content bank plugin type to add. If none is defined, contenttype_testable is used.
* @param int $itemstocreate Number of items to add to the content bank.
* @param int $userid The user identifier creating the content.
* @param context $context The context where the content will be created.
* @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 ', int $visibility = content::VISIBILITY_PUBLIC): array {
global $DB, $USER;
$records = [];
$contenttype = $contenttype ?? 'contenttype_testable';
$contenttypeclass = "\\$contenttype\\contenttype";
if (!class_exists($contenttypeclass)) {
// Early return with empty array because the contenttype doesn't exist.
return $records;
}
if (empty($context)) {
$context = \context_system::instance();
}
$type = new $contenttypeclass($context);
$fs = get_file_storage();
for ($i = 0; $i < $itemstocreate; $i++) {
// Create content.
$record = new stdClass();
// If only 1 item is being created, do not add a number suffix to the content name.
$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();
// Create a dummy file.
$filerecord = array(
'contextid' => $context->id,
'component' => 'contentbank',
'filearea' => 'public',
'itemid' => $record->id,
'filepath' => '/',
'filename' => basename($filepath)
);
if (file_exists($filepath)) {
$fs->create_file_from_pathname($filerecord, $filepath);
} else {
$fs->create_file_from_string($filerecord, 'Dummy content ' . $i);
}
// Prepare the return value.
if ($convert2class) {
$records[$record->id] = $content;
} else {
$records[$record->id] = $record;
}
}
return $records;
}
}
+411
View File
@@ -0,0 +1,411 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Base class for unit tests for core_contentbank.
*
* @package core_contentbank
* @category test
* @copyright 2020 Carlos Escobedo <carlos@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_contentbank\privacy;
use stdClass;
use context_system;
use context_coursecat;
use context_course;
use context_user;
use core_contentbank\privacy\provider;
use core_privacy\local\request\approved_contextlist;
use core_privacy\local\request\writer;
use core_privacy\tests\provider_testcase;
use core_privacy\local\request\userlist;
use core_privacy\local\request\approved_userlist;
/**
* Unit tests for contentbank\classes\privacy\provider.php
*
* @copyright 2020 Carlos Escobedo <carlos@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider_test extends provider_testcase {
/**
* Setup to ensure that fixtures are loaded.
*/
public static function setupBeforeClass(): void {
global $CFG;
require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_content.php');
}
/**
* Test for provider::get_contexts_for_userid().
*/
public function test_get_contexts_for_userid(): void {
$this->resetAfterTest();
// Setup scenario.
$scenario = $this->setup_scenario();
// Testing againts Manager who has content in the three contexts.
$contextlist = provider::get_contexts_for_userid($scenario->manager->id);
// There are three contexts in the list.
$contextlistids = $contextlist->get_contextids();
$this->assertCount(3, $contextlistids);
// Check the list against the expected list of contexts.
$this->assertContainsEquals($scenario->systemcontext->id, $contextlistids);
$this->assertContainsEquals($scenario->coursecategorycontext->id,
$contextlistids);
$this->assertContainsEquals($scenario->coursecontext->id, $contextlistids);
// Testing againts Teacher who has content in the one context.
$contextlist = provider::get_contexts_for_userid($scenario->teacher->id);
// There are only one context in the list.
$contextlistids = $contextlist->get_contextids();
$this->assertCount(1, $contextlistids);
// Check the againts Course Context.
$this->assertContainsEquals($scenario->coursecontext->id, $contextlistids);
// And there is not a System and Course Category Context.
$this->assertNotContainsEquals($scenario->systemcontext->id, $contextlistids);
$this->assertNotContainsEquals($scenario->coursecategorycontext->id, $contextlistids);
}
/**
* Test for provider::get_users_in_context().
*/
public function test_get_users_in_context(): void {
$this->resetAfterTest();
// Setup scenario.
$scenario = $this->setup_scenario();
// Get the userlist to Context System, only Manager will be there.
$userlist = new userlist($scenario->systemcontext, 'core_contentbank');
provider::get_users_in_context($userlist);
$this->assertEquals([$scenario->manager->id], $userlist->get_userids());
// Teacher will not be there.
$this->assertNotEquals([$scenario->teacher->id], $userlist->get_userids());
// Get the userlist to Context Course, Manager and Teacher will be there.
$userlist = new userlist($scenario->coursecontext, 'core_contentbank');
provider::get_users_in_context($userlist);
$expected = [$scenario->manager->id, $scenario->teacher->id];
sort($expected);
$actual = $userlist->get_userids();
sort($actual);
$this->assertEquals($expected, $actual);
}
/**
* Test for provider::test_export_user_data().
*/
public function test_export_user_data(): void {
$this->resetAfterTest();
// Setup scenario.
$scenario = $this->setup_scenario();
$subcontexts = [
get_string('name', 'core_contentbank')
];
// Get the data for the System Context.
$writer = writer::with_context($scenario->systemcontext);
$this->assertFalse($writer->has_any_data());
// Export data for Manager.
$this->export_context_data_for_user($scenario->manager->id,
$scenario->systemcontext, 'core_contentbank');
$data = $writer->get_data($subcontexts);
$this->assertCount(3, (array) $data);
$this->assertCount(3, $writer->get_files($subcontexts));
// Get the data for the Course Categoy Context.
$writer = writer::with_context($scenario->coursecategorycontext);
// Export data for Manager.
$this->export_context_data_for_user($scenario->manager->id,
$scenario->coursecategorycontext, 'core_contentbank');
$data = $writer->get_data($subcontexts);
$this->assertCount(2, (array) $data);
$this->assertCount(2, $writer->get_files($subcontexts));
// Get the data for the Course Context.
$writer = writer::with_context($scenario->coursecontext);
// Export data for Manager.
$this->export_context_data_for_user($scenario->manager->id,
$scenario->coursecontext, 'core_contentbank');
$data = $writer->get_data($subcontexts);
$this->assertCount(2, (array) $data);
$this->assertCount(2, $writer->get_files($subcontexts));
// Export data for Teacher.
$writer = writer::reset();
$writer = writer::with_context($scenario->coursecontext);
$this->export_context_data_for_user($scenario->teacher->id,
$scenario->coursecontext, 'core_contentbank');
$data = $writer->get_data($subcontexts);
$this->assertCount(3, (array) $data);
$this->assertCount(3, $writer->get_files($subcontexts));
}
/**
* Test for provider::delete_data_for_all_users_in_context().
*/
public function test_delete_data_for_all_users_in_context(): void {
global $DB;
$this->resetAfterTest();
// Setup scenario.
$scenario = $this->setup_scenario();
// Before delete data, we have 4 contents.
// - 3 in a system context.
// - 2 in a course category context.
// - 5 in a course context (2 by manager and 3 by teacher).
// Delete data based on system context.
provider::delete_data_for_all_users_in_context($scenario->systemcontext);
$count = $DB->count_records('contentbank_content');
// 3 content should be deleted.
// 7 contents should be remain.
$this->assertEquals(7, $count);
// Delete data based on course category context.
provider::delete_data_for_all_users_in_context($scenario->coursecategorycontext);
$count = $DB->count_records('contentbank_content');
// 2 contents should be deleted.
// 5 content should be remain.
$this->assertEquals(5, $count);
// Delete data based on course context.
provider::delete_data_for_all_users_in_context($scenario->coursecontext);
$count = $DB->count_records('contentbank_content');
// 5 content should be deleted.
// 0 content should be remain.
$this->assertEquals(0, $count);
}
/**
* Test for provider::test_delete_data_for_users().
*/
public function test_delete_data_for_users(): void {
global $DB;
$this->resetAfterTest();
// Setup scenario.
$scenario = $this->setup_scenario();
// Before delete data, we have 4 contents.
// - 3 in a system context.
// - 2 in a course category context.
// - 5 in a course context (2 by manager and 3 by teacher).
// A list of users who has created content in Course Category Context.
$userlist1 = new userlist($scenario->coursecategorycontext,
'core_contentbank');
provider::get_users_in_context($userlist1);
$this->assertCount(1, $userlist1);
// Only Manager should be.
$this->assertEquals([$scenario->manager->id], $userlist1->get_userids());
// A list of users who has created content in Course Context.
$userlist2 = new userlist($scenario->coursecontext, 'core_contentbank');
provider::get_users_in_context($userlist2);
$this->assertCount(2, $userlist2);
// Manager and Teacher should be.
$expected = [$scenario->manager->id, $scenario->teacher->id];
sort($expected);
$actual = $userlist2->get_userids();
sort($actual);
$this->assertEquals($expected, $actual);
// Convert $userlist1 into an approved_contextlist.
$approvedlist1 = new approved_userlist($scenario->coursecategorycontext, 'core_contentbank', $userlist1->get_userids());
// Delete data for users in course category context.
provider::delete_data_for_users($approvedlist1);
// Re-fetch users in course category context.
$userlist1 = new userlist($scenario->coursecategorycontext,
'core_contentbank');
provider::get_users_in_context($userlist1);
// The user data in course category context should be deleted.
$this->assertCount(0, $userlist1);
// Re-fetch users in course category context.
$userlist2 = new userlist($scenario->coursecontext, 'core_contentbank');
provider::get_users_in_context($userlist2);
// The user data in course context should be still present.
$this->assertCount(2, $userlist2);
// Convert $userlist2 into an approved_contextlist.
$approvedlist2 = new approved_userlist($scenario->coursecontext,
'core_contentbank', $userlist2->get_userids());
// Delete data for users in course context.
provider::delete_data_for_users($approvedlist2);
$userlist2 = new userlist($scenario->coursecontext, 'core_contentbank');
provider::get_users_in_context($userlist2);
// The user data in course context should be deleted.
$this->assertCount(0, $userlist2);
}
/**
* Test for provider::delete_data_for_user().
*/
public function test_delete_data_for_user(): void {
global $DB;
$this->resetAfterTest();
// Setup scenario.
$scenario = $this->setup_scenario();
// Before delete data, we have 4 contents.
// - 3 in a system context.
// - 2 in a course category context.
// - 5 in a course context (2 by manager and 3 by teacher).
// Get all the context for Manager.
$contextlist = provider::get_contexts_for_userid($scenario->manager->id);
$approvedcontextlist = new approved_contextlist($scenario->manager,
'core_contentbank', $contextlist->get_contextids());
// Delete all the data created by the Manager in all the contexts.
provider::delete_data_for_user($approvedcontextlist);
// After deletion, only 3 content for teacher should be present.
$count = $DB->count_records('contentbank_content');
$this->assertEquals(3, $count);
// Confirm that the remaining content was created by the teacher.
$count = $DB->count_records('contentbank_content',
['usercreated' => $scenario->teacher->id]);
$this->assertEquals(3, $count);
// Get all the context for Teacher.
$contextlist = provider::get_contexts_for_userid($scenario->teacher->id);
$approvedcontextlist = new approved_contextlist($scenario->teacher,
'core_contentbank', $contextlist->get_contextids());
// Delete all the data created by the Teacher in all the contexts.
provider::delete_data_for_user($approvedcontextlist);
// After deletion, no content should be present.
$count = $DB->count_records('contentbank_content');
$this->assertEquals(0, $count);
}
/**
* Create a complex scenario to use into the tests.
*
* @return stdClass $scenario
*/
protected function setup_scenario() {
global $DB;
$systemcontext = context_system::instance();
$manager = $this->getDataGenerator()->create_user();
$managerroleid = $DB->get_field('role', 'id', ['shortname' => 'manager']);
$this->getDataGenerator()->role_assign($managerroleid, $manager->id);
$coursecategory = $this->getDataGenerator()->create_category();
$coursecategorycontext = context_coursecat::instance($coursecategory->id);
$course = $this->getDataGenerator()->create_course();
$coursecontext = context_course::instance($course->id);
$teacher = $this->getDataGenerator()->create_and_enrol($course,
'editingteacher');
// Add some content to the content bank.
$generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank');
// Add contents by Manager in Context System.
$records = $generator->generate_contentbank_data('contenttype_testable',
1, $manager->id, $systemcontext, false, 'systemtestfile1.h5p');
$records = $generator->generate_contentbank_data('contenttype_testable',
1, $manager->id, $systemcontext, false, 'systemtestfile2.h5p');
$records = $generator->generate_contentbank_data('contenttype_testable',
1, $manager->id, $systemcontext, false, 'systemtestfile3.h5p');
// Add contents by Manager in Context Course Category.
$records = $generator->generate_contentbank_data('contenttype_testable',
1, $manager->id, $coursecategorycontext, false, 'coursecattestfile1.h5p');
$records = $generator->generate_contentbank_data('contenttype_testable',
1, $manager->id, $coursecategorycontext, false, 'coursecattestfile2.h5p');
// Add contents by Manager in Context Course.
$records = $generator->generate_contentbank_data('contenttype_testable',
1, $manager->id, $coursecontext, false, 'coursetestfile1.h5p');
$records = $generator->generate_contentbank_data('contenttype_testable',
1, $manager->id, $coursecontext, false, 'coursetestfile2.h5p');
// Add contents by Teacher.
$records = $generator->generate_contentbank_data('contenttype_testable',
1, $teacher->id, $coursecontext, false, 'courseteacherfile1.h5p');
$records = $generator->generate_contentbank_data('contenttype_testable',
1, $teacher->id, $coursecontext, false, 'courseteacherfile2.h5p');
$records = $generator->generate_contentbank_data('contenttype_testable',
1, $teacher->id, $coursecontext, false, 'courseteacherfile3.h5p');
$scenario = new stdClass();
$scenario->systemcontext = $systemcontext;
$scenario->coursecategorycontext = $coursecategorycontext;
$scenario->coursecontext = $coursecontext;
$scenario->manager = $manager;
$scenario->teacher = $teacher;
return $scenario;
}
/**
* Ensure that export_user_preferences returns no data if the user has not visited any content bank.
*/
public function test_export_user_preferences_no_pref(): void {
global $DB;
$this->resetAfterTest();
$user = $this->getDataGenerator()->create_user();
$managerroleid = $DB->get_field('role', 'id', ['shortname' => 'manager']);
$this->getDataGenerator()->role_assign($managerroleid, $user->id);
provider::export_user_preferences($user->id);
$writer = writer::with_context(context_system::instance());
$this->assertFalse($writer->has_any_data());
}
/**
* Test for provider::test_export_user_preferences().
*/
public function test_export_user_preferences(): void {
global $DB;
// Test setup.
$this->resetAfterTest(true);
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
set_user_preference('core_contentbank_view_list', 1);
// Test the user preferences export contains 1 user preference record for the User.
provider::export_user_preferences($user->id);
$contextuser = context_user::instance($user->id);
$writer = writer::with_context($contextuser);
$this->assertTrue($writer->has_any_data());
$prefs = $writer->get_user_preferences('core_contentbank');
$this->assertCount(1, (array) $prefs);
$this->assertEquals(1, $prefs->core_contentbank_view_list->value);
$this->assertEquals(
get_string('privacy:request:preference:set', 'core_contentbank', (object) [
'name' => 'core_contentbank_view_list',
'value' => $prefs->core_contentbank_view_list->value,
]),
$prefs->core_contentbank_view_list->description
);
}
}
+8
View File
@@ -0,0 +1,8 @@
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.
* Added set_visibility()/get_visibility() methods to let users decide if their content should be listed in the content bank.
* The contentbank/upload.php page for displaying the upload files form has been removed. The form for uploading/replacing
files now is displayed in a modal.
+117
View File
@@ -0,0 +1,117 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Generic content bank visualizer.
*
* @package core_contentbank
* @copyright 2020 Amaia Anabitarte <amaia@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use core_contentbank\content;
require('../config.php');
require_login();
$id = required_param('id', PARAM_INT);
$deletecontent = optional_param('deletecontent', null, PARAM_INT);
$PAGE->requires->js_call_amd('core_contentbank/actions', 'init');
$record = $DB->get_record('contentbank_content', ['id' => $id], '*', MUST_EXIST);
$context = context::instance_by_id($record->contextid, MUST_EXIST);
require_capability('moodle/contentbank:access', $context);
// If notifications had been sent we don't pay attention to message parameter.
if (empty($SESSION->notifications)) {
$statusmsg = optional_param('statusmsg', '', PARAM_ALPHANUMEXT);
$errormsg = optional_param('errormsg', '', PARAM_ALPHANUMEXT);
}
$returnurl = new \moodle_url('/contentbank/index.php', ['contextid' => $context->id]);
$plugin = core_plugin_manager::instance()->get_plugin_info($record->contenttype);
if (!$plugin || !$plugin->is_enabled()) {
throw new \moodle_exception('unsupported', 'core_contentbank', $returnurl);
}
$title = get_string('contentbank');
\core_contentbank\helper::get_page_ready($context, $title, true);
if ($PAGE->course) {
require_login($PAGE->course->id);
}
$cb = new \core_contentbank\contentbank();
$content = $cb->get_content_from_id($record->id);
$contenttype = $content->get_content_type_instance();
if (!$content->is_view_allowed()) {
$cburl = new \moodle_url('/contentbank/index.php', ['contextid' => $context->id, 'errormsg' => 'notavailable']);
redirect($cburl);
}
if ($context->contextlevel == CONTEXT_COURSECAT) {
$PAGE->set_primary_active_tab('home');
}
$PAGE->set_url(new \moodle_url('/contentbank/view.php', ['id' => $id]));
if ($context->id == \context_system::instance()->id) {
$PAGE->set_context(context_course::instance($context->id));
} else {
$PAGE->set_context($context);
}
$PAGE->navbar->add($record->name);
$title .= ": ".$record->name;
$PAGE->set_title($title);
$PAGE->set_pagetype('contentbank');
$PAGE->set_pagelayout('incourse');
$PAGE->set_secondary_active_tab('contentbank');
echo $OUTPUT->header();
// If needed, display notifications.
if (!empty($errormsg) && get_string_manager()->string_exists($errormsg, 'core_contentbank')) {
$errormsg = get_string($errormsg, 'core_contentbank');
echo $OUTPUT->notification($errormsg);
} else if (!empty($statusmsg) && get_string_manager()->string_exists($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:
throw new \moodle_exception('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()) {
$viewcontent = new core_contentbank\output\viewcontent($contenttype, $content);
echo $OUTPUT->render($viewcontent);
} else {
$message = get_string('contenttypenoaccess', 'core_contentbank', $record->contenttype);
echo $OUTPUT->notification($message, 'error');
}
echo $OUTPUT->footer();