From 830a1b49a87f527a6b62e277c223c72f0ca16975 Mon Sep 17 00:00:00 2001 From: Craig Williams Date: Fri, 4 Feb 2022 12:36:47 +0000 Subject: [PATCH] Update organisation functionality --- assets/avsdev/js/widgets/organisations.js | 18 ++ locale/en_US/messages.php | 7 +- routes/organisations.php | 12 +- schema/requests/organisation/edit-info.yaml | 26 +++ src/Controller/OrganisationController.php | 197 ++++++++++++++++++ .../Seeds/OrganisationPermissions.php | 7 + templates/tables/organisations.html.twig | 5 + 7 files changed, 267 insertions(+), 5 deletions(-) create mode 100644 schema/requests/organisation/edit-info.yaml diff --git a/assets/avsdev/js/widgets/organisations.js b/assets/avsdev/js/widgets/organisations.js index 867fecd..84d50cf 100644 --- a/assets/avsdev/js/widgets/organisations.js +++ b/assets/avsdev/js/widgets/organisations.js @@ -58,6 +58,24 @@ function bindOrganisationButtons(el, options) { * Link row buttons after table is loaded. */ + /** + * Buttons that launch a modal dialog + */ + // Edit organisation details button + el.find('.js-organisation-edit').click(function(e) { + e.preventDefault(); + + $("body").ufModal({ + sourceUrl: site.uri.public + "/modals/organisations/edit", + ajaxParams: { + slug: $(this).data('slug') + }, + msgTarget: $("#alerts-page") + }); + + attachOrganisationForm(); + }); + // Delete organisation button el.find('.js-organisation-delete').click(function(e) { e.preventDefault(); diff --git a/locale/en_US/messages.php b/locale/en_US/messages.php index 605df4d..285352c 100644 --- a/locale/en_US/messages.php +++ b/locale/en_US/messages.php @@ -17,13 +17,18 @@ return [ 1 => 'Organisation', 2 => 'Organisations', + 'PAGE_DESCRIPTION' => 'A listing of the organisations for your site. Provides management tools for editing and deleting organisations.', + 'CREATE' => 'Create organisation', 'CREATION_SUCCESSFUL' => 'Successfully created organisation {{name}}', + + 'EDIT' => 'Edit organistion', + 'UPDATE' => 'Details updated for organistion {{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' => [ 1 => 'Organisation name', diff --git a/routes/organisations.php b/routes/organisations.php index aa5162f..5aa24b3 100644 --- a/routes/organisations.php +++ b/routes/organisations.php @@ -18,17 +18,21 @@ $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'); + + $this->put('/o/{slug}', 'UserFrosting\Sprinkle\Organisations\Controller\OrganisationController:update'); + + $this->delete('/o/{slug}', 'UserFrosting\Sprinkle\Organisations\Controller\OrganisationController:delete'); })->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'); + + $this->get('/edit', 'UserFrosting\Sprinkle\Organisations\Controller\OrganisationController:getModalEdit'); + + $this->get('/confirm-delete', 'UserFrosting\Sprinkle\Organisations\Controller\OrganisationController:getModalConfirmDelete'); })->add('authGuard')->add(new NoCache()); // TODO: add route for accepting members diff --git a/schema/requests/organisation/edit-info.yaml b/schema/requests/organisation/edit-info.yaml new file mode 100644 index 0000000..26b0bb5 --- /dev/null +++ b/schema/requests/organisation/edit-info.yaml @@ -0,0 +1,26 @@ +--- +name: + validators: + required: + label: "&NAME" + message: VALIDATE.REQUIRED + length: + label: "&NAME" + min: 1 + max: 255 + message: VALIDATE.LENGTH_RANGE + transformations: + - trim +slug: + validators: + required: + label: "&SLUG" + message: VALIDATE.REQUIRED + length: + label: "&SLUG" + min: 1 + max: 255 + message: VALIDATE.LENGTH_RANGE + transformations: + - trim +description: diff --git a/src/Controller/OrganisationController.php b/src/Controller/OrganisationController.php index 5ef88d3..99250dc 100644 --- a/src/Controller/OrganisationController.php +++ b/src/Controller/OrganisationController.php @@ -125,6 +125,128 @@ class OrganisationController extends SimpleController return $response->withJson([], 200); } + /** + * Processes the request to update an existing organisation's details. + * + * Processes the request from the organisation update form, checking that: + * 1. The organisation name/slug are not already in use; + * 2. The user has the necessary permissions to update the posted field(s); + * 3. The submitted data is valid. + * This route requires authentication (and should generally be limited to admins or the root user). + * + * Request type: PUT + * + * @see getModalOrganisationEdit + * + * @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 + */ + public function update(Request $request, Response $response, $args) + { + // Get the organisation based on slug in URL + $organisation = $this->getOrganisationFromParams($args); + + if (!$organisation) { + throw new NotFoundException(); + } + + // Get PUT parameters: (name, slug, icon, description) + $params = $request->getParsedBody(); + + /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ + $ms = $this->ci->alerts; + + // Load the request schema + $schema = new RequestSchema('schema://requests/organisation/edit-info.yaml'); + + // Whitelist and set parameter defaults + $transformer = new RequestDataTransformer($schema); + $data = $transformer->transform($params); + + $error = false; + + // Validate request data + $validator = new ServerSideValidator($schema, $this->ci->translator); + if (!$validator->validate($data)) { + $ms->addValidationErrors($validator); + $error = true; + } + + // Determine targeted fields + $fieldNames = []; + foreach ($data as $name => $value) { + $fieldNames[] = $name; + } + + /** @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 resource - check that currentUser has permission to edit submitted fields for this organisation + if (!$authorizer->checkAccess($currentUser, 'update_organisation_field', [ + 'organisation' => $organisation, + 'fields' => array_values(array_unique($fieldNames)), + ])) { + throw new ForbiddenException(); + } + + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + // Check if name or slug already exists + if ( + isset($data['name']) && + $data['name'] != $organisation->name && + $classMapper->getClassMapping('organisation')::where('name', $data['name'])->first() + ) { + $ms->addMessageTranslated('danger', 'ORGANISATION.NAME.IN_USE', $data); + $error = true; + } + + if ( + isset($data['slug']) && + $data['slug'] != $organisation->slug && + $classMapper->getClassMapping('organisation')::where('slug', $data['slug'])->first() + ) { + $ms->addMessageTranslated('danger', 'ORGANISATION.SLUG.IN_USE', $data); + $error = true; + } + + if ($error) { + return $response->withJson([], 400); + } + + // Begin transaction - DB will be rolled back if an exception occurs + Capsule::transaction(function () use ($data, $organisation, $currentUser) { + // Update the organisation and generate success messages + foreach ($data as $name => $value) { + if ($value != $organisation->$name) { + $organisation->$name = $value; + } + } + + $organisation->save(); + + // Create activity record + $this->ci->userActivityLogger->info("User {$currentUser->user_name} updated details for organisation {$organisation->name}.", [ + 'type' => 'organisation_update_info', + 'user_id' => $currentUser->id, + ]); + }); + + $ms->addMessageTranslated('success', 'ORGANISATION.UPDATE', [ + 'name' => $organisation->name, + ]); + + return $response->withJson([], 200); + } + /** * Processes the request to delete an existing organisation. * @@ -197,6 +319,7 @@ class OrganisationController extends SimpleController return $response->withJson([], 200); } + /** * Returns a list of Organisations. * @@ -344,6 +467,79 @@ class OrganisationController extends SimpleController ]); } + /** + * Renders the modal form for editing an existing organisation. + * + * This does NOT render a complete page. Instead, it renders the HTML for the modal, which can be embedded in other pages. + * This page requires authentication. + * + * Request type: GET + * + * @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 + */ + public function getModalEdit(Request $request, Response $response, $args) + { + // GET parameters + $params = $request->getQueryParams(); + + $organisation = $this->getOrganisationFromParams($params); + + // If the organisation doesn't exist, return 404 + if (!$organisation) { + throw new NotFoundException(); + } + + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + /** @var \UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var \UserFrosting\Sprinkle\Account\Database\Models\Interfaces\UserInterface $currentUser */ + $currentUser = $this->ci->currentUser; + + /** @var \UserFrosting\I18n\Translator $translator */ + $translator = $this->ci->translator; + + // Access-controlled resource - check that currentUser has permission to edit basic fields "name", "slug", "description" for this organisation + $fieldNames = ['name', 'slug', 'description']; + if (!$authorizer->checkAccess($currentUser, 'update_organisation_field', [ + 'organisation' => $organisation, + 'fields' => $fieldNames, + ])) { + throw new ForbiddenException(); + } + + // Generate form + $fields = [ + 'hidden' => [], + 'disabled' => [], + ]; + + // Load validation rules + $schema = new RequestSchema('schema://requests/organisation/edit-info.yaml'); + $validator = new JqueryValidationAdapter($schema, $translator); + + return $this->ci->view->render($response, 'modals/organisation.html.twig', [ + 'organisation' => $organisation, + 'form' => [ + 'action' => "api/organisations/o/{$organisation->slug}", + 'method' => 'PUT', + 'fields' => $fields, + 'submit_text' => $translator->translate('UPDATE'), + ], + 'page' => [ + 'validators' => $validator->rules('json', false), + ], + ]); + } + + /** * Renders the organisation listing page. * @@ -375,6 +571,7 @@ class OrganisationController extends SimpleController return $this->ci->view->render($response, 'pages/organisations.html.twig'); } + /** * Get organisation from params. * diff --git a/src/Database/Seeds/OrganisationPermissions.php b/src/Database/Seeds/OrganisationPermissions.php index a85476b..6659de1 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.', ]), + 'update_organisation_field' => new Permission([ + 'slug' => 'update_organisation_field', + 'name' => 'Edit organisation', + 'conditions' => 'always()', + 'description' => 'Edit basic properties of any organisation.', + ]), 'delete_organisation' => new Permission([ 'slug' => 'delete_organisation', 'name' => 'Delete organisation', @@ -97,6 +103,7 @@ class OrganisationPermissions extends BaseSeed if ($roleSiteAdmin) { $roleSiteAdmin->permissions()->sync([ $permissions['create_organisation'], + $permissions['update_organisation_field'], $permissions['delete_organisation'], $permissions['uri_organisations'], ], false); diff --git a/templates/tables/organisations.html.twig b/templates/tables/organisations.html.twig index 8f91b87..eb600c6 100644 --- a/templates/tables/organisations.html.twig +++ b/templates/tables/organisations.html.twig @@ -51,6 +51,11 @@