From ae5dc809ec5740e0e98110628412f6ed7cccdb9d Mon Sep 17 00:00:00 2001 From: Amaia Anabitarte Date: Wed, 20 Jul 2022 16:54:43 +0200 Subject: [PATCH] MDL-75187 mod_data: Move delete preset to a modal Co-author: Ferran Recio --- mod/data/amd/build/deletepreset.min.js | 10 ++ mod/data/amd/build/deletepreset.min.js.map | 1 + mod/data/amd/src/deletepreset.js | 109 ++++++++++++++ .../classes/external/delete_saved_preset.php | 122 ++++++++++++++++ mod/data/classes/form/save_as_preset.php | 8 +- mod/data/classes/output/presets.php | 11 +- mod/data/classes/preset.php | 63 ++++++++ mod/data/db/services.php | 7 + mod/data/lang/en/data.php | 6 +- mod/data/lib.php | 17 +++ mod/data/preset.php | 80 ++++------- mod/data/tests/behat/data_presets.feature | 25 +++- .../external/delete_saved_preset_test.php | 135 ++++++++++++++++++ mod/data/tests/preset_test.php | 116 +++++++++++++++ mod/data/version.php | 2 +- 15 files changed, 646 insertions(+), 66 deletions(-) create mode 100644 mod/data/amd/build/deletepreset.min.js create mode 100644 mod/data/amd/build/deletepreset.min.js.map create mode 100644 mod/data/amd/src/deletepreset.js create mode 100644 mod/data/classes/external/delete_saved_preset.php create mode 100644 mod/data/tests/external/delete_saved_preset_test.php diff --git a/mod/data/amd/build/deletepreset.min.js b/mod/data/amd/build/deletepreset.min.js new file mode 100644 index 00000000000..3858c7223f4 --- /dev/null +++ b/mod/data/amd/build/deletepreset.min.js @@ -0,0 +1,10 @@ +define("mod_data/deletepreset",["exports","core/notification","core/prefetch","core/str","core/ajax","core/url"],(function(_exports,_notification,_prefetch,_str,_ajax,_url){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} +/** + * Javascript module for deleting a database as a preset. + * + * @module mod_data/deletepreset + * @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);const selectors_deletePresetButton='[data-action="deletepreset"]';_exports.init=()=>{(0,_prefetch.prefetchStrings)("mod_data",["deleteconfirm","deletewarning"]),(0,_prefetch.prefetchStrings)("core",["delete"]),registerEventListeners()};const registerEventListeners=()=>{document.addEventListener("click",(event=>{const deleteOption=event.target.closest(selectors_deletePresetButton);deleteOption&&(event.preventDefault(),deletePresetConfirm(deleteOption))}))},deletePresetConfirm=deleteOption=>{const presetName=deleteOption.getAttribute("data-presetname"),dataId=deleteOption.getAttribute("data-dataid");_notification.default.saveCancelPromise((0,_str.get_string)("deleteconfirm","mod_data",presetName),(0,_str.get_string)("deletewarning","mod_data"),(0,_str.get_string)("delete","core")).then((()=>async function(dataId,presetName){var request={methodname:"mod_data_delete_saved_preset",args:{dataid:dataId,presetnames:{presetname:presetName}}};try{await _ajax.default.call([request])[0],window.location.href=_url.default.relativeUrl("mod/data/preset.php",{d:dataId},!1)}catch(error){_notification.default.exception(error)}}(dataId,presetName))).catch((()=>{}))}})); + +//# sourceMappingURL=deletepreset.min.js.map \ No newline at end of file diff --git a/mod/data/amd/build/deletepreset.min.js.map b/mod/data/amd/build/deletepreset.min.js.map new file mode 100644 index 00000000000..e4b6b359ae3 --- /dev/null +++ b/mod/data/amd/build/deletepreset.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"deletepreset.min.js","sources":["../src/deletepreset.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/deletepreset\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 {prefetchStrings} from 'core/prefetch';\nimport {get_string as getString} from 'core/str';\nimport Ajax from 'core/ajax';\nimport Url from 'core/url';\n\nconst selectors = {\n deletePresetButton: '[data-action=\"deletepreset\"]',\n};\n\n/**\n * Initialize module\n */\nexport const init = () => {\n prefetchStrings('mod_data', [\n 'deleteconfirm',\n 'deletewarning',\n ]);\n prefetchStrings('core', [\n 'delete',\n ]);\n\n registerEventListeners();\n};\n\n/**\n * Register events for delete preset option in action menu.\n */\nconst registerEventListeners = () => {\n document.addEventListener('click', (event) => {\n const deleteOption = event.target.closest(selectors.deletePresetButton);\n if (deleteOption) {\n event.preventDefault();\n deletePresetConfirm(deleteOption);\n }\n });\n};\n\n/**\n * Show the confirmation modal to delete the preset.\n *\n * @param {HTMLElement} deleteOption the element to delete.\n */\nconst deletePresetConfirm = (deleteOption) => {\n const presetName = deleteOption.getAttribute('data-presetname');\n const dataId = deleteOption.getAttribute('data-dataid');\n\n Notification.saveCancelPromise(\n getString('deleteconfirm', 'mod_data', presetName),\n getString('deletewarning', 'mod_data'),\n getString('delete', 'core'),\n ).then(() => {\n return deletePreset(dataId, presetName);\n }).catch(() => {\n return;\n });\n};\n\n/**\n * Delete site user preset.\n *\n * @param {int} dataId 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 */\nasync function deletePreset(dataId, presetName) {\n var request = {\n methodname: 'mod_data_delete_saved_preset',\n args: {\n dataid: dataId,\n presetnames: {presetname: presetName},\n }\n };\n try {\n await Ajax.call([request])[0];\n window.location.href = Url.relativeUrl(\n 'mod/data/preset.php',\n {\n d: dataId,\n },\n false\n );\n } catch (error) {\n Notification.exception(error);\n }\n}\n"],"names":["selectors","registerEventListeners","document","addEventListener","event","deleteOption","target","closest","preventDefault","deletePresetConfirm","presetName","getAttribute","dataId","saveCancelPromise","then","request","methodname","args","dataid","presetnames","presetname","Ajax","call","window","location","href","Url","relativeUrl","d","error","exception","deletePreset","catch"],"mappings":";;;;;;;kNA6BMA,6BACkB,6CAMJ,mCACA,WAAY,CACxB,gBACA,gDAEY,OAAQ,CACpB,WAGJC,gCAMEA,uBAAyB,KAC3BC,SAASC,iBAAiB,SAAUC,cAC1BC,aAAeD,MAAME,OAAOC,QAAQP,8BACtCK,eACAD,MAAMI,iBACNC,oBAAoBJ,mBAU1BI,oBAAuBJ,qBACnBK,WAAaL,aAAaM,aAAa,mBACvCC,OAASP,aAAaM,aAAa,qCAE5BE,mBACT,mBAAU,gBAAiB,WAAYH,aACvC,mBAAU,gBAAiB,aAC3B,mBAAU,SAAU,SACtBI,MAAK,mBAciBF,OAAQF,gBAC5BK,QAAU,CACVC,WAAY,+BACZC,KAAM,CACFC,OAAQN,OACRO,YAAa,CAACC,WAAYV,wBAIxBW,cAAKC,KAAK,CAACP,UAAU,GAC3BQ,OAAOC,SAASC,KAAOC,aAAIC,YACvB,sBACA,CACIC,EAAGhB,SAEP,GAEN,MAAOiB,6BACQC,UAAUD,QA/BhBE,CAAanB,OAAQF,cAC7BsB,OAAM"} \ No newline at end of file diff --git a/mod/data/amd/src/deletepreset.js b/mod/data/amd/src/deletepreset.js new file mode 100644 index 00000000000..5e5d6586a3b --- /dev/null +++ b/mod/data/amd/src/deletepreset.js @@ -0,0 +1,109 @@ +// 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/deletepreset + * @copyright 2022 Amaia Anabitarte + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import Notification from 'core/notification'; +import {prefetchStrings} from 'core/prefetch'; +import {get_string as getString} from 'core/str'; +import Ajax from 'core/ajax'; +import Url from 'core/url'; + +const selectors = { + deletePresetButton: '[data-action="deletepreset"]', +}; + +/** + * Initialize module + */ +export const init = () => { + prefetchStrings('mod_data', [ + 'deleteconfirm', + 'deletewarning', + ]); + prefetchStrings('core', [ + 'delete', + ]); + + registerEventListeners(); +}; + +/** + * Register events for delete preset option in action menu. + */ +const registerEventListeners = () => { + document.addEventListener('click', (event) => { + const deleteOption = event.target.closest(selectors.deletePresetButton); + if (deleteOption) { + event.preventDefault(); + deletePresetConfirm(deleteOption); + } + }); +}; + +/** + * Show the confirmation modal to delete the preset. + * + * @param {HTMLElement} deleteOption the element to delete. + */ +const deletePresetConfirm = (deleteOption) => { + const presetName = deleteOption.getAttribute('data-presetname'); + const dataId = deleteOption.getAttribute('data-dataid'); + + Notification.saveCancelPromise( + getString('deleteconfirm', 'mod_data', presetName), + getString('deletewarning', 'mod_data'), + getString('delete', 'core'), + ).then(() => { + return deletePreset(dataId, presetName); + }).catch(() => { + return; + }); +}; + +/** + * Delete site user preset. + * + * @param {int} dataId 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. + */ +async function deletePreset(dataId, presetName) { + var request = { + methodname: 'mod_data_delete_saved_preset', + args: { + dataid: dataId, + presetnames: {presetname: presetName}, + } + }; + try { + await Ajax.call([request])[0]; + window.location.href = Url.relativeUrl( + 'mod/data/preset.php', + { + d: dataId, + }, + false + ); + } catch (error) { + Notification.exception(error); + } +} diff --git a/mod/data/classes/external/delete_saved_preset.php b/mod/data/classes/external/delete_saved_preset.php new file mode 100644 index 00000000000..e6a4e52e50a --- /dev/null +++ b/mod/data/classes/external/delete_saved_preset.php @@ -0,0 +1,122 @@ +. + +namespace mod_data\external; + +use core\notification; +use mod_data\manager; +use mod_data\preset; + +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 delete_saved_preset extends \external_api { + /** + * Parameters. + * + * @return \external_function_parameters + */ + public static function execute_parameters(): \external_function_parameters { + return new \external_function_parameters([ + 'dataid' => new \external_value(PARAM_INT, 'Id of the data activity', VALUE_REQUIRED), + 'presetnames' => new \external_multiple_structure( + new \external_value(PARAM_TEXT, 'The preset name to delete', VALUE_REQUIRED) + ) + ]); + } + + /** + * Delete saved preset from the file system. + * + * @param int $dataid Id of the data activity to check context and permissions. + * @param array $presetnames List of saved preset names to delete. + * @return array True if the content has been deleted; false and the warning, otherwise. + */ + public static function execute(int $dataid, array $presetnames): array { + global $DB; + + $result = false; + $warnings = []; + + $params = self::validate_parameters(self::execute_parameters(), ['dataid' => $dataid, 'presetnames' => $presetnames]); + + $instance = $DB->get_record('data', ['id' => $params['dataid']], '*', MUST_EXIST); + $manager = manager::create_from_instance($instance); + + foreach ($params['presetnames'] as $presetname) { + try { + $preset = preset::create_from_instance($manager, $presetname); + if ($preset->can_manage()) { + if ($preset->delete()) { + notification::success(get_string('presetdeleted', 'mod_data')); + $result = true; + } else { + // An error ocurred while deleting the preset. + $warnings[] = [ + 'item' => $presetname, + 'warningcode' => 'failedpresetdelete', + 'message' => get_string('failedpresetdelete', 'mod_data') + ]; + notification::error(get_string('failedpresetdelete', 'mod_data')); + } + } else { + // The user has no permission to delete the preset. + $warnings[] = [ + 'item' => $presetname, + 'warningcode' => 'cannotdeletepreset', + 'message' => get_string('cannotdeletepreset', 'mod_data') + ]; + notification::error(get_string('cannotdeletepreset', 'mod_data')); + } + } catch (\moodle_exception $e) { + // The saved preset has not been deleted. + $warnings[] = [ + 'item' => $presetname, + 'warningcode' => 'exception', + 'message' => $e->getMessage() + ]; + notification::error($e->getMessage()); + } + } + + 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() + ]); + } +} diff --git a/mod/data/classes/form/save_as_preset.php b/mod/data/classes/form/save_as_preset.php index 7cf56adbb11..526110c43a2 100644 --- a/mod/data/classes/form/save_as_preset.php +++ b/mod/data/classes/form/save_as_preset.php @@ -95,7 +95,7 @@ class save_as_preset extends dynamic_form { break; } } - if (isset($selectedpreset->name) && !data_user_can_delete_preset($context, $selectedpreset)) { + if (!$selectedpreset instanceof preset || !$selectedpreset->can_manage()) { $errors['name'] = get_string('cannotoverwritepreset', 'data'); } } else if ($formdata['action'] == 'saveaspreset' || $formdata['oldpresetname'] != $formdata['name']) { @@ -105,7 +105,7 @@ class save_as_preset extends dynamic_form { $usercandelete = false; foreach ($sitepresets as $preset) { if ($formdata['name'] == $preset->name) { - if (data_user_can_delete_preset($context, $preset)) { + if ($preset->can_manage()) { $errors['name'] = get_string('errorpresetexists', 'data'); $usercandelete = true; } else { @@ -176,8 +176,8 @@ class save_as_preset extends dynamic_form { break; } } - if (isset($selectedpreset->name) && data_user_can_delete_preset($context, $selectedpreset)) { - data_delete_site_preset($formdata->name); + if ($selectedpreset instanceof preset && $selectedpreset->can_manage()) { + $selectedpreset->delete(); } } $presetname = $formdata->name; diff --git a/mod/data/classes/output/presets.php b/mod/data/classes/output/presets.php index c148b619499..9b8ce3f91c4 100644 --- a/mod/data/classes/output/presets.php +++ b/mod/data/classes/output/presets.php @@ -136,7 +136,7 @@ class presets implements templatable, renderable { $actionmenu->attributes['class'] .= ' presets-actions'; // Only users with mod/data:manageuserpresets capability have options to edit the preset. - if (data_user_can_delete_preset($PAGE->context, $preset)) { + if ($preset->can_manage()) { $params = [ 'd' => $this->id, 'action' => 'edit', @@ -158,14 +158,19 @@ class presets implements templatable, renderable { // Delete. $params = [ 'd' => $this->id, - 'fullname' => "{$userid}/{$preset->shortname}", - 'action' => 'confirmdelete', + 'action' => 'delete', ]; $deleteactionurl = new moodle_url('/mod/data/preset.php', $params); + $attributes = [ + 'data-action' => 'deletepreset', + 'data-dataid' => $this->id, + "data-presetname" => $preset->name, + ]; $actionmenu->add(new action_menu_link_secondary( $deleteactionurl, null, get_string('delete'), + $attributes, )); } } diff --git a/mod/data/classes/preset.php b/mod/data/classes/preset.php index ab1a5c9bb44..18020b7a103 100644 --- a/mod/data/classes/preset.php +++ b/mod/data/classes/preset.php @@ -535,4 +535,67 @@ class preset { return $preset->asXML(); } + + /** + * Checks to see if the user has permission to manage the preset. + * + * @return bool Returns true if the user can manage this preset, false otherwise. + */ + public function can_manage(): bool { + global $USER; + + if ($this->isplugin) { + // Plugin presets can't be removed or edited. + return false; + } + + $context = $this->manager->get_context(); + if (has_capability('mod/data:manageuserpresets', $context)) { + return true; + } else { + if ($this->get_userid() == $USER->id) { + return true; + } + } + return false; + } + + /** + * Deletes all files related to a saved preset. + * + * @return bool True if the preset is a saved preset and the file exists in the file system; false otherwise. + */ + public function delete(): bool { + if ($this->isplugin) { + // Plugin presets can't be removed. + return false; + } + + $exists = false; + $filepath = $this->get_path(); + + $dir = self::get_file($filepath, '.'); + if (!empty($dir)) { + $exists = true; + + $fs = get_file_storage(); + $files = $fs->get_directory_files( + $dir->get_contextid(), + $dir->get_component(), + $dir->get_filearea(), + $dir->get_itemid(), + $filepath + ); + if (!empty($files)) { + foreach ($files as $file) { + $file->delete(); + } + } + $dir->delete(); + // Reseting storedfile property because the file has been removed. + $this->storedfile = null; + } + + return $exists; + } } diff --git a/mod/data/db/services.php b/mod/data/db/services.php index bf44f610404..e2784e62a2f 100644 --- a/mod/data/db/services.php +++ b/mod/data/db/services.php @@ -115,4 +115,11 @@ $functions = array( 'capabilities' => 'mod/data:writeentry', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), + 'mod_data_delete_saved_preset' => array( + 'classname' => 'mod_data\external\delete_saved_preset', + 'description' => 'Delete site user preset.', + 'type' => 'write', + 'ajax' => true, + 'capabilities' => 'mod/data:manageuserpresets', + ), ); diff --git a/mod/data/lang/en/data.php b/mod/data/lang/en/data.php index c61d4360faf..2fe50706bfe 100644 --- a/mod/data/lang/en/data.php +++ b/mod/data/lang/en/data.php @@ -115,11 +115,12 @@ $string['delcheck'] = 'Bulk delete checkbox'; $string['delete'] = 'Delete'; $string['deleteallentries'] = 'Delete all entries'; $string['deletecomment'] = 'Are you sure you want to delete this comment?'; +$string['deleteconfirm'] = 'Delete preset {$a}?'; $string['deleted'] = 'deleted'; $string['deleteentry'] = 'Delete entry'; $string['deletefield'] = 'Delete field'; $string['deletenotenrolled'] = 'Delete entries by users not enrolled'; -$string['deletewarning'] = 'Are you sure you want to delete this preset?'; +$string['deletewarning'] = 'Deleting a preset removes it from the list of available presets in all courses.'; $string['descending'] = 'Descending'; $string['directorynotapreset'] = '{$a->directory} Not a preset: missing files: {$a->missing_files}'; $string['disapprove'] = 'Undo approval'; @@ -163,7 +164,7 @@ $string['exportformat'] = 'Export format'; $string['exportoptions'] = 'Export options'; $string['exportownentries'] = 'Export your own entries only? ({$a->mine}/{$a->all})'; $string['exportpreset'] = 'Export preset'; -$string['failedpresetdelete'] = 'Error deleting a preset!'; +$string['failedpresetdelete'] = 'An error was encountered while trying to delete the preset.'; $string['fieldadded'] = 'Field added'; $string['fieldallowautolink'] = 'Allow autolink'; $string['fielddeleted'] = 'Field deleted'; @@ -321,6 +322,7 @@ $string['pleaseaddsome'] = 'Please create some below or choose a $string['pluginadministration'] = 'Database activity administration'; $string['pluginname'] = 'Database'; $string['portfolionotfile'] = 'Export to a portfolio rather than a file (csv and leap2a only)'; +$string['presetdeleted'] = 'Preset deleted.'; $string['presetinfo'] = 'Saving as a preset will publish this template. Other users may be able to use it in their databases.'; $string['presetnotselected'] = 'No preset has been selected.'; $string['presets'] = 'Presets'; diff --git a/mod/data/lib.php b/mod/data/lib.php index e184e152bc6..5651a12e018 100644 --- a/mod/data/lib.php +++ b/mod/data/lib.php @@ -2085,8 +2085,13 @@ function data_get_available_site_presets($context, array $presets=array()) { * * @param string $name * @return bool + * @deprecated since Moodle 4.1 MDL-75187 - please, use the preset::delete() function instead. + * @todo MDL-75189 This will be deleted in Moodle 4.5. + * @see preset::delete() */ function data_delete_site_preset($name) { + debugging('data_delete_site_preset() is deprecated. Please use preset::delete() instead.', DEBUG_DEVELOPER); + $fs = get_file_storage(); $files = $fs->get_directory_files(DATA_PRESET_CONTEXT, DATA_PRESET_COMPONENT, DATA_PRESET_FILEAREA, 0, '/'.$name.'/'); @@ -3852,10 +3857,22 @@ function data_get_advanced_search_sql($sort, $data, $recordids, $selectdata, $so * @param stdClass $context Context object. * @param stdClass $preset The preset object that we are checking for deletion. * @return bool Returns true if the user can delete, otherwise false. + * @deprecated since Moodle 4.1 MDL-75187 - please, use the preset::can_manage() function instead. + * @todo MDL-75189 This will be deleted in Moodle 4.5. + * @see preset::can_manage() */ function data_user_can_delete_preset($context, $preset) { global $USER; + debugging('data_user_can_delete_preset() is deprecated. Please use manager::can_manage() instead.', DEBUG_DEVELOPER); + + if ($context->contextlevel == CONTEXT_MODULE && isset($preset->name)) { + $cm = get_coursemodule_from_id('', $context->instanceid, 0, false, MUST_EXIST); + $manager = manager::create_from_coursemodule($cm); + $todelete = preset::create_from_instance($manager, $preset->name); + return $todelete->can_manage(); + } + if (has_capability('mod/data:manageuserpresets', $context)) { return true; } else { diff --git a/mod/data/preset.php b/mod/data/preset.php index 52b350f5e50..794941ad529 100644 --- a/mod/data/preset.php +++ b/mod/data/preset.php @@ -53,7 +53,7 @@ if ($id) { } $action = optional_param('action', 'view', PARAM_ALPHA); // The page action. -$allowedactions = ['view', 'delete', 'confirmdelete', 'import', 'importzip', 'finishimport', +$allowedactions = ['view', 'import', 'importzip', 'finishimport', 'export']; if (!in_array($action, $allowedactions)) { throw new moodle_exception('invalidaccess'); @@ -71,6 +71,7 @@ $PAGE->set_title(get_string('course') . ': ' . $course->fullname); $PAGE->set_heading($course->fullname); $PAGE->force_settings_menu(true); $PAGE->activityheader->disable(); +$PAGE->requires->js_call_amd('mod_data/deletepreset', 'init'); // fill in missing properties needed for updating of instance $data->course = $cm->course; @@ -124,7 +125,7 @@ if ($formdata = $formimportzip->get_data()) { exit(0); } -if (in_array($action, ['confirmdelete', 'delete', 'finishimport'])) { +if ($action === 'finishimport') { $fullname = optional_param('fullname', '' , PARAM_PATH); // The directory the preset is in. // Find out preset owner userid and shortname. $parts = explode('/', $fullname, 2); @@ -132,60 +133,29 @@ if (in_array($action, ['confirmdelete', 'delete', 'finishimport'])) { $shortname = empty($parts[1]) ? '' : $parts[1]; echo html_writer::start_div('overflow-hidden'); - if ($action === 'confirmdelete') { - $path = data_preset_path($course, $userid, $shortname); - $strwarning = get_string('deletewarning', 'data').'
'.$shortname; - $optionsyes = [ - 'fullname' => $fullname, - 'action' => 'delete', - 'd' => $data->id, - ]; - $optionsno = ['d' => $data->id]; - echo $OUTPUT->confirm($strwarning, new moodle_url('/mod/data/preset.php', $optionsyes), - new moodle_url('/mod/data/preset.php', $optionsno)); - echo $OUTPUT->footer(); - exit(0); - } else if ($action === 'delete') { - if (!confirm_sesskey()) { - throw new moodle_exception('invalidsesskey'); - } - $selectedpreset = new stdClass(); - foreach ($presets as $preset) { - if ($preset->shortname == $shortname) { - $selectedpreset = $preset; - } - } - if (!isset($selectedpreset->shortname) || !data_user_can_delete_preset($context, $selectedpreset)) { - throw new \moodle_exception('invalidrequest'); - } - - data_delete_site_preset($shortname); - $strdeleted = get_string('deleted', 'data'); - echo $OUTPUT->notification("$shortname $strdeleted", 'notifysuccess'); - } else if ($action === 'finishimport') { - if (!confirm_sesskey()) { - throw new moodle_exception('invalidsesskey'); - } - $overwritesettings = optional_param('overwritesettings', false, PARAM_BOOL); - 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 data_preset_upload_importer($course, $cm, $data, $presetdir); - } else { - $importer = new data_preset_existing_importer($course, $cm, $data, $fullname); - } - $importer->import($overwritesettings); - $strimportsuccess = get_string('importsuccess', 'data'); - $straddentries = get_string('addentries', 'data'); - $strtodatabase = get_string('todatabase', 'data'); - if (!$DB->get_records('data_records', array('dataid'=>$data->id))) { - echo $OUTPUT->notification("$strimportsuccess
$straddentries $strtodatabase", 'notifysuccess'); - } else { - echo $OUTPUT->notification("$strimportsuccess", 'notifysuccess'); - } + if (!confirm_sesskey()) { + throw new moodle_exception('invalidsesskey'); } + $overwritesettings = optional_param('overwritesettings', false, PARAM_BOOL); + 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 data_preset_upload_importer($course, $cm, $data, $presetdir); + } else { + $importer = new data_preset_existing_importer($course, $cm, $data, $fullname); + } + $importer->import($overwritesettings); + $strimportsuccess = get_string('importsuccess', 'data'); + $straddentries = get_string('addentries', 'data'); + $strtodatabase = get_string('todatabase', 'data'); + if (!$DB->get_records('data_records', ['dataid' => $data->id])) { + echo $OUTPUT->notification("$strimportsuccess $straddentries $strtodatabase", 'notifysuccess'); + } else { + echo $OUTPUT->notification("$strimportsuccess", 'notifysuccess'); + } + echo $OUTPUT->continue_button(new moodle_url('/mod/data/preset.php', ['d' => $data->id])); echo html_writer::end_div(); echo $OUTPUT->footer(); diff --git a/mod/data/tests/behat/data_presets.feature b/mod/data/tests/behat/data_presets.feature index 15b5a22c9b7..a70c936c6dc 100644 --- a/mod/data/tests/behat/data_presets.feature +++ b/mod/data/tests/behat/data_presets.feature @@ -169,7 +169,6 @@ Feature: Users can view and manage data presets And I click on "Save" "button" in the "Edit preset" "dialogue" Then I should not see "Preset saved." And I should see "Saved preset by teacher1" - And I should see "This preset has also a description" in the "Saved preset by teacher1" "table_row" @javascript Scenario: Teachers can edit presets and overwrite them if they are the authors @@ -237,3 +236,27 @@ Feature: Users can view and manage data presets And I should see "This preset has also a description" in the "Saved preset 1" "table_row" And I should not see "Saved preset by teacher1" And I should not see "The preset1 has description" + + @javascript + Scenario: Teachers can delete their own presets + Given the following "mod_data > fields" exist: + | database | type | name | description | + | data1 | text | Test field name | Test field description | + And the following "mod_data > presets" exist: + | database | name | description | user | + | data1 | Saved preset by teacher1 | My funny description goes here. | teacher1 | + And I am on the "Mountain landscapes" "data activity" page logged in as teacher1 + When I follow "Presets" + And I should see "Image gallery" + And I should see "Saved preset 1" + And I should see "Saved preset by teacher1" + # Plugin presets can't be removed. + And I should not see "Actions" in the "Image gallery" "table_row" + # The teacher should not be able to delete presets saved by others. + And I should not see "Actions" in the "Saved preset 1" "table_row" + # The teacher should be able to delete their own preset. + And I open the action menu in "Saved preset by teacher" "table_row" + And I follow "Delete" + And I click on "Delete" "button" in the "Delete preset Saved preset by teacher1?" "dialogue" + And I should see "Preset deleted" + And I should not see "Saved preset by teacher1" diff --git a/mod/data/tests/external/delete_saved_preset_test.php b/mod/data/tests/external/delete_saved_preset_test.php new file mode 100644 index 00000000000..e36ae5e0364 --- /dev/null +++ b/mod/data/tests/external/delete_saved_preset_test.php @@ -0,0 +1,135 @@ +. + +namespace mod_data\external; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/webservice/tests/helpers.php'); + +use externallib_advanced_testcase; +use external_api; +use mod_data\manager; + +/** + * External function test for delete_saved_preset. + * + * @package mod_data + * @category external + * @since Moodle 4.1 + * @copyright 2022 Amaia Anabitarte + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass \mod_data\external\delete_saved_preset + */ +class delete_saved_preset_test extends externallib_advanced_testcase { + + /** + * Test the behaviour of delete_saved_preset(). + * + * @covers ::execute + */ + public function test_delete_saved_preset() { + $this->resetAfterTest(); + + // Create course, database activity and users. + $course = $this->getDataGenerator()->create_course(); + $data = $this->getDataGenerator()->create_module('data', ['course' => $course->id]); + $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); + $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); + $manager = manager::create_from_instance($data); + $initialpresets = $manager->get_available_presets(); + + $preset1name = 'Admin preset'; + $preset2name = 'Teacher preset'; + + // Trying to delete a preset when there is no saved preset created. + $result = delete_saved_preset::execute($data->id, [$preset1name, $preset2name]); + $result = external_api::clean_returnvalue(delete_saved_preset::execute_returns(), $result); + $this->assertFalse($result['result']); + $this->assertCount(2, $result['warnings']); + // Check no preset has been deleted. + $currentpresets = $manager->get_available_presets(); + $this->assertEquals(count($initialpresets), count($currentpresets)); + + // Create a saved preset. + $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); + $record = (object)[ + 'name' => $preset1name, + 'description' => 'Testing preset description', + ]; + $adminpreset = $plugingenerator->create_preset($data, $record); + // Update initial preset list. + $initialpresets = $manager->get_available_presets(); + + // There is a warning for non-existing preset. + $result = delete_saved_preset::execute($data->id, ['Another preset']); + $result = external_api::clean_returnvalue(delete_saved_preset::execute_returns(), $result); + $this->assertFalse($result['result']); + $this->assertCount(1, $result['warnings']); + // Check no preset has been deleted. + $currentpresets = $manager->get_available_presets(); + $this->assertEquals(count($initialpresets), count($currentpresets)); + + // Create a saved preset by teacher. + $this->setUser($teacher); + $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); + $record = (object)[ + 'name' => $preset2name, + 'description' => 'Testing preset description', + ]; + $teacherpreset = $plugingenerator->create_preset($data, $record); + // Update initial preset list. + $this->setAdminUser(); + $initialpresets = $manager->get_available_presets(); + + // Student can't delete presets. + $this->setUser($student); + $result = delete_saved_preset::execute($data->id, [$preset1name, $preset2name]); + $result = external_api::clean_returnvalue(delete_saved_preset::execute_returns(), $result); + $this->assertFalse($result['result']); + $this->assertCount(2, $result['warnings']); + // Check no preset has been deleted. + $this->setAdminUser(); + $currentpresets = $manager->get_available_presets(); + $this->assertEquals(count($initialpresets), count($currentpresets)); + + // Teacher can delete their preset. + $this->setUser($teacher); + $result = delete_saved_preset::execute($data->id, [$preset2name]); + $result = external_api::clean_returnvalue(delete_saved_preset::execute_returns(), $result); + $this->assertTrue($result['result']); + $this->assertCount(0, $result['warnings']); + // Check the preset has been deleted. + $this->setAdminUser(); + $currentpresets = $manager->get_available_presets(); + $this->assertEquals(count($initialpresets) - 1, count($currentpresets)); + foreach ($currentpresets as $currentpreset) { + $this->assertNotEquals($currentpreset->name, $preset2name); + } + + // Teacher can't delete other users' preset. + $this->setUser($teacher); + $result = delete_saved_preset::execute($data->id, [$preset1name]); + $result = external_api::clean_returnvalue(delete_saved_preset::execute_returns(), $result); + $this->assertFalse($result['result']); + $this->assertCount(1, $result['warnings']); + // Check no preset has been deleted. + $this->setAdminUser(); + $currentpresets = $manager->get_available_presets(); + $this->assertEquals(count($initialpresets) - 1, count($currentpresets)); + } +} diff --git a/mod/data/tests/preset_test.php b/mod/data/tests/preset_test.php index 691de902dc2..5d2e031caf4 100644 --- a/mod/data/tests/preset_test.php +++ b/mod/data/tests/preset_test.php @@ -610,4 +610,120 @@ class preset_test extends \advanced_testcase { $file = preset::get_file($preset->get_path(), 'unexistingpreset.xml'); $this->assertNull($file); } + + /** + * Test for can_manage(). + * + * @covers ::can_manage + */ + public function test_can_manage() { + $this->resetAfterTest(); + + // Create course, database activity and users. + $course = $this->getDataGenerator()->create_course(); + $data = $this->getDataGenerator()->create_module('data', ['course' => $course->id]); + $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); + $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); + $manager = manager::create_from_instance($data); + + $preset1name = 'Admin preset'; + $preset2name = 'Teacher preset'; + + // Create a saved preset by admin. + $this->setAdminUser(); + $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); + $record = (object) [ + 'name' => $preset1name, + 'description' => 'Testing preset description', + ]; + $adminpreset = $plugingenerator->create_preset($data, $record); + + // Create a saved preset by teacher. + $this->setUser($teacher); + $record = (object) [ + 'name' => $preset2name, + 'description' => 'Testing preset description', + ]; + $teacherpreset = $plugingenerator->create_preset($data, $record); + + // Plugins can't be deleted. + $pluginpresets = manager::get_available_plugin_presets(); + $pluginpreset = reset($pluginpresets); + $this->assertFalse($pluginpreset->can_manage()); + + // Admin can delete all saved presets. + $this->setAdminUser(); + $this->assertTrue($adminpreset->can_manage()); + $this->assertTrue($teacherpreset->can_manage()); + + // Teacher can delete their own preset only. + $this->setUser($teacher); + $this->assertFalse($adminpreset->can_manage()); + $this->assertTrue($teacherpreset->can_manage()); + + // Student can't delete any of the presets. + $this->setUser($student); + $this->assertFalse($adminpreset->can_manage()); + $this->assertFalse($teacherpreset->can_manage()); + } + + /** + * Test for delete(). + * + * @covers ::delete + */ + public function test_delete() { + $this->resetAfterTest(); + + // Create course, database activity and users. + $course = $this->getDataGenerator()->create_course(); + $data = $this->getDataGenerator()->create_module('data', ['course' => $course->id]); + $manager = manager::create_from_instance($data); + $presetname = 'Admin preset'; + + // Create a saved preset by admin. + $this->setAdminUser(); + $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); + $record = (object) [ + 'name' => $presetname, + 'description' => 'Testing preset description', + ]; + $adminpreset = $plugingenerator->create_preset($data, $record); + $initialpresets = $manager->get_available_presets(); + + // Plugins can't be deleted. + $pluginpresets = manager::get_available_plugin_presets(); + $pluginpreset = reset($pluginpresets); + $result = $pluginpreset->delete(); + $currentpluginpresets = manager::get_available_plugin_presets(); + $this->assertEquals(count($pluginpresets), count($currentpluginpresets)); + + $result = $adminpreset->delete(); + $this->assertTrue($result); + + // After deleting the preset, there is no file linked. + $adminpreset = preset::create_from_instance($manager, $presetname); + $this->assertEmpty($adminpreset->storedfile); + + // Check the preset has been deleted. + $currentpresets = $manager->get_available_presets(); + $this->assertEquals(count($initialpresets) - 1, count($currentpresets)); + + // The behavior of trying to delete a preset twice. + $result = $adminpreset->delete(); + $this->assertFalse($result); + + // Check the preset has not been deleted. + $currentpresets = $manager->get_available_presets(); + $this->assertEquals(count($initialpresets) - 1, count($currentpresets)); + + $emptypreset = preset::create_from_instance($manager, $presetname); + // The behavior of deleting an empty preset. + $result = $emptypreset->delete(); + $this->assertFalse($result); + + // Check the preset has not been deleted. + $currentpresets = $manager->get_available_presets(); + $this->assertEquals(count($initialpresets) - 1, count($currentpresets)); + } } diff --git a/mod/data/version.php b/mod/data/version.php index 42a618aa0e2..e49fdbfad58 100644 --- a/mod/data/version.php +++ b/mod/data/version.php @@ -24,7 +24,7 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2022081600; // The current module version (Date: YYYYMMDDXX). +$plugin->version = 2022082601; // 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;