From f850e4cace996d3c8bf974a88a218facc20bc497 Mon Sep 17 00:00:00 2001 From: Craig Williams Date: Fri, 4 Feb 2022 11:23:48 +0000 Subject: [PATCH] Create organisation functionality --- asset-bundles.json | 1 + assets/avsdev/js/pages/organisations.js | 3 + assets/avsdev/js/widgets/organisations.js | 61 +++++++ locale/en_US/messages.php | 12 ++ routes/organisations.php | 9 +- schema/requests/organisation/create.yaml | 26 +++ src/Controller/OrganisationController.php | 156 ++++++++++++++++++ .../Seeds/OrganisationPermissions.php | 7 + templates/forms/organisation.html.twig | 58 +++++++ templates/modals/organisation.html.twig | 7 + templates/pages/organisations.html.twig | 7 + 11 files changed, 346 insertions(+), 1 deletion(-) create mode 100644 assets/avsdev/js/widgets/organisations.js create mode 100644 schema/requests/organisation/create.yaml create mode 100644 templates/forms/organisation.html.twig create mode 100644 templates/modals/organisation.html.twig diff --git a/asset-bundles.json b/asset-bundles.json index 4b5daf6..25f4a1a 100644 --- a/asset-bundles.json +++ b/asset-bundles.json @@ -2,6 +2,7 @@ "bundle": { "js/pages/organisations": { "scripts": [ + "avsdev/js/widgets/organisations.js", "avsdev/js/pages/organisations.js" ] } diff --git a/assets/avsdev/js/pages/organisations.js b/assets/avsdev/js/pages/organisations.js index 0450bd5..afbbaef 100644 --- a/assets/avsdev/js/pages/organisations.js +++ b/assets/avsdev/js/pages/organisations.js @@ -13,4 +13,7 @@ $(document).ready(function() { dataUrl: site.uri.public + "/api/organisations", useLoadingTransition: site.uf_table.use_loading_transition }); + + // Bind creation button + bindOrganisationCreationButton($("#widget-organisations")); }); diff --git a/assets/avsdev/js/widgets/organisations.js b/assets/avsdev/js/widgets/organisations.js new file mode 100644 index 0000000..615595a --- /dev/null +++ b/assets/avsdev/js/widgets/organisations.js @@ -0,0 +1,61 @@ +/** + * Organisations widget. Sets up dropdowns, modals, etc for a table of organisations. + */ + +/** + * Set up the form in a modal after being successfully attached to the body. + */ +function attachOrganisationForm() { + $("body").on('renderSuccess.ufModal', function(data) { + var modal = $(this).ufModal('getModal'); + var form = modal.find('.js-form'); + + /** + * Set up modal widgets + */ + // Set up any widgets inside the modal + form.find(".js-select2").select2({ + width: '100%' + }); + + // Auto-generate slug + form.find('input[name=name]').on('input change', function() { + var manualSlug = form.find('#form-organisation-slug-override').prop('checked'); + if (!manualSlug) { + var slug = getSlug($(this).val()); + form.find('input[name=slug]').val(slug); + } + }); + + form.find('#form-organisation-slug-override').on('change', function() { + if ($(this).prop('checked')) { + form.find('input[name=slug]').prop('readonly', false); + } else { + form.find('input[name=slug]').prop('readonly', true); + form.find('input[name=name]').trigger('change'); + } + }); + + // Set up the form for submission + form.ufForm({ + validator: page.validators + }).on("submitSuccess.ufForm", function() { + // Reload page on success + window.location.reload(); + }); + }); +} + +function bindOrganisationCreationButton(el) { + // Link create button + el.find('.js-organisation-create').click(function(e) { + e.preventDefault(); + + $("body").ufModal({ + sourceUrl: site.uri.public + "/modals/organisations/create", + msgTarget: $("#alerts-page") + }); + + attachOrganisationForm(); + }); +}; diff --git a/locale/en_US/messages.php b/locale/en_US/messages.php index 9e6bc00..933b7a5 100644 --- a/locale/en_US/messages.php +++ b/locale/en_US/messages.php @@ -17,6 +17,18 @@ return [ 1 => 'Organisation', 2 => 'Organisations', + 'CREATE' => 'Create organisation', + 'CREATION_SUCCESSFUL' => 'Successfully created organisation {{name}}', 'PAGE_DESCRIPTION' => 'A listing of the organisations for your site. Provides management tools for editing and deleting organisations.', + + 'NAME' => [ + 1 => 'Organisation name', + + 'EXPLAIN' => 'Please enter a name for the organisation', + 'IN_USE' => 'Organisation name {{name}} is already in use.', + ], + 'SLUG' => [ + 'IN_USE' => 'Organisation slug {{slug}} is already in use.', + ], ], ]; diff --git a/routes/organisations.php b/routes/organisations.php index 7197159..06ec39d 100644 --- a/routes/organisations.php +++ b/routes/organisations.php @@ -19,6 +19,13 @@ $app->group('/organisations', function () { $app->group('/api/organisations', function () { $this->get('', 'UserFrosting\Sprinkle\Organisations\Controller\OrganisationController:getList'); + + $this->post('', 'UserFrosting\Sprinkle\Organisations\Controller\OrganisationController:create'); })->add('authGuard')->add(new NoCache()); -// TODO: add route for accepting members \ No newline at end of file +$app->group('/modals/organisations', function () { + $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 diff --git a/schema/requests/organisation/create.yaml b/schema/requests/organisation/create.yaml new file mode 100644 index 0000000..8004184 --- /dev/null +++ b/schema/requests/organisation/create.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 1170fea..eb18371 100644 --- a/src/Controller/OrganisationController.php +++ b/src/Controller/OrganisationController.php @@ -29,6 +29,102 @@ use UserFrosting\Support\Exception\NotFoundException; */ class OrganisationController extends SimpleController { + /** + * Processes the request to create a new organisation. + * + * Processes the request from the organisation creation form, checking that: + * 1. The organisation name and slug are not already in use; + * 2. The user has permission to create a new organisation; + * 3. The submitted data is valid. + * This route requires authentication (and should generally be limited to admins or the root user). + * + * Request type: POST + * + * @see getModalCreateOrganisation + * + * @param Request $request + * @param Response $response + * @param array $args + * + * @throws ForbiddenException If user is not authorized to access page + */ + public function create(Request $request, Response $response, $args) + { + // Get POST parameters: name, slug, icon, description + $params = $request->getParsedBody(); + + /** @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, 'create_organisation')) { + throw new ForbiddenException(); + } + + /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ + $ms = $this->ci->alerts; + + // Load the request schema + $schema = new RequestSchema('schema://requests/organisation/create.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; + } + + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + // Check if name or slug already exists + if ($classMapper->getClassMapping('organisation')::where('name', $data['name'])->first()) { + $ms->addMessageTranslated('danger', 'ORGANISATION.NAME.IN_USE', $data); + $error = true; + } + + if ($classMapper->getClassMapping('organisation')::where('slug', $data['slug'])->first()) { + $ms->addMessageTranslated('danger', 'ORGANISATION.SLUG.IN_USE', $data); + $error = true; + } + + if ($error) { + return $response->withJson([], 400); + } + + /** @var \UserFrosting\Support\Repository\Repository $config */ + $config = $this->ci->config; + + // All checks passed! log events/activities and create organisation + // Begin transaction - DB will be rolled back if an exception occurs + Capsule::transaction(function () use ($classMapper, $data, $ms, $currentUser) { + // Create the organisation + $organisation = $classMapper->createInstance('organisation', $data); + + // Store new organisation to database + $organisation->save(); + + // Create activity record + $this->ci->userActivityLogger->info("User {$currentUser->user_name} created organisation {$organisation->name}.", [ + 'type' => 'organisation_create', + 'user_id' => $currentUser->id, + ]); + + $ms->addMessageTranslated('success', 'ORGANISATION.CREATION_SUCCESSFUL', $data); + }); + + return $response->withJson([], 200); + } + /** * Returns a list of Organisations. * @@ -69,6 +165,66 @@ class OrganisationController extends SimpleController return $sprunje->toResponse($response); } + /** + * Renders the modal form for creating a new 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 ForbiddenException If user is not authorized to access page + */ + public function getModalCreate(Request $request, Response $response, $args) + { + /** @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 page + if (!$authorizer->checkAccess($currentUser, 'create_organisation')) { + throw new ForbiddenException(); + } + + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + // Create a dummy organisation to prepopulate fields + $organisation = $classMapper->createInstance('organisation', []); + + $fieldNames = ['name', 'slug', 'description']; + $fields = [ + 'hidden' => [], + 'disabled' => [], + ]; + + // Load validation rules + $schema = new RequestSchema('schema://requests/organisation/create.yaml'); + $validator = new JqueryValidationAdapter($schema, $this->ci->translator); + + return $this->ci->view->render($response, 'modals/organisation.html.twig', [ + 'organisation' => $organisation, + 'form' => [ + 'action' => 'api/organisations', + 'method' => 'POST', + 'fields' => $fields, + 'submit_text' => $translator->translate('CREATE'), + ], + 'page' => [ + 'validators' => $validator->rules('json', false), + ], + ]); + } + /** * Renders the organisation listing page. * diff --git a/src/Database/Seeds/OrganisationPermissions.php b/src/Database/Seeds/OrganisationPermissions.php index 1f20e64..1629087 100644 --- a/src/Database/Seeds/OrganisationPermissions.php +++ b/src/Database/Seeds/OrganisationPermissions.php @@ -43,6 +43,12 @@ class OrganisationPermissions extends BaseSeed protected function getPermissions() { return [ + 'create_organisation' => new Permission([ + 'slug' => 'create_organisation', + 'name' => 'Create organisation', + 'conditions' => 'always()', + 'description' => 'Create a new organisation.', + ]), 'uri_organisations' => new Permission([ 'slug' => 'uri_organisations', 'name' => 'Organisation management page', @@ -84,6 +90,7 @@ class OrganisationPermissions extends BaseSeed $roleSiteAdmin = Role::where('slug', 'site-admin')->first(); if ($roleSiteAdmin) { $roleSiteAdmin->permissions()->sync([ + $permissions['create_organisation'], $permissions['uri_organisations'], ], false); } diff --git a/templates/forms/organisation.html.twig b/templates/forms/organisation.html.twig new file mode 100644 index 0000000..ec48337 --- /dev/null +++ b/templates/forms/organisation.html.twig @@ -0,0 +1,58 @@ +
+ {% include "forms/csrf.html.twig" %} +
+
+
+ {% block organisation_form %} + {% if 'name' not in form.fields.hidden %} +
+
+ +
+ + +
+
+
+ {% endif %} + {% if 'slug' not in form.fields.hidden %} +
+
+ +
+ + + {% if 'slug' not in form.fields.disabled %} + + + + {% endif %} +
+
+
+ {% endif %} + {% if 'description' not in form.fields.hidden %} +
+
+ + +
+
+ {% endif %} + {% endblock %} +

+
+
+ +
+
+ +
+
+
+ + diff --git a/templates/modals/organisation.html.twig b/templates/modals/organisation.html.twig new file mode 100644 index 0000000..28588e1 --- /dev/null +++ b/templates/modals/organisation.html.twig @@ -0,0 +1,7 @@ +{% extends "modals/modal.html.twig" %} + +{% block modal_title %}{{translate('ORGANISATION')}}{% endblock %} + +{% block modal_body %} + {% include "forms/organisation.html.twig" %} +{% endblock %} diff --git a/templates/pages/organisations.html.twig b/templates/pages/organisations.html.twig index 4c018ce..8859213 100644 --- a/templates/pages/organisations.html.twig +++ b/templates/pages/organisations.html.twig @@ -26,6 +26,13 @@ } %} + {% if checkAccess('create_organisation') %} + + {% endif %}