From 81b761bab02ebce5d68b1bb7e615bc9d32747552 Mon Sep 17 00:00:00 2001 From: Amaia Anabitarte Date: Mon, 7 Nov 2022 16:23:56 +0100 Subject: [PATCH] MDL-75492 mod_data: Show mapping dialog --- .../amd/build/importmappingdialogue.min.js | 10 + .../build/importmappingdialogue.min.js.map | 1 + mod/data/amd/build/selectpreset.min.js | 2 +- mod/data/amd/build/selectpreset.min.js.map | 2 +- mod/data/amd/src/importmappingdialogue.js | 164 ++++++++++++ mod/data/amd/src/selectpreset.js | 8 +- .../external/get_mapping_information.php | 97 ++++++++ .../local/importer/preset_importer.php | 65 ++++- mod/data/classes/output/preset_preview.php | 4 +- mod/data/classes/output/presets.php | 2 + mod/data/classes/preset.php | 3 +- mod/data/db/services.php | 7 + mod/data/field.php | 2 +- mod/data/lang/en/data.php | 9 + mod/data/styles.css | 1 + .../templates/fields_mapping_body.mustache | 48 ++++ .../templates/fields_mapping_footer.mustache | 33 +++ mod/data/templates/preset_preview.mustache | 13 +- mod/data/templates/presets.mustache | 8 +- mod/data/tests/behat/preview_preset.feature | 2 +- mod/data/tests/behat/use_presets.feature | 77 +++++- .../external/get_mapping_information_test.php | 235 ++++++++++++++++++ mod/data/tests/preset_importer_test.php | 176 +++++++++++++ mod/data/version.php | 2 +- 24 files changed, 939 insertions(+), 32 deletions(-) create mode 100644 mod/data/amd/build/importmappingdialogue.min.js create mode 100644 mod/data/amd/build/importmappingdialogue.min.js.map create mode 100644 mod/data/amd/src/importmappingdialogue.js create mode 100644 mod/data/classes/external/get_mapping_information.php create mode 100644 mod/data/templates/fields_mapping_body.mustache create mode 100644 mod/data/templates/fields_mapping_footer.mustache create mode 100644 mod/data/tests/external/get_mapping_information_test.php diff --git a/mod/data/amd/build/importmappingdialogue.min.js b/mod/data/amd/build/importmappingdialogue.min.js new file mode 100644 index 00000000000..20e603ce9c9 --- /dev/null +++ b/mod/data/amd/build/importmappingdialogue.min.js @@ -0,0 +1,10 @@ +define("mod_data/importmappingdialogue",["exports","core/notification","core/ajax","core/url","core/templates","core/modal_factory","core/prefetch","core/str"],(function(_exports,_notification,_ajax,_url,_templates,_modal_factory,_prefetch,_str){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} +/** + * Javascript module for deleting a database as a preset. + * + * @module mod_data/importmappingdialogue + * @copyright 2022 Amaia Anabitarte + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_notification=_interopRequireDefault(_notification),_ajax=_interopRequireDefault(_ajax),_url=_interopRequireDefault(_url),_templates=_interopRequireDefault(_templates),_modal_factory=_interopRequireDefault(_modal_factory),(0,_prefetch.prefetchStrings)("mod_data",["mapping:dialogtitle:usepreset"]);const selectors_selectPresetButton='input[name="selectpreset"]';_exports.init=()=>{registerEventListeners()};const registerEventListeners=()=>{document.addEventListener("click",(event=>{const usepreset=event.target.closest(selectors_selectPresetButton);usepreset&&(event.preventDefault(),showMappingDialogue(usepreset))}))},showMappingDialogue=usepreset=>{const presetName=usepreset.dataset.presetname,cmId=usepreset.dataset.cmid;getMappingInformation(cmId,presetName).then((result=>(result.data&&result.data.needsmapping?buildModal({title:(0,_str.get_string)("mapping:dialogtitle:usepreset","mod_data",result.data.presetname),body:_templates.default.render("mod_data/fields_mapping_body",result.data),footer:_templates.default.render("mod_data/fields_mapping_footer",getMappingButtons(cmId,presetName)),large:!0}):window.location.href=_url.default.relativeUrl("mod/data/field.php",{id:cmId,mode:"usepreset",fullname:presetName},!1),!0))).catch(_notification.default.exception)},buildModal=params=>_modal_factory.default.create({...params,type:_modal_factory.default.types.DEFAULT}).then((modal=>(modal.show(),modal.showFooter(),modal.registerCloseOnCancel(),modal))).catch(_notification.default.exception),getMappingButtons=(cmId,presetName)=>{const data={};return data.mapfieldsbutton=_url.default.relativeUrl("mod/data/field.php",{id:cmId,fullname:presetName,mode:"usepreset",action:"select"},!1),data.applybutton=_url.default.relativeUrl("mod/data/field.php",{id:cmId,fullname:presetName,mode:"usepreset",action:"notmapping"},!1),data},getMappingInformation=(cmId,presetName)=>{const request={methodname:"mod_data_get_mapping_information",args:{cmid:cmId,importedpreset:presetName}};return _ajax.default.call([request])[0]}})); + +//# sourceMappingURL=importmappingdialogue.min.js.map \ No newline at end of file diff --git a/mod/data/amd/build/importmappingdialogue.min.js.map b/mod/data/amd/build/importmappingdialogue.min.js.map new file mode 100644 index 00000000000..e728d110fa0 --- /dev/null +++ b/mod/data/amd/build/importmappingdialogue.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"importmappingdialogue.min.js","sources":["../src/importmappingdialogue.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 .\n\n/**\n * Javascript module for deleting a database as a preset.\n *\n * @module mod_data/importmappingdialogue\n * @copyright 2022 Amaia Anabitarte \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Notification from 'core/notification';\nimport Ajax from 'core/ajax';\nimport Url from 'core/url';\nimport Templates from 'core/templates';\nimport ModalFactory from 'core/modal_factory';\nimport {prefetchStrings} from 'core/prefetch';\nimport {get_string as getString} from 'core/str';\n\n// Load global strings.\nprefetchStrings('mod_data', ['mapping:dialogtitle:usepreset']);\n\nconst selectors = {\n selectPresetButton: 'input[name=\"selectpreset\"]',\n};\n\n/**\n * Initialize module\n */\nexport const init = () => {\n registerEventListeners();\n};\n\n/**\n * Register events.\n */\nconst registerEventListeners = () => {\n document.addEventListener('click', (event) => {\n const usepreset = event.target.closest(selectors.selectPresetButton);\n if (usepreset) {\n event.preventDefault();\n showMappingDialogue(usepreset);\n }\n });\n};\n\n/**\n * Show the confirmation modal for uploading a preset.\n *\n * @param {HTMLElement} usepreset the preset to import.\n */\nconst showMappingDialogue = (usepreset) => {\n const presetName = usepreset.dataset.presetname;\n const cmId = usepreset.dataset.cmid;\n\n getMappingInformation(cmId, presetName).then((result) => {\n if (result.data && result.data.needsmapping) {\n buildModal({\n title: getString('mapping:dialogtitle:usepreset', 'mod_data', result.data.presetname),\n body: Templates.render('mod_data/fields_mapping_body', result.data),\n footer: Templates.render('mod_data/fields_mapping_footer', getMappingButtons(cmId, presetName)),\n large: true,\n });\n } else {\n window.location.href = Url.relativeUrl(\n 'mod/data/field.php',\n {\n id: cmId,\n mode: 'usepreset',\n fullname: presetName,\n },\n false\n );\n }\n return true;\n }).catch(Notification.exception);\n};\n\n/**\n * Given an object we want to build a modal ready to show\n *\n * @method buildModal\n * @param {Object} params the modal params\n * @param {Promise} params.title\n * @param {Promise} params.body\n * @param {Promise} params.footer\n * @return {Object} The modal ready to display immediately and render body in later.\n */\nconst buildModal = (params) => {\n return ModalFactory.create({\n ...params,\n type: ModalFactory.types.DEFAULT,\n }).then(modal => {\n modal.show();\n modal.showFooter();\n modal.registerCloseOnCancel();\n return modal;\n }).catch(Notification.exception);\n};\n\n/**\n * Add buttons to render on mapping modal.\n *\n * @param {int} cmId The id of the current database activity.\n * @param {string} presetName The preset name to delete.\n * @return {array} Same data with buttons.\n */\nconst getMappingButtons = (cmId, presetName) => {\n const data = {};\n\n data.mapfieldsbutton = Url.relativeUrl(\n 'mod/data/field.php',\n {\n id: cmId,\n fullname: presetName,\n mode: 'usepreset',\n action: 'select',\n },\n false\n );\n\n data.applybutton = Url.relativeUrl(\n 'mod/data/field.php',\n {\n id: cmId,\n fullname: presetName,\n mode: 'usepreset',\n action: 'notmapping'\n },\n false\n );\n\n return data;\n};\n\n/**\n * Check whether we should show the mapping dialogue or not.\n *\n * @param {int} cmId The id of the current database activity.\n * @param {string} presetName The preset name to delete.\n * @return {promise} Resolved with the result and warnings of deleting a preset.\n */\nconst getMappingInformation = (cmId, presetName) => {\n const request = {\n methodname: 'mod_data_get_mapping_information',\n args: {\n cmid: cmId,\n importedpreset: presetName,\n }\n };\n return Ajax.call([request])[0];\n};\n"],"names":["selectors","registerEventListeners","document","addEventListener","event","usepreset","target","closest","preventDefault","showMappingDialogue","presetName","dataset","presetname","cmId","cmid","getMappingInformation","then","result","data","needsmapping","buildModal","title","body","Templates","render","footer","getMappingButtons","large","window","location","href","Url","relativeUrl","id","mode","fullname","catch","Notification","exception","params","ModalFactory","create","type","types","DEFAULT","modal","show","showFooter","registerCloseOnCancel","mapfieldsbutton","action","applybutton","request","methodname","args","importedpreset","Ajax","call"],"mappings":";;;;;;;8UAgCgB,WAAY,CAAC,wCAEvBA,6BACkB,2CAMJ,KAChBC,gCAMEA,uBAAyB,KAC3BC,SAASC,iBAAiB,SAAUC,cAC1BC,UAAYD,MAAME,OAAOC,QAAQP,8BACnCK,YACAD,MAAMI,iBACNC,oBAAoBJ,gBAU1BI,oBAAuBJ,kBACnBK,WAAaL,UAAUM,QAAQC,WAC/BC,KAAOR,UAAUM,QAAQG,KAE/BC,sBAAsBF,KAAMH,YAAYM,MAAMC,SACtCA,OAAOC,MAAQD,OAAOC,KAAKC,aAC3BC,WAAW,CACPC,OAAO,mBAAU,gCAAiC,WAAYJ,OAAOC,KAAKN,YAC1EU,KAAMC,mBAAUC,OAAO,+BAAgCP,OAAOC,MAC9DO,OAAQF,mBAAUC,OAAO,iCAAkCE,kBAAkBb,KAAMH,aACnFiB,OAAO,IAGXC,OAAOC,SAASC,KAAOC,aAAIC,YACvB,qBACA,CACIC,GAAIpB,KACJqB,KAAM,YACNC,SAAUzB,aAEd,IAGD,KACR0B,MAAMC,sBAAaC,YAapBlB,WAAcmB,QACTC,uBAAaC,OAAO,IACpBF,OACHG,KAAMF,uBAAaG,MAAMC,UAC1B5B,MAAK6B,QACJA,MAAMC,OACND,MAAME,aACNF,MAAMG,wBACCH,SACRT,MAAMC,sBAAaC,WAUpBZ,kBAAoB,CAACb,KAAMH,oBACvBQ,KAAO,UAEbA,KAAK+B,gBAAkBlB,aAAIC,YACvB,qBACA,CACIC,GAAIpB,KACJsB,SAAUzB,WACVwB,KAAM,YACNgB,OAAQ,WAEZ,GAGJhC,KAAKiC,YAAcpB,aAAIC,YACnB,qBACA,CACIC,GAAIpB,KACJsB,SAAUzB,WACVwB,KAAM,YACNgB,OAAQ,eAEZ,GAGGhC,MAULH,sBAAwB,CAACF,KAAMH,oBAC3B0C,QAAU,CACZC,WAAY,mCACZC,KAAM,CACFxC,KAAMD,KACN0C,eAAgB7C,oBAGjB8C,cAAKC,KAAK,CAACL,UAAU"} \ No newline at end of file diff --git a/mod/data/amd/build/selectpreset.min.js b/mod/data/amd/build/selectpreset.min.js index 9e64d02194d..46e992f9533 100644 --- a/mod/data/amd/build/selectpreset.min.js +++ b/mod/data/amd/build/selectpreset.min.js @@ -6,6 +6,6 @@ define("mod_data/selectpreset",["exports"],(function(_exports){Object.defineProp * @copyright 2021 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -const selectors_presetRadioButton='input[name="fullname"]',selectors_selectPresetButton='input[name="selectpreset"]',selectors_selectedPresetRadioButton='input[name="fullname"]:checked';_exports.init=()=>{const radioButton=document.querySelectorAll(selectors_presetRadioButton);disableUsePresetButton(),radioButton.forEach((elem=>{elem.addEventListener("change",(function(event){event.preventDefault(),disableUsePresetButton()}))}))};const disableUsePresetButton=()=>{let selectPresetButton=document.querySelector(selectors_selectPresetButton);document.querySelectorAll(selectors_selectedPresetRadioButton).length>0?(selectPresetButton.removeAttribute("disabled"),selectPresetButton.classList.remove("btn-secondary"),selectPresetButton.classList.add("btn-primary")):(selectPresetButton.setAttribute("disabled",!0),selectPresetButton.classList.remove("btn-primary"),selectPresetButton.classList.add("btn-secondary"))}})); +const selectors_presetRadioButton='input[name="fullname"]',selectors_selectPresetButton='input[name="selectpreset"]',selectors_selectedPresetRadioButton='input[name="fullname"]:checked';_exports.init=()=>{const radioButton=document.querySelectorAll(selectors_presetRadioButton);disableUsePresetButton(),radioButton.forEach((elem=>{elem.addEventListener("change",(function(event){event.preventDefault(),disableUsePresetButton()}))}))};const disableUsePresetButton=()=>{let selectPresetButton=document.querySelector(selectors_selectPresetButton);const selectedRadioButton=document.querySelector(selectors_selectedPresetRadioButton);selectedRadioButton?(selectPresetButton.removeAttribute("disabled"),selectPresetButton.classList.remove("btn-secondary"),selectPresetButton.classList.add("btn-primary"),selectPresetButton.setAttribute("data-presetname",selectedRadioButton.getAttribute("value")),selectPresetButton.setAttribute("data-cmid",selectedRadioButton.getAttribute("data-cmid"))):(selectPresetButton.setAttribute("disabled",!0),selectPresetButton.classList.remove("btn-primary"),selectPresetButton.classList.add("btn-secondary"),selectPresetButton.removeAttribute("data-presetname"),selectPresetButton.removeAttribute("data-cmid"))}})); //# sourceMappingURL=selectpreset.min.js.map \ No newline at end of file diff --git a/mod/data/amd/build/selectpreset.min.js.map b/mod/data/amd/build/selectpreset.min.js.map index e7ee516bd6d..97239a2842b 100644 --- a/mod/data/amd/build/selectpreset.min.js.map +++ b/mod/data/amd/build/selectpreset.min.js.map @@ -1 +1 @@ -{"version":3,"file":"selectpreset.min.js","sources":["../src/selectpreset.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 .\n\n/**\n * Javascript module to control the form responsible for selecting a preset.\n *\n * @module mod_data/selectpreset\n * @copyright 2021 Mihail Geshoski \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nconst selectors = {\n presetRadioButton: 'input[name=\"fullname\"]',\n selectPresetButton: 'input[name=\"selectpreset\"]',\n selectedPresetRadioButton: 'input[name=\"fullname\"]:checked',\n};\n\n/**\n * Initialize module.\n */\nexport const init = () => {\n const radioButton = document.querySelectorAll(selectors.presetRadioButton);\n\n // Initialize the \"Use a preset\" button properly.\n disableUsePresetButton();\n\n radioButton.forEach((elem) => {\n elem.addEventListener('change', function(event) {\n event.preventDefault();\n // Enable the \"Use a preset\" button when any of the radio buttons in the presets list is checked.\n disableUsePresetButton();\n });\n });\n\n};\n\n/**\n * Decide whether to disable or not the \"Use a preset\" button.\n * When there is no preset selected, the button should be displayed disabled; otherwise, it will appear enabled as a primary button.\n *\n * @method\n * @private\n */\nconst disableUsePresetButton = () => {\n let selectPresetButton = document.querySelector(selectors.selectPresetButton);\n const selectedRadioButton = document.querySelectorAll(selectors.selectedPresetRadioButton);\n\n if (selectedRadioButton.length > 0) {\n // There is one preset selected, so the button should be enabled.\n selectPresetButton.removeAttribute('disabled');\n selectPresetButton.classList.remove('btn-secondary');\n selectPresetButton.classList.add('btn-primary');\n } else {\n // There is no any preset selected, so the button should be disabled.\n selectPresetButton.setAttribute('disabled', true);\n selectPresetButton.classList.remove('btn-primary');\n selectPresetButton.classList.add('btn-secondary');\n }\n};\n"],"names":["selectors","radioButton","document","querySelectorAll","disableUsePresetButton","forEach","elem","addEventListener","event","preventDefault","selectPresetButton","querySelector","length","removeAttribute","classList","remove","add","setAttribute"],"mappings":";;;;;;;;MAuBMA,4BACiB,yBADjBA,6BAEkB,6BAFlBA,oCAGyB,+CAMX,WACVC,YAAcC,SAASC,iBAAiBH,6BAG9CI,yBAEAH,YAAYI,SAASC,OACjBA,KAAKC,iBAAiB,UAAU,SAASC,OACrCA,MAAMC,iBAENL,sCAaNA,uBAAyB,SACvBM,mBAAqBR,SAASS,cAAcX,8BACpBE,SAASC,iBAAiBH,qCAE9BY,OAAS,GAE7BF,mBAAmBG,gBAAgB,YACnCH,mBAAmBI,UAAUC,OAAO,iBACpCL,mBAAmBI,UAAUE,IAAI,iBAGjCN,mBAAmBO,aAAa,YAAY,GAC5CP,mBAAmBI,UAAUC,OAAO,eACpCL,mBAAmBI,UAAUE,IAAI"} \ No newline at end of file +{"version":3,"file":"selectpreset.min.js","sources":["../src/selectpreset.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 .\n\n/**\n * Javascript module to control the form responsible for selecting a preset.\n *\n * @module mod_data/selectpreset\n * @copyright 2021 Mihail Geshoski \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nconst selectors = {\n presetRadioButton: 'input[name=\"fullname\"]',\n selectPresetButton: 'input[name=\"selectpreset\"]',\n selectedPresetRadioButton: 'input[name=\"fullname\"]:checked',\n};\n\n/**\n * Initialize module.\n */\nexport const init = () => {\n const radioButton = document.querySelectorAll(selectors.presetRadioButton);\n\n // Initialize the \"Use a preset\" button properly.\n disableUsePresetButton();\n\n radioButton.forEach((elem) => {\n elem.addEventListener('change', function(event) {\n event.preventDefault();\n // Enable the \"Use a preset\" button when any of the radio buttons in the presets list is checked.\n disableUsePresetButton();\n });\n });\n\n};\n\n/**\n * Decide whether to disable or not the \"Use a preset\" button.\n * When there is no preset selected, the button should be displayed disabled; otherwise, it will appear enabled as a primary button.\n *\n * @method\n * @private\n */\nconst disableUsePresetButton = () => {\n let selectPresetButton = document.querySelector(selectors.selectPresetButton);\n const selectedRadioButton = document.querySelector(selectors.selectedPresetRadioButton);\n\n if (selectedRadioButton) {\n // There is one preset selected, so the button should be enabled.\n selectPresetButton.removeAttribute('disabled');\n selectPresetButton.classList.remove('btn-secondary');\n selectPresetButton.classList.add('btn-primary');\n selectPresetButton.setAttribute('data-presetname', selectedRadioButton.getAttribute('value'));\n selectPresetButton.setAttribute('data-cmid', selectedRadioButton.getAttribute('data-cmid'));\n } else {\n // There is no any preset selected, so the button should be disabled.\n selectPresetButton.setAttribute('disabled', true);\n selectPresetButton.classList.remove('btn-primary');\n selectPresetButton.classList.add('btn-secondary');\n selectPresetButton.removeAttribute('data-presetname');\n selectPresetButton.removeAttribute('data-cmid');\n }\n};\n"],"names":["selectors","radioButton","document","querySelectorAll","disableUsePresetButton","forEach","elem","addEventListener","event","preventDefault","selectPresetButton","querySelector","selectedRadioButton","removeAttribute","classList","remove","add","setAttribute","getAttribute"],"mappings":";;;;;;;;MAuBMA,4BACiB,yBADjBA,6BAEkB,6BAFlBA,oCAGyB,+CAMX,WACVC,YAAcC,SAASC,iBAAiBH,6BAG9CI,yBAEAH,YAAYI,SAASC,OACjBA,KAAKC,iBAAiB,UAAU,SAASC,OACrCA,MAAMC,iBAENL,sCAaNA,uBAAyB,SACvBM,mBAAqBR,SAASS,cAAcX,oCAC1CY,oBAAsBV,SAASS,cAAcX,qCAE/CY,qBAEAF,mBAAmBG,gBAAgB,YACnCH,mBAAmBI,UAAUC,OAAO,iBACpCL,mBAAmBI,UAAUE,IAAI,eACjCN,mBAAmBO,aAAa,kBAAmBL,oBAAoBM,aAAa,UACpFR,mBAAmBO,aAAa,YAAaL,oBAAoBM,aAAa,gBAG9ER,mBAAmBO,aAAa,YAAY,GAC5CP,mBAAmBI,UAAUC,OAAO,eACpCL,mBAAmBI,UAAUE,IAAI,iBACjCN,mBAAmBG,gBAAgB,mBACnCH,mBAAmBG,gBAAgB"} \ No newline at end of file diff --git a/mod/data/amd/src/importmappingdialogue.js b/mod/data/amd/src/importmappingdialogue.js new file mode 100644 index 00000000000..a1112a6b48e --- /dev/null +++ b/mod/data/amd/src/importmappingdialogue.js @@ -0,0 +1,164 @@ +// 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 . + +/** + * Javascript module for deleting a database as a preset. + * + * @module mod_data/importmappingdialogue + * @copyright 2022 Amaia Anabitarte + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import Notification from 'core/notification'; +import Ajax from 'core/ajax'; +import Url from 'core/url'; +import Templates from 'core/templates'; +import ModalFactory from 'core/modal_factory'; +import {prefetchStrings} from 'core/prefetch'; +import {get_string as getString} from 'core/str'; + +// Load global strings. +prefetchStrings('mod_data', ['mapping:dialogtitle:usepreset']); + +const selectors = { + selectPresetButton: 'input[name="selectpreset"]', +}; + +/** + * Initialize module + */ +export const init = () => { + registerEventListeners(); +}; + +/** + * Register events. + */ +const registerEventListeners = () => { + document.addEventListener('click', (event) => { + const usepreset = event.target.closest(selectors.selectPresetButton); + if (usepreset) { + event.preventDefault(); + showMappingDialogue(usepreset); + } + }); +}; + +/** + * Show the confirmation modal for uploading a preset. + * + * @param {HTMLElement} usepreset the preset to import. + */ +const showMappingDialogue = (usepreset) => { + const presetName = usepreset.dataset.presetname; + const cmId = usepreset.dataset.cmid; + + getMappingInformation(cmId, presetName).then((result) => { + if (result.data && result.data.needsmapping) { + buildModal({ + title: getString('mapping:dialogtitle:usepreset', 'mod_data', result.data.presetname), + body: Templates.render('mod_data/fields_mapping_body', result.data), + footer: Templates.render('mod_data/fields_mapping_footer', getMappingButtons(cmId, presetName)), + large: true, + }); + } else { + window.location.href = Url.relativeUrl( + 'mod/data/field.php', + { + id: cmId, + mode: 'usepreset', + fullname: presetName, + }, + false + ); + } + return true; + }).catch(Notification.exception); +}; + +/** + * Given an object we want to build a modal ready to show + * + * @method buildModal + * @param {Object} params the modal params + * @param {Promise} params.title + * @param {Promise} params.body + * @param {Promise} params.footer + * @return {Object} The modal ready to display immediately and render body in later. + */ +const buildModal = (params) => { + return ModalFactory.create({ + ...params, + type: ModalFactory.types.DEFAULT, + }).then(modal => { + modal.show(); + modal.showFooter(); + modal.registerCloseOnCancel(); + return modal; + }).catch(Notification.exception); +}; + +/** + * Add buttons to render on mapping modal. + * + * @param {int} cmId The id of the current database activity. + * @param {string} presetName The preset name to delete. + * @return {array} Same data with buttons. + */ +const getMappingButtons = (cmId, presetName) => { + const data = {}; + + data.mapfieldsbutton = Url.relativeUrl( + 'mod/data/field.php', + { + id: cmId, + fullname: presetName, + mode: 'usepreset', + action: 'select', + }, + false + ); + + data.applybutton = Url.relativeUrl( + 'mod/data/field.php', + { + id: cmId, + fullname: presetName, + mode: 'usepreset', + action: 'notmapping' + }, + false + ); + + return data; +}; + +/** + * Check whether we should show the mapping dialogue or not. + * + * @param {int} cmId The id of the current database activity. + * @param {string} presetName The preset name to delete. + * @return {promise} Resolved with the result and warnings of deleting a preset. + */ +const getMappingInformation = (cmId, presetName) => { + const request = { + methodname: 'mod_data_get_mapping_information', + args: { + cmid: cmId, + importedpreset: presetName, + } + }; + return Ajax.call([request])[0]; +}; diff --git a/mod/data/amd/src/selectpreset.js b/mod/data/amd/src/selectpreset.js index 6fd881c2957..9dbe7d8a769 100644 --- a/mod/data/amd/src/selectpreset.js +++ b/mod/data/amd/src/selectpreset.js @@ -55,17 +55,21 @@ export const init = () => { */ const disableUsePresetButton = () => { let selectPresetButton = document.querySelector(selectors.selectPresetButton); - const selectedRadioButton = document.querySelectorAll(selectors.selectedPresetRadioButton); + const selectedRadioButton = document.querySelector(selectors.selectedPresetRadioButton); - if (selectedRadioButton.length > 0) { + if (selectedRadioButton) { // There is one preset selected, so the button should be enabled. selectPresetButton.removeAttribute('disabled'); selectPresetButton.classList.remove('btn-secondary'); selectPresetButton.classList.add('btn-primary'); + selectPresetButton.setAttribute('data-presetname', selectedRadioButton.getAttribute('value')); + selectPresetButton.setAttribute('data-cmid', selectedRadioButton.getAttribute('data-cmid')); } else { // There is no any preset selected, so the button should be disabled. selectPresetButton.setAttribute('disabled', true); selectPresetButton.classList.remove('btn-primary'); selectPresetButton.classList.add('btn-secondary'); + selectPresetButton.removeAttribute('data-presetname'); + selectPresetButton.removeAttribute('data-cmid'); } }; diff --git a/mod/data/classes/external/get_mapping_information.php b/mod/data/classes/external/get_mapping_information.php new file mode 100644 index 00000000000..0051456d760 --- /dev/null +++ b/mod/data/classes/external/get_mapping_information.php @@ -0,0 +1,97 @@ +. + +namespace mod_data\external; + +use core\notification; +use mod_data\local\importer\preset_importer; +use mod_data\manager; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->libdir . '/externallib.php'); + +/** + * This is the external method for deleting a saved preset. + * + * @package mod_data + * @since Moodle 4.1 + * @copyright 2022 Amaia Anabitarte + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class get_mapping_information extends \external_api { + /** + * Parameters. + * + * @return \external_function_parameters + */ + public static function execute_parameters(): \external_function_parameters { + return new \external_function_parameters([ + 'cmid' => new \external_value(PARAM_INT, 'Id of the data activity', VALUE_REQUIRED), + 'importedpreset' => new \external_value(PARAM_TEXT, 'Preset to be imported'), + ]); + } + + /** + * Get importing information for the given database course module. + * + * @param int $cmid Id of the course module where to import the preset. + * @param string $importedpreset Plugin or saved preset to be imported. + * @return array Information needed to decide whether to show the dialogue or not. + */ + public static function execute(int $cmid, string $importedpreset): array { + + $params = self::validate_parameters( + self::execute_parameters(), + ['cmid' => $cmid, 'importedpreset' => $importedpreset] + ); + + try { + // Let's get the manager. + list($course, $cm) = get_course_and_cm_from_cmid($params['cmid'], manager::MODULE); + $manager = manager::create_from_coursemodule($cm); + + $importer = preset_importer::create_from_plugin_or_directory($manager, $params['importedpreset']); + $result['data'] = $importer->get_mapping_information(); + } catch (\moodle_exception $e) { + $result['warnings'][] = [ + 'item' => $importedpreset, + 'warningcode' => 'exception', + 'message' => $e->getMessage() + ]; + notification::error($e->getMessage()); + } + return $result; + } + + /** + * Return. + * + * @return \external_single_structure + */ + public static function execute_returns(): \external_single_structure { + return new \external_single_structure([ + 'data' => new \external_single_structure([ + 'needsmapping' => new \external_value(PARAM_BOOL, 'Whether the importing needs mapping or not'), + 'presetname' => new \external_value(PARAM_TEXT, 'Name of the applied preset'), + 'fieldstocreate' => new \external_value(PARAM_TEXT, 'List of field names to create'), + 'fieldstoremove' => new \external_value(PARAM_TEXT, 'List of field names to remove'), + ], 'Information to import if everything went fine', VALUE_OPTIONAL), + 'warnings' => new \external_warnings(), + ]); + } +} diff --git a/mod/data/classes/local/importer/preset_importer.php b/mod/data/classes/local/importer/preset_importer.php index 5382f64b8d9..74b86c0f1ba 100644 --- a/mod/data/classes/local/importer/preset_importer.php +++ b/mod/data/classes/local/importer/preset_importer.php @@ -448,17 +448,64 @@ abstract class preset_importer { * @throws \moodle_exception when the file provided as parameter (POST or GET) does not exist */ public static function create_from_parameters(manager $manager): preset_importer { - global $CFG; + $fullname = optional_param('fullname', '', PARAM_PATH); // Directory the preset is in. if (!$fullname) { - $presetdir = $CFG->tempdir . '/forms/' . required_param('directory', PARAM_FILE); - if (!file_exists($presetdir) || !is_dir($presetdir)) { - throw new \moodle_exception('cannotimport'); - } - $importer = new preset_upload_importer($manager, $presetdir); - } else { - $importer = new preset_existing_importer($manager, $fullname); + $fullname = required_param('directory', PARAM_FILE); } - return $importer; + + return self::create_from_plugin_or_directory($manager, $fullname); + } + + /** + * Get the right importer instance from the provided parameters (POST or GET) + * + * @param manager $manager the current database manager + * @param string $pluginordirectory The plugin name or directory to create the importer from. + * @return preset_importer the relevant preset_importer instance + */ + public static function create_from_plugin_or_directory(manager $manager, string $pluginordirectory): preset_importer { + global $CFG; + + if (!$pluginordirectory) { + throw new \moodle_exception('emptypresetname', 'mod_data'); + } + try { + $presetdir = $CFG->tempdir . '/forms/' . $pluginordirectory; + if (file_exists($presetdir) && is_dir($presetdir)) { + return new preset_upload_importer($manager, $presetdir); + } else { + return new preset_existing_importer($manager, $pluginordirectory); + } + } catch (\moodle_exception $e) { + throw new \moodle_exception('errorpresetnotfound', 'mod_data', '', $pluginordirectory); + } + } + + /** + * Get the information needed to decide the modal + * + * @return array An array with all the information to decide the mapping + */ + public function get_mapping_information(): array { + return [ + 'needsmapping' => $this->needs_mapping(), + 'presetname' => preset::get_name_from_plugin($this->get_directory()), + 'fieldstocreate' => $this->get_field_names($this->fieldstocreate), + 'fieldstoremove' => $this->get_field_names($this->fieldstoremove), + ]; + } + + /** + * Returns a list of the fields + * + * @param array $fields Array of fields to get name from. + * @return string A string listing the names of the fields. + */ + public function get_field_names(array $fields): string { + $fieldnames = array_map(function($field) { + return $field->name; + }, $fields); + return implode(', ', $fieldnames); } } diff --git a/mod/data/classes/output/preset_preview.php b/mod/data/classes/output/preset_preview.php index a0f85a285c1..f35c10fd9e5 100644 --- a/mod/data/classes/output/preset_preview.php +++ b/mod/data/classes/output/preset_preview.php @@ -83,7 +83,7 @@ class preset_preview implements templatable, renderable { * @return array */ public function export_for_template(\renderer_base $output): array { - $instance = $this->manager->get_instance(); + $coursemodule = $this->manager->get_coursemodule(); $preset = $this->preset; // Get fields for preview. @@ -107,7 +107,7 @@ class preset_preview implements templatable, renderable { } return [ - 'd' => $instance->id, + 'cmid' => $coursemodule->id, 'description' => $preset->description ?? '', 'preview' => $content, 'formactionurl' => $useurl->out(), diff --git a/mod/data/classes/output/presets.php b/mod/data/classes/output/presets.php index d001fe82687..db55ad1b1ec 100644 --- a/mod/data/classes/output/presets.php +++ b/mod/data/classes/output/presets.php @@ -102,6 +102,7 @@ class presets implements templatable, renderable { $fullname = $preset->get_fullname(); $id = $this->manager->get_instance()->id; + $cmid = $this->manager->get_coursemodule()->id; $previewurl = new moodle_url( '/mod/data/preset.php', ['d' => $id, 'fullname' => $fullname, 'action' => 'preview'] @@ -109,6 +110,7 @@ class presets implements templatable, renderable { $presets[] = [ 'id' => $id, + 'cmid' => $cmid, 'name' => $preset->name, 'url' => $previewurl->out(), 'shortname' => $preset->shortname, diff --git a/mod/data/classes/preset.php b/mod/data/classes/preset.php index fdad67ef147..da69c5365c5 100644 --- a/mod/data/classes/preset.php +++ b/mod/data/classes/preset.php @@ -576,7 +576,8 @@ class preset { * @return string The plugin preset name to display. */ public static function get_name_from_plugin(string $pluginname): string { - if ($pos = strpos($pluginname, '/')) { + $pos = strpos($pluginname, '/'); + if ($pos !== false) { $pluginname = substr($pluginname, $pos + 1); } if (!strpos(trim($pluginname), ' ') && get_string_manager()->string_exists('modulename', 'datapreset_'.$pluginname)) { diff --git a/mod/data/db/services.php b/mod/data/db/services.php index e2784e62a2f..ac6ef13ef2a 100644 --- a/mod/data/db/services.php +++ b/mod/data/db/services.php @@ -122,4 +122,11 @@ $functions = array( 'ajax' => true, 'capabilities' => 'mod/data:manageuserpresets', ), + 'mod_data_get_mapping_information' => array( + 'classname' => 'mod_data\external\get_mapping_information', + 'description' => 'Get importing information', + 'type' => 'read', + 'ajax' => true, + 'capabilities' => 'mod/data:managetemplates', + ), ); diff --git a/mod/data/field.php b/mod/data/field.php index a357b7f0f35..71a0aad0e6b 100644 --- a/mod/data/field.php +++ b/mod/data/field.php @@ -255,7 +255,7 @@ switch ($mode) { case 'usepreset': $importer = preset_importer::create_from_parameters($manager); - if (!$importer->needs_mapping()) { + if (!$importer->needs_mapping() || $action == 'notmapping') { $backurl = new moodle_url('/mod/data/field.php', ['id' => $cm->id]); if ($importer->import(false)) { notification::success(get_string('importsuccess', 'mod_data')); diff --git a/mod/data/lang/en/data.php b/mod/data/lang/en/data.php index 9c1cd55f452..baab27eaaad 100644 --- a/mod/data/lang/en/data.php +++ b/mod/data/lang/en/data.php @@ -146,6 +146,7 @@ $string['eventrecordupdated'] = 'Record updated'; $string['eventtemplateupdated'] = 'Template updated'; $string['eventtemplateviewed'] = 'Templates viewed'; $string['fileencoding'] = 'Encoding'; +$string['emptypresetname'] = 'Preset name or file cannot be empty'; $string['entries'] = 'Entries'; $string['entrieslefttoadd'] = 'You must add {$a->entriesleft} more entry/entries in order to complete this activity'; $string['entrieslefttoaddtoview'] = 'You must add {$a->entrieslefttoview} more entry/entries before you can view other participants\' entries.'; @@ -155,6 +156,7 @@ $string['errormustbeteacher'] = 'You need to be a teacher to use this page!'; $string['errorpresetexists'] = 'A preset with this name already exists.'; $string['errorpresetexistsbutnotoverwrite'] = 'A preset with this name already exists. Choose a different name.'; $string['errormustsupplyvalue'] = 'You must supply a value here.'; +$string['errorpresetnotfound'] = 'Preset with name {$a} not found.'; $string['example'] = 'Database module example'; $string['excel'] = 'Excel'; $string['export'] = 'Export'; @@ -271,6 +273,13 @@ $string['manageapproved_help'] = 'If disabled, approved entries are no longer ed $string['mapexistingfield'] = 'Map to {$a}'; $string['mapnewfield'] = 'Create a new field'; $string['mappingwarning'] = 'All old fields not mapped to a new field will be lost and all data in that field will be removed.'; +$string['mapping:fieldstocreate'] = 'Fields to be created: {$a}'; +$string['mapping:fieldstodelete'] = 'Existing fields to be deleted: {$a}'; +$string['mapping:warningmessagedeleteandcreate'] = 'If fields to be deleted are of the same type as fields to be created, you may be able to map them in Map fields.'; +$string['mapping:warningmessagedelete'] = 'If fields to be deleted are of the same type as new fields in the preset you\'re applying, you may be able to map them in Map fields.'; +$string['mapping:mapfields'] = 'Map fields'; +$string['mapping:applypresets'] = 'Apply preset'; +$string['mapping:dialogtitle:usepreset'] = 'Apply preset {$a}?'; $string['maxentries'] = 'Maximum number of entries'; $string['maxentries_help'] = 'The maximum number of entries a student is allowed to submit for this activity.'; $string['maxsize'] = 'Maximum size'; diff --git a/mod/data/styles.css b/mod/data/styles.css index 3102d3d21c7..4640028ec66 100644 --- a/mod/data/styles.css +++ b/mod/data/styles.css @@ -198,6 +198,7 @@ .entriesactions .dropdown-toggle:hover, .entriesactions .show > .btn-secondary.dropdown-toggle, #page-mod-data-view .whitebutton .btn-secondary, +#page-mod-data-preset .whitebutton .btn-secondary, #page-mod-data-field- .whitebutton .btn-secondary, #page-mod-data-templates .whitebutton .btn-secondary { background: white; diff --git a/mod/data/templates/fields_mapping_body.mustache b/mod/data/templates/fields_mapping_body.mustache new file mode 100644 index 00000000000..4feb12e33cb --- /dev/null +++ b/mod/data/templates/fields_mapping_body.mustache @@ -0,0 +1,48 @@ +{{! + 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 . +}} +{{! + @template mod_data/field_mapping_modal + + Display a modal to decide on field mapping + + Example context (json): + { + "presetname": "Image gallery", + "fieldstocreate": "A, B, C", + "fieldstodelete": "A, B, C", + "cancelbutton": "", + "mapfieldsbutton": "", + "applybutton": "" + } +}} +
+ {{#fieldstocreate}} +
{{#str}}mapping:fieldstocreate, mod_data, {{.}}{{/str}}
+ {{/fieldstocreate}} + {{#fieldstoremove}} +
+ {{#str}}mapping:fieldstodelete, mod_data, {{.}}{{/str}} +
+ {{#fieldstocreate}} +
+ {{#str}}mapping:warningmessagedeleteandcreate, mod_data{{/str}} +
+ {{/fieldstocreate}} + {{^fieldstocreate}} +
+ {{#str}}mapping:warningmessagedelete, mod_data{{/str}} +
+ {{/fieldstocreate}} + {{/fieldstoremove}} +
diff --git a/mod/data/templates/fields_mapping_footer.mustache b/mod/data/templates/fields_mapping_footer.mustache new file mode 100644 index 00000000000..9db4b5c029a --- /dev/null +++ b/mod/data/templates/fields_mapping_footer.mustache @@ -0,0 +1,33 @@ +{{! + 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 . +}} +{{! + @template mod_data/field_mapping_modal + + Display a modal to decide on field mapping + + Example context (json): + { + "mapfieldsbutton": "", + "applybutton": "" + } +}} + + + {{#str}} mapping:mapfields, mod_data {{/str}} + + + {{#str}} mapping:applypresets, mod_data {{/str}} + diff --git a/mod/data/templates/preset_preview.mustache b/mod/data/templates/preset_preview.mustache index 4496d0cef16..6d6b0b24784 100644 --- a/mod/data/templates/preset_preview.mustache +++ b/mod/data/templates/preset_preview.mustache @@ -19,7 +19,7 @@ Example context (json): { "formactionurl": "#", - "d": "42", + "cmid": "42", "description": "Preset description", "preview": "

The preset preview HTML

", "userid": "0", @@ -34,11 +34,18 @@ {{< core/sticky_footer }} {{$ stickycontent }}
- + - +
{{/ stickycontent }} {{/ core/sticky_footer }} + +{{#js}} + require(['mod_data/importmappingdialogue'], function(importPreset) { + importPreset.init(); + }); +{{/js}} diff --git a/mod/data/templates/presets.mustache b/mod/data/templates/presets.mustache index 0a4b6956cfb..855a2a71459 100644 --- a/mod/data/templates/presets.mustache +++ b/mod/data/templates/presets.mustache @@ -28,6 +28,7 @@ "presets": [ { "id": 1, + "cmid": 1, "name": "Image gallery", "shortname": "imagegallery", "fullname": "Image gallery", @@ -38,6 +39,7 @@ }, { "id": 2, + "cmid": 2, "name": "Preset saved manually", "shortname": "Preset saved manually", "fullname": "Preset saved manually (admin)", @@ -68,7 +70,7 @@ {{#presets}} - + {{fullname}} @@ -104,8 +106,10 @@ selectPreset.init(); }); - require(['mod_data/editpreset'], function(editPreset) { editPreset.init(); }); + require(['mod_data/importmappingdialogue'], function(importPreset) { + importPreset.init(); + }); {{/js}} diff --git a/mod/data/tests/behat/preview_preset.feature b/mod/data/tests/behat/preview_preset.feature index 20a1a263852..293004c5d06 100644 --- a/mod/data/tests/behat/preview_preset.feature +++ b/mod/data/tests/behat/preview_preset.feature @@ -161,5 +161,5 @@ Feature: Users can preview presets When I follow "Presets" And I click on "Saved preset by teacher1" "link" And I click on "Use this preset" "button" - Then I should see "Field mappings" + Then I should see "The preset has been successfully applied" And I should see "My URL field" diff --git a/mod/data/tests/behat/use_presets.feature b/mod/data/tests/behat/use_presets.feature index 2451443c18a..c2074f40334 100644 --- a/mod/data/tests/behat/use_presets.feature +++ b/mod/data/tests/behat/use_presets.feature @@ -20,6 +20,7 @@ Feature: Users can use predefined presets And the following "mod_data > fields" exist: | database | type | name | description | | data1 | text | Test field name | Test field description | + Scenario: Using a preset on a non empty database could create new fields Given the following "mod_data > fields" exist: | database | type | name | @@ -32,10 +33,13 @@ Feature: Users can use predefined presets And I follow "Presets" And I click on "fullname" "radio" in the "Image gallery" "table_row" And I click on "Use this preset" "button" + And I should see "Apply preset Image gallery" + And I click on "Map fields" "button" + And I should see "Field mappings" And I click on "Continue" "button" And I should see "The preset has been successfully applied" And I click on "Continue" "button" - And I follow "Fields" + When I follow "Fields" Then I should see "title" And I should see "description" in the "description" "table_row" And I should see "image" in the "image" "table_row" @@ -53,6 +57,10 @@ Feature: Users can use predefined presets And I click on "fullname" "radio" in the "Image gallery" "table_row" And I click on "Use this preset" "button" # Let's map a field that is not mapped by default + And I should see "Apply preset Image gallery" + And I should see "Fields to be created: image, title, description" + And I should see "Existing fields to be deleted: Test field name, oldtitle" + When I click on "Map fields" "button" And I should see "Create a new field" in the "oldtitle" "table_row" And I set the field "id_title" to "Map to oldtitle" And I click on "Continue" "button" @@ -73,7 +81,9 @@ Feature: Users can use predefined presets And I follow "Presets" And I click on "fullname" "radio" in the "Image gallery" "table_row" And the "Use this preset" "button" should be enabled - Then I click on "Use this preset" "button" + When I click on "Use this preset" "button" + And I should see "Apply preset Image gallery" + And I click on "Map fields" "button" Then I should see "Field mappings" And I should see "title" And I should see "Create a new field" in the "title" "table_row" @@ -91,15 +101,15 @@ Feature: Users can use predefined presets And I follow "Presets" And I click on "fullname" "radio" in the "Image gallery" "table_row" When I click on "Use this preset" "button" - And I should see "Field mappings" + And I should see "Apply preset" + And I click on "Map fields" "button" And I set the field "id_title" to "Map to Test field name" And I click on "Continue" "button" And I should see "The preset has been successfully applied" - And I click on "Continue" "button" And I follow "Presets" And I click on "fullname" "radio" in the "Image gallery" "table_row" And I click on "Use this preset" "button" - Then I should not see "Field mappings" + Then I should not see "Apply preset Image gallery" And I should see "The preset has been successfully applied" Scenario: Using a preset from preset preview page on a non empty database could create new fields @@ -114,9 +124,9 @@ Feature: Users can use predefined presets And I follow "Presets" And I click on "Image gallery" "link" And I click on "Use this preset" "button" - And I click on "Continue" "button" + And I should see "Apply preset Image gallery" + When I click on "Apply preset" "button" And I should see "The preset has been successfully applied" - And I click on "Continue" "button" And I follow "Fields" Then I should see "title" And I should see "description" in the "description" "table_row" @@ -135,6 +145,10 @@ Feature: Users can use predefined presets And I click on "Image gallery" "link" And I click on "Use this preset" "button" # Let's map a field that is not mapped by default + And I should see "Apply preset Image gallery" + And I should see "Fields to be created: image, title, description" + And I should see "Existing fields to be deleted: Test field name, oldtitle" + When I click on "Map fields" "button" And I should see "Create a new field" in the "oldtitle" "table_row" And I set the field "id_title" to "Map to oldtitle" And I click on "Continue" "button" @@ -155,7 +169,9 @@ Feature: Users can use predefined presets And I follow "Presets" And I click on "Image gallery" "link" And the "Use this preset" "button" should be enabled - Then I click on "Use this preset" "button" + When I click on "Use this preset" "button" + And I should see "Apply preset Image gallery" + And I click on "Map fields" "button" Then I should see "Field mappings" And I should see "title" And I should see "Create a new field" in the "title" "table_row" @@ -174,6 +190,8 @@ Feature: Users can use predefined presets And I follow "Presets" And I click on "Image gallery" "link" When I click on "Use this preset" "button" + And I should see "Apply preset Image gallery" + And I click on "Map fields" "button" And I should see "Field mappings" And I set the field "id_title" to "Map to Test field name" And I click on "Continue" "button" @@ -184,3 +202,46 @@ Feature: Users can use predefined presets And I click on "Use this preset" "button" Then I should not see "Field mappings" And I should see "The preset has been successfully applied" + + Scenario: Apply preset dialogue should show helpful information to the user + Given the following "activities" exist: + | activity | name | intro | course | idnumber | + | data | Sea landscapes | introduction... | C1 | data2 | + And the following "mod_data > fields" exist: + | database | type | name | + | data2 | text | title | + And I am on the "Sea landscapes" "data activity" page logged in as teacher1 + And I follow "Presets" + And I click on "Image gallery" "link" + When I click on "Use this preset" "button" + And I should see "Apply preset Image gallery" + # Fields to be created only. + Then I should see "Fields to be created: image, description" + And I should not see "If fields to be deleted are of the same type as fields to be created" + And I should not see "If fields to be deleted are of the same type as new fields in the preset" + And I click on "Cancel" "button" in the "Apply preset Image gallery?" "dialogue" + And I follow "Presets" + And the following "mod_data > fields" exist: + | database | type | name | + | data2 | number | number | + And I click on "Image gallery" "link" + And I click on "Use this preset" "button" + And I should see "Apply preset Image gallery" + # Fields to be created and fields to be deleted. + And I should see "Fields to be created: image, description" + And I should see "Existing fields to be deleted: number" + And I should see "If fields to be deleted are of the same type as fields to be created" + And I should not see "If fields to be deleted are of the same type as new fields in the preset" + And I click on "Cancel" "button" in the "Apply preset Image gallery?" "dialogue" + And I follow "Presets" + And the following "mod_data > fields" exist: + | database | type | name | + | data2 | textarea | description | + | data2 | picture | image | + And I click on "Image gallery" "link" + And I click on "Use this preset" "button" + And I should see "Apply preset Image gallery" + # Fields to be deleted only. + And I should see "Existing fields to be deleted: number" + And I should not see "If fields to be deleted are of the same type as fields to be created" + And I should see "If fields to be deleted are of the same type as new fields in the preset" diff --git a/mod/data/tests/external/get_mapping_information_test.php b/mod/data/tests/external/get_mapping_information_test.php new file mode 100644 index 00000000000..cbadf5966b2 --- /dev/null +++ b/mod/data/tests/external/get_mapping_information_test.php @@ -0,0 +1,235 @@ +. + +namespace mod_data\external; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/webservice/tests/helpers.php'); + +use external_api; +use mod_data\manager; + +/** + * External function tests class for get_mapping_information. + * + * @package mod_data + * @category external + * @copyright 2022 Amaia Anabitarte + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass \mod_data\external\get_mapping_information + */ +class get_mapping_information_test extends \advanced_testcase { + + /** + * Data provider for test_get_mapping_information(). + * + * @return array[] + */ + public function get_mapping_information_provider(): array { + // Image gallery preset is: ['title' => 'text', 'description' => 'textarea', 'image' => 'picture']; + + $titlefield = new \stdClass(); + $titlefield->name = 'title'; + $titlefield->type = 'text'; + + $descfield = new \stdClass(); + $descfield->name = 'description'; + $descfield->type = 'textarea'; + + $imagefield = new \stdClass(); + $imagefield->name = 'image'; + $imagefield->type = 'picture'; + + $difffield = new \stdClass(); + $difffield->name = 'title'; + $difffield->type = 'textarea'; + + $newfield = new \stdClass(); + $newfield->name = 'number'; + $newfield->type = 'number'; + + return [ + 'Empty database / Empty importer' => [ + 'currentfields' => [], + 'newfields' => [], + 'pluginname' => '', + 'fieldstocreate' => '', + 'fieldstoremove' => '', + ], + 'Empty database / Importer with fields' => [ + 'currentfields' => [], + 'newfields' => [$imagefield, $titlefield, $descfield], + 'pluginname' => 'imagegallery', + 'fieldstocreate' => 'image, title, description', + 'fieldstoremove' => '', + ], + 'Database with fields / Empty importer' => [ + 'currentfields' => [$imagefield, $titlefield, $descfield], + 'newfields' => [], + 'pluginname' => '', + 'fieldstocreate' => '', + 'fieldstoremove' => 'image, title, description', + ], + 'Same fields' => [ + 'currentfields' => [$imagefield, $titlefield, $descfield], + 'newfields' => [$imagefield, $titlefield, $descfield], + 'pluginname' => 'imagegallery', + 'fieldstocreate' => '', + 'fieldstoremove' => '', + ], + 'Fields to create' => [ + 'currentfields' => [$titlefield, $descfield], + 'newfields' => [$imagefield, $titlefield, $descfield], + 'pluginname' => 'imagegallery', + 'fieldstocreate' => 'image', + 'fieldstoremove' => '', + ], + 'Fields to remove' => [ + 'currentfields' => [$imagefield, $titlefield, $descfield, $difffield], + 'newfields' => [$imagefield, $titlefield, $descfield], + 'pluginname' => 'imagegallery', + 'fieldstocreate' => '', + 'fieldstoremove' => 'title', + ], + 'Fields to update' => [ + 'currentfields' => [$imagefield, $difffield, $descfield], + 'newfields' => [$imagefield, $titlefield, $descfield], + 'pluginname' => 'imagegallery', + 'fieldstocreate' => 'title', + 'fieldstoremove' => 'title', + ], + 'Fields to create, remove and update' => [ + 'currentfields' => [$titlefield, $descfield, $imagefield, $difffield], + 'newfields' => [$titlefield, $descfield, $newfield], + 'pluginname' => '', + 'fieldstocreate' => 'number', + 'fieldstoremove' => 'image, title', + ], + ]; + } + + /** + * Test for get_mapping_information method. + * + * @dataProvider get_mapping_information_provider + * @covers ::execute + * + * @param array $currentfields Fields of the current activity. + * @param array $newfields Fields to be imported. + * @param string $pluginname The plugin preset to be imported. + * @param string $fieldstocreate Expected fields on $fieldstocreate. + * @param string $fieldstoremove Expected fields on $fieldstoremove. + */ + public function test_get_mapping_information( + array $currentfields, + array $newfields, + string $pluginname, + string $fieldstocreate, + string $fieldstoremove + ) { + global $USER; + + $this->resetAfterTest(); + $this->setAdminUser(); + $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); + + // Create a course and a database activity. + $course = $this->getDataGenerator()->create_course(); + $activity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); + // Add current fields to the activity. + foreach ($currentfields as $field) { + $plugingenerator->create_field($field, $activity); + } + $manager = manager::create_from_instance($activity); + $module = $manager->get_coursemodule(); + + $presetactivity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); + // Add current fields to the activity. + foreach ($newfields as $field) { + $plugingenerator->create_field($field, $presetactivity); + } + + $record = (object) [ + 'name' => 'Testing preset name', + 'description' => 'Testing preset description', + ]; + $saved = $plugingenerator->create_preset($presetactivity, $record); + + $result = get_mapping_information::execute($module->id, $USER->id . '/' . $saved->name); + $result = external_api::clean_returnvalue(get_mapping_information::execute_returns(), $result); + + $this->assertEquals($result['data']['fieldstocreate'], $fieldstocreate); + $this->assertEquals($result['data']['fieldstoremove'], $fieldstoremove); + + // Create presets and importers. + if ($pluginname) { + $result = get_mapping_information::execute($module->id, '/' . $pluginname);; + $result = external_api::clean_returnvalue(get_mapping_information::execute_returns(), $result); + $this->assertEquals($result['data']['fieldstoremove'], $fieldstoremove); + $this->assertEquals($result['data']['fieldstocreate'], $fieldstocreate); + } + } + + /** + * Test for get_mapping_information method for wrong presets. + * + * @covers ::execute + * + */ + public function test_get_mapping_information_for_wrong_preset() { + global $USER; + + $this->resetAfterTest(); + $this->setAdminUser(); + $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); + + // Create a course and a database activity. + $course = $this->getDataGenerator()->create_course(); + $activity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); + + $manager = manager::create_from_instance($activity); + $module = $manager->get_coursemodule(); + + // We get warnings with empty preset name. + $result = get_mapping_information::execute($module->id, ''); + $result = external_api::clean_returnvalue(get_mapping_information::execute_returns(), $result); + + $this->assertFalse(array_key_exists('data', $result)); + $this->assertTrue(array_key_exists('warnings', $result)); + + // We get warnings with non-existing preset name. + $result = get_mapping_information::execute($module->id, $USER->id . '/Non-existing'); + $result = external_api::clean_returnvalue(get_mapping_information::execute_returns(), $result); + + $this->assertFalse(array_key_exists('data', $result)); + $this->assertTrue(array_key_exists('warnings', $result)); + + $record = (object) [ + 'name' => 'Testing preset name', + 'description' => 'Testing preset description', + ]; + $saved = $plugingenerator->create_preset($activity, $record); + + // We get no warning with the right preset. + $result = get_mapping_information::execute($module->id, $USER->id . '/' . $saved->name); + $result = external_api::clean_returnvalue(get_mapping_information::execute_returns(), $result); + + $this->assertTrue(array_key_exists('data', $result)); + $this->assertFalse(array_key_exists('warnings', $result)); + } +} diff --git a/mod/data/tests/preset_importer_test.php b/mod/data/tests/preset_importer_test.php index 1b2cef1adae..34cefbb5084 100644 --- a/mod/data/tests/preset_importer_test.php +++ b/mod/data/tests/preset_importer_test.php @@ -17,6 +17,7 @@ namespace mod_data; use mod_data\local\importer\preset_existing_importer; +use mod_data\local\importer\preset_importer; /** * Preset importer tests class for mod_data. @@ -280,4 +281,179 @@ class preset_importer_test extends \advanced_testcase { $this->assertEquals(count($pluginimporter->fieldstoupdate), $fieldstoupdate); } } + + /** + * Test for get_mapping_information method. + * + * @dataProvider set_affected_provider + * @covers ::get_mapping_information + * + * @param array $currentfields Fields of the current activity. + * @param array $newfields Fields to be imported. + * @param string $pluginname The plugin preset to be imported. + * @param int $fieldstocreate Expected number of fields on $fieldstocreate. + * @param int $fieldstoremove Expected number of fields on $fieldstoremove. + * @param int $fieldstoupdate Expected number of fields on $fieldstoupdate. + */ + public function test_get_mapping_information( + array $currentfields, + array $newfields, + string $pluginname, + int $fieldstocreate, + int $fieldstoremove, + int $fieldstoupdate + ) { + global $USER; + + $this->resetAfterTest(); + $this->setAdminUser(); + $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); + + // Create a course and a database activity. + $course = $this->getDataGenerator()->create_course(); + $activity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); + // Add current fields to the activity. + foreach ($currentfields as $field) { + $plugingenerator->create_field($field, $activity); + } + $manager = manager::create_from_instance($activity); + + $presetactivity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); + // Add current fields to the activity. + foreach ($newfields as $field) { + $plugingenerator->create_field($field, $presetactivity); + } + + $record = (object) [ + 'name' => 'Testing preset name', + 'description' => 'Testing preset description', + ]; + $saved = $plugingenerator->create_preset($presetactivity, $record); + $savedimporter = new preset_existing_importer($manager, $USER->id . '/Testing preset name'); + $information = $savedimporter->get_mapping_information(); + $this->assertEquals($savedimporter->needs_mapping(), $information['needsmapping']); + $this->assertEquals(count($savedimporter->fieldstoremove), $fieldstoremove); + $this->assertEquals(count($savedimporter->fieldstocreate), $fieldstocreate); + $this->assertEquals(count($savedimporter->fieldstoupdate), $fieldstoupdate); + + // Create presets and importers. + if ($pluginname) { + $plugin = preset::create_from_plugin(null, $pluginname); + $pluginimporter = new preset_existing_importer($manager, '/' . $pluginname); + $information = $pluginimporter->get_mapping_information(); + $this->assertEquals($pluginimporter->needs_mapping(), $information['needsmapping']); + $this->assertEquals(count($pluginimporter->fieldstoremove), $fieldstoremove); + $this->assertEquals(count($pluginimporter->fieldstocreate), $fieldstocreate); + $this->assertEquals(count($pluginimporter->fieldstoupdate), $fieldstoupdate); + } + } + + /** + * Data provider for get_field_names(). + * + * @return array[] + */ + public function get_field_names_provider(): array { + return [ + 'Empty list' => [ + 'fields' => [], + 'expected' => '', + ], + 'List with one field' => [ + 'fields' => ['fieldname' => 'text'], + 'expected' => 'fieldname', + ], + 'List of fields with same type' => [ + 'fields' => ['textfield' => 'text', 'other' => 'text'], + 'expected' => 'textfield, other', + ], + 'List of fields with different type' => [ + 'fields' => ['textfield' => 'text', 'number' => 'number'], + 'expected' => 'textfield, number', + ], + ]; + } + + /** + * Test for get_field_names method. + * + * @dataProvider get_field_names_provider + * @covers ::get_field_names + * + * @param array $fields List of fields to get the names from. + * @param string $expected The list of field names expected. + */ + public function test_get_field_names(array $fields, string $expected) { + global $USER; + + $this->resetAfterTest(); + $this->setAdminUser(); + $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); + + // Create a course and a database activity. + $course = $this->getDataGenerator()->create_course(); + $presetactivity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); + foreach ($fields as $fieldname => $fieldtype) { + $newfield = new \stdClass(); + $newfield->name = $fieldname; + $newfield->type = $fieldtype; + + $createdfield = $plugingenerator->create_field($newfield, $presetactivity); + } + $manager = manager::create_from_instance($presetactivity); + + $record = (object) [ + 'name' => 'Testing preset name', + 'description' => 'Testing preset description', + ]; + $saved = $plugingenerator->create_preset($presetactivity, $record); + $savedimporter = new preset_existing_importer($manager, $USER->id . '/Testing preset name'); + $this->assertEquals($expected, $savedimporter->get_field_names($manager->get_field_records())); + } + + /** + * Test for create_from_plugin_or_directory creation static method. + * + * @covers ::create_from_plugin_or_directory + * + */ + public function test_create_from_plugin_or_directory() { + + global $USER; + + $this->resetAfterTest(); + $this->setAdminUser(); + $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); + + // Create a course and a database activity. + $course = $this->getDataGenerator()->create_course(); + $activity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); + $manager = manager::create_from_instance($activity); + + $presetactivity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); + + $record = (object) [ + 'name' => 'Testing preset name', + 'description' => 'Testing preset description', + ]; + $saved = $plugingenerator->create_preset($presetactivity, $record); + + // A plugin preset returns an instance of preset_existing_importer. + $preset = preset_importer::create_from_plugin_or_directory($manager, '/imagegallery'); + $this->assertInstanceOf('\mod_data\local\importer\preset_existing_importer', $preset); + + // A saved preset returns an instance of preset_existing_importer. + $preset = preset_importer::create_from_plugin_or_directory($manager, $USER->id . '/Testing preset name'); + $this->assertInstanceOf('\mod_data\local\importer\preset_existing_importer', $preset); + + // An empty preset name throws an exception. + $this->expectException('moodle_exception'); + try { + preset_importer::create_from_plugin_or_directory($manager, ''); + } finally { + // A non-existing preset name throws an exception. + $this->expectException('moodle_exception'); + preset_importer::create_from_plugin_or_directory($manager, $USER->id . '/Non-existing'); + } + } } diff --git a/mod/data/version.php b/mod/data/version.php index bfad973c615..d5702ede6d3 100644 --- a/mod/data/version.php +++ b/mod/data/version.php @@ -24,7 +24,7 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2022100600; // The current module version (Date: YYYYMMDDXX). +$plugin->version = 2022110600; // The current module version (Date: YYYYMMDDXX). $plugin->requires = 2022041200; // Requires this Moodle version. $plugin->component = 'mod_data'; // Full name of the plugin (used for diagnostics) $plugin->cron = 0;