From 2cf2777494bfa7ce36704ba885f1f3159106aedb Mon Sep 17 00:00:00 2001 From: Craig Williams Date: Fri, 4 Feb 2022 11:42:43 +0000 Subject: [PATCH] Delete organisation functionality --- assets/avsdev/js/pages/organisations.js | 5 + assets/avsdev/js/widgets/organisations.js | 38 ++++ locale/en_US/messages.php | 4 + routes/organisations.php | 6 +- schema/requests/organisation/get-by-slug.yaml | 6 + src/Controller/OrganisationController.php | 165 +++++++++++++++++- .../Seeds/OrganisationPermissions.php | 7 + .../confirm-delete-organisation.html.twig | 17 ++ templates/tables/organisations.html.twig | 7 +- 9 files changed, 251 insertions(+), 4 deletions(-) create mode 100644 schema/requests/organisation/get-by-slug.yaml create mode 100644 templates/modals/confirm-delete-organisation.html.twig diff --git a/assets/avsdev/js/pages/organisations.js b/assets/avsdev/js/pages/organisations.js index afbbaef..b78fc9a 100644 --- a/assets/avsdev/js/pages/organisations.js +++ b/assets/avsdev/js/pages/organisations.js @@ -16,4 +16,9 @@ $(document).ready(function() { // Bind creation button bindOrganisationCreationButton($("#widget-organisations")); + + // Bind table buttons + $("#widget-organisations").on("pagerComplete.ufTable", function () { + bindOrganisationButtons($(this)); + }); }); diff --git a/assets/avsdev/js/widgets/organisations.js b/assets/avsdev/js/widgets/organisations.js index 615595a..867fecd 100644 --- a/assets/avsdev/js/widgets/organisations.js +++ b/assets/avsdev/js/widgets/organisations.js @@ -46,6 +46,44 @@ function attachOrganisationForm() { }); } +/** + * Link organisation action buttons, for example in a table or on a specific organisation's page. + * @param {module:jQuery} el jQuery wrapped element to target. + * @param {{delete_redirect: string}} options Options used to modify behaviour of button actions. + */ +function bindOrganisationButtons(el, options) { + if (!options) options = {}; + + /** + * Link row buttons after table is loaded. + */ + + // Delete organisation button + el.find('.js-organisation-delete').click(function(e) { + e.preventDefault(); + + $("body").ufModal({ + sourceUrl: site.uri.public + "/modals/organisations/confirm-delete", + ajaxParams: { + slug: $(this).data('slug') + }, + msgTarget: $("#alerts-page") + }); + + $("body").on('renderSuccess.ufModal', function() { + var modal = $(this).ufModal('getModal'); + var form = modal.find('.js-form'); + + form.ufForm() + .on("submitSuccess.ufForm", function() { + // Navigate or reload page on success + if (options.delete_redirect) window.location.href = options.delete_redirect; + else window.location.reload(); + }); + }); + }); +} + function bindOrganisationCreationButton(el) { // Link create button el.find('.js-organisation-create').click(function(e) { diff --git a/locale/en_US/messages.php b/locale/en_US/messages.php index 933b7a5..605df4d 100644 --- a/locale/en_US/messages.php +++ b/locale/en_US/messages.php @@ -19,6 +19,10 @@ return [ 'CREATE' => 'Create organisation', 'CREATION_SUCCESSFUL' => 'Successfully created organisation {{name}}', + 'DELETE' => 'Delete organisation', + 'DELETE_CONFIRM' => 'Are you sure you want to delete the organisation {{name}}?', + 'DELETE_YES' => 'Yes, delete organisation', + 'DELETION_SUCCESSFUL' => 'Successfully deleted organisation {{name}}', 'PAGE_DESCRIPTION' => 'A listing of the organisations for your site. Provides management tools for editing and deleting organisations.', 'NAME' => [ diff --git a/routes/organisations.php b/routes/organisations.php index 06ec39d..aa5162f 100644 --- a/routes/organisations.php +++ b/routes/organisations.php @@ -18,14 +18,18 @@ $app->group('/organisations', function () { })->add('authGuard')->add(new NoCache()); $app->group('/api/organisations', function () { + $this->delete('/o/{slug}', 'UserFrosting\Sprinkle\Organisations\Controller\OrganisationController:delete'); + $this->get('', 'UserFrosting\Sprinkle\Organisations\Controller\OrganisationController:getList'); $this->post('', 'UserFrosting\Sprinkle\Organisations\Controller\OrganisationController:create'); })->add('authGuard')->add(new NoCache()); $app->group('/modals/organisations', function () { + $this->get('/confirm-delete', 'UserFrosting\Sprinkle\Organisations\Controller\OrganisationController:getModalConfirmDelete'); + $this->get('/create', 'UserFrosting\Sprinkle\Organisations\Controller\OrganisationController:getModalCreate'); })->add('authGuard')->add(new NoCache()); // TODO: add route for accepting members -// TODO: add route for verifying organisations \ No newline at end of file +// TODO: add route for verifying organisations diff --git a/schema/requests/organisation/get-by-slug.yaml b/schema/requests/organisation/get-by-slug.yaml new file mode 100644 index 0000000..2aa41b5 --- /dev/null +++ b/schema/requests/organisation/get-by-slug.yaml @@ -0,0 +1,6 @@ +--- +slug: + validators: + required: + label: "&SLUG" + message: VALIDATE.REQUIRED diff --git a/src/Controller/OrganisationController.php b/src/Controller/OrganisationController.php index eb18371..5ef88d3 100644 --- a/src/Controller/OrganisationController.php +++ b/src/Controller/OrganisationController.php @@ -16,7 +16,7 @@ use UserFrosting\Fortress\Adapter\JqueryValidationAdapter; use UserFrosting\Fortress\RequestDataTransformer; use UserFrosting\Fortress\RequestSchema; use UserFrosting\Fortress\ServerSideValidator; -use UserFrosting\Sprinkle\Account\Database\Models\Group; +use UserFrosting\Sprinkle\Organisations\Database\Models\Organisation; use UserFrosting\Sprinkle\Core\Controller\SimpleController; use UserFrosting\Support\Exception\BadRequestException; use UserFrosting\Support\Exception\ForbiddenException; @@ -125,6 +125,78 @@ class OrganisationController extends SimpleController return $response->withJson([], 200); } + /** + * Processes the request to delete an existing organisation. + * + * Deletes the specified organisation. + * Organisations with members will retain the membership information. + * Before doing so, checks that: + * 1. The user has permission to delete this organisation; + * 2. The submitted data is valid. + * This route requires authentication (and should generally be limited to admins or the root user). + * + * Request type: DELETE + * + * @param Request $request + * @param Response $response + * @param array $args + * + * @throws NotFoundException If organisation is not found + * @throws ForbiddenException If user is not authorized to access page + * @throws BadRequestException + */ + public function delete(Request $request, Response $response, $args) + { + $organisation = $this->getOrganisationFromParams($args); + + // If the organisation doesn't exist, return 404 + if (!$organisation) { + throw new NotFoundException(); + } + + /** @var \UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var \UserFrosting\Sprinkle\Account\Database\Models\Interfaces\UserInterface $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'delete_organisation', [ + 'organisation' => $organisation, + ])) { + throw new ForbiddenException(); + } + + /** @var \UserFrosting\Support\Repository\Repository $config */ + $config = $this->ci->config; + + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + $organisationName = $organisation->name; + + // Begin transaction - DB will be rolled back if an exception occurs + Capsule::transaction(function () use ($organisation, $organisationName, $currentUser) { + $organisation->delete(); + unset($organisation); + + // Create activity record + $this->ci->userActivityLogger->info("User {$currentUser->user_name} deleted organisation {$organisationName}.", [ + 'type' => 'organisation_delete', + 'user_id' => $currentUser->id, + ]); + }); + + /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ + $ms = $this->ci->alerts; + + $ms->addMessageTranslated('success', 'ORGANISATION.DELETION_SUCCESSFUL', [ + 'name' => $organisationName, + ]); + + return $response->withJson([], 200); + } + /** * Returns a list of Organisations. * @@ -165,6 +237,53 @@ class OrganisationController extends SimpleController return $sprunje->toResponse($response); } + /** + * Get deletion confirmation modal. + * + * @param Request $request + * @param Response $response + * @param array $args + * + * @throws NotFoundException If organisation is not found + * @throws ForbiddenException If user is not authorized to access page + * @throws BadRequestException + */ + public function getModalConfirmDelete(Request $request, Response $response, $args) + { + // GET parameters + $params = $request->getQueryParams(); + + $organisation = $this->getOrganisationFromParams($params); + + // If the organisation no longer exists, forward to main organisation listing page + if (!$organisation) { + throw new NotFoundException(); + } + + /** @var \UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var \UserFrosting\Sprinkle\Account\Database\Models\Interfaces\UserInterface $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'delete_organisation', [ + 'organisation' => $organisation, + ])) { + throw new ForbiddenException(); + } + + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + return $this->ci->view->render($response, 'modals/confirm-delete-organisation.html.twig', [ + 'organisation' => $organisation, + 'form' => [ + 'action' => "api/organisations/o/{$organisation->slug}", + ], + ]); + } + /** * Renders the modal form for creating a new organisation. * @@ -228,7 +347,7 @@ class OrganisationController extends SimpleController /** * Renders the organisation listing page. * - * This page renders a table of groups, with dropdown menus for admin actions for each organisation. + * This page renders a table of organisations, with dropdown menus for admin actions for each organisation. * Actions typically include: edit organisation, delete organisation. * This page requires authentication. * @@ -255,4 +374,46 @@ class OrganisationController extends SimpleController return $this->ci->view->render($response, 'pages/organisations.html.twig'); } + + /** + * Get organisation from params. + * + * @param array $params + * + * @throws BadRequestException + * + * @return Organisation + */ + protected function getOrganisationFromParams($params) + { + // Load the request schema + $schema = new RequestSchema('schema://requests/organisation/get-by-slug.yaml'); + + // Whitelist and set parameter defaults + $transformer = new RequestDataTransformer($schema); + $data = $transformer->transform($params); + + // Validate, and throw exception on validation errors. + $validator = new ServerSideValidator($schema, $this->ci->translator); + if (!$validator->validate($data)) { + // TODO: encapsulate the communication of error messages from ServerSideValidator to the BadRequestException + $e = new BadRequestException(); + foreach ($validator->errors() as $idx => $field) { + foreach ($field as $eidx => $error) { + $e->addUserMessage($error); + } + } + + throw $e; + } + + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + // Get the organisation + $organisation = $classMapper->getClassMapping('organisation')::where('slug', $data['slug']) + ->first(); + + return $organisation; + } } diff --git a/src/Database/Seeds/OrganisationPermissions.php b/src/Database/Seeds/OrganisationPermissions.php index 1629087..a85476b 100644 --- a/src/Database/Seeds/OrganisationPermissions.php +++ b/src/Database/Seeds/OrganisationPermissions.php @@ -49,6 +49,12 @@ class OrganisationPermissions extends BaseSeed 'conditions' => 'always()', 'description' => 'Create a new organisation.', ]), + 'delete_organisation' => new Permission([ + 'slug' => 'delete_organisation', + 'name' => 'Delete organisation', + 'conditions' => 'always()', + 'description' => 'Delete an organisation.', + ]), 'uri_organisations' => new Permission([ 'slug' => 'uri_organisations', 'name' => 'Organisation management page', @@ -91,6 +97,7 @@ class OrganisationPermissions extends BaseSeed if ($roleSiteAdmin) { $roleSiteAdmin->permissions()->sync([ $permissions['create_organisation'], + $permissions['delete_organisation'], $permissions['uri_organisations'], ], false); } diff --git a/templates/modals/confirm-delete-organisation.html.twig b/templates/modals/confirm-delete-organisation.html.twig new file mode 100644 index 0000000..f255e3c --- /dev/null +++ b/templates/modals/confirm-delete-organisation.html.twig @@ -0,0 +1,17 @@ +{% extends "modals/modal.html.twig" %} + +{% block modal_title %}{{translate("ORGANISATION.DELETE")}}{% endblock %} + +{% block modal_body %} +
+ {% include "forms/csrf.html.twig" %} +
+
+

{{translate("ORGANISATION.DELETE_CONFIRM", {name: organisation.name})}}
{{translate("DELETE_CANNOT_UNDONE")}}

+
+
+ + +
+
+{% endblock %} diff --git a/templates/tables/organisations.html.twig b/templates/tables/organisations.html.twig index 9c2453c..8f91b87 100644 --- a/templates/tables/organisations.html.twig +++ b/templates/tables/organisations.html.twig @@ -45,12 +45,17 @@