diff --git a/asset-bundles.json b/asset-bundles.json index 25f4a1a..eca6eaf 100644 --- a/asset-bundles.json +++ b/asset-bundles.json @@ -1,5 +1,12 @@ { "bundle": { + "js/pages/organisation": { + "scripts": [ + "userfrosting/js/widgets/users.js", + "avsdev/js/widgets/organisations.js", + "avsdev/js/pages/organisation.js" + ] + }, "js/pages/organisations": { "scripts": [ "avsdev/js/widgets/organisations.js", diff --git a/assets/avsdev/js/pages/organisation.js b/assets/avsdev/js/pages/organisation.js new file mode 100644 index 0000000..d04ccfa --- /dev/null +++ b/assets/avsdev/js/pages/organisation.js @@ -0,0 +1,24 @@ +/** + * Page-specific Javascript file. Should generally be included as a separate asset bundle in your page template. + * example: {{ assets.js('js/pages/sign-in-or-register') | raw }} + * + * This script depends on uf-table.js, moment.js, handlebars-helpers.js, widgets/users.js + * + * Target page: /organisations/o/{slug} + */ + +$(document).ready(function() { + // Control buttons + bindOrganisationButtons($("#view-organisation"), { delete_redirect: page.delete_redirect }); + + // Table of users in this organisation + $("#widget-organisation-members").ufTable({ + dataUrl: site.uri.public + '/api/organisations/o/' + page.organisation_slug + '/members', + useLoadingTransition: site.uf_table.use_loading_transition + }); + + // Bind user table buttons + $("#widget-organisation-members").on("pagerComplete.ufTable", function () { + bindUserButtons($(this)); + }); +}); diff --git a/locale/en_US/messages.php b/locale/en_US/messages.php index 6f34695..85e65b2 100644 --- a/locale/en_US/messages.php +++ b/locale/en_US/messages.php @@ -18,6 +18,8 @@ return [ 2 => 'Organisations', 'PAGE_DESCRIPTION' => 'A listing of the organisations for your site. Provides management tools for editing and deleting organisations.', + 'INFO_PAGE' => 'Organisation information page for {{name}}', + 'SUMMARY' => 'Organisation Summary', 'CREATE' => 'Create organisation', 'CREATION_SUCCESSFUL' => 'Successfully created organisation {{name}}', @@ -43,4 +45,14 @@ return [ 'IN_USE' => 'Organisation slug {{slug}} is already in use.', ], ], + + 'MEMBER' => [ + 1 => 'Member', + 2 => 'Members', + ], + + 'ADMIN' => [ + 1 => 'Administrator', + 2 => 'Administrators', + ], ]; diff --git a/routes/organisations.php b/routes/organisations.php index 5aa24b3..ffbef11 100644 --- a/routes/organisations.php +++ b/routes/organisations.php @@ -15,16 +15,25 @@ use UserFrosting\Sprinkle\Core\Util\NoCache; $app->group('/organisations', function () { $this->get('', 'UserFrosting\Sprinkle\Organisations\Controller\OrganisationController:pageList') ->setName('uri_organisations'); + + $this->get('/o/{slug}', 'UserFrosting\Sprinkle\Organisations\Controller\OrganisationController:pageInfo') + ->setName('uri_organisations'); })->add('authGuard')->add(new NoCache()); $app->group('/api/organisations', function () { $this->get('', 'UserFrosting\Sprinkle\Organisations\Controller\OrganisationController:getList'); + $this->post('', 'UserFrosting\Sprinkle\Organisations\Controller\OrganisationController:create'); + $this->get('/o/{slug}', 'UserFrosting\Sprinkle\Organisations\Controller\OrganisationController:getInfo'); + $this->put('/o/{slug}', 'UserFrosting\Sprinkle\Organisations\Controller\OrganisationController:update'); - + $this->delete('/o/{slug}', 'UserFrosting\Sprinkle\Organisations\Controller\OrganisationController:delete'); + + + $this->get('/o/{slug}/members', 'UserFrosting\Sprinkle\Organisations\Controller\OrganisationController:getMembers'); })->add('authGuard')->add(new NoCache()); $app->group('/modals/organisations', function () { diff --git a/src/Controller/OrganisationController.php b/src/Controller/OrganisationController.php index 99250dc..a8a45ad 100644 --- a/src/Controller/OrganisationController.php +++ b/src/Controller/OrganisationController.php @@ -125,6 +125,57 @@ class OrganisationController extends SimpleController return $response->withJson([], 200); } + /** + * Returns info for a single organisation. + * + * This page requires authentication. + * Request type: GET + * + * @param Request $request + * @param Response $response + * @param string[] $args + * + * @throws NotFoundException If organisation is not found + * @throws ForbiddenException If user is not authorized to access page + */ + public function getInfo(Request $request, Response $response, array $args) + { + $organisation = $this->getOrganisationFromParams($args); + + // If the organisation doesn't exist, return 404 + if (!$organisation) { + throw new NotFoundException(); + } + + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + // Join organisation's most recent activity + $organisation = $classMapper->createInstance('organisation') + ->where('slug', $organisation->slug) + ->joinMemberCounts() + ->first(); + + /** @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, 'uri_organisation', [ + 'organisation' => $organisation, + ])) { + throw new ForbiddenException(); + } + + $result = $organisation->toArray(); + + // Be careful how you consume this data - it has not been escaped and contains untrusted user-supplied content. + // For example, if you plan to insert it into an HTML DOM, you must escape it on the client side (or use client-side templating). + return $response->withJson($result, 200, JSON_PRETTY_PRINT); + } + /** * Processes the request to update an existing organisation's details. * @@ -539,6 +590,139 @@ class OrganisationController extends SimpleController ]); } + /** + * Members List API. + * + * @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 getMembers(Request $request, Response $response, $args) + { + $organisation = $this->getOrganisationFromParams($args); + + // If the organisation no longer exists, forward to main organisation listing page + if (!$organisation) { + throw new NotFoundException(); + } + + // GET parameters + $params = $request->getQueryParams(); + + /** @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, 'view_organisation_field', [ + 'organisation' => $organisation, + 'property' => 'members', + ])) { + throw new ForbiddenException(); + } + + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + $sprunje = $classMapper->createInstance('user_sprunje', $classMapper, $params); + $sprunje->extendQuery(function ($query) use ($classMapper, $organisation) { + return $query + ->join('organisation_members', function($join) use($organisation) { + $join->on('organisation_members.user_id', '=', 'users.id')->where('organisation_id', $organisation->id); + }); + }); + + // Be careful how you consume this data - it has not been escaped and contains untrusted user-supplied content. + // For example, if you plan to insert it into an HTML DOM, you must escape it on the client side (or use client-side templating). + return $sprunje->toResponse($response); + } + + + /** + * Renders a page displaying a organisation's information, in read-only mode. + * + * This checks that the currently logged-in user has permission to view the requested organisation's info. + * It checks each field individually, showing only those that you have permission to view. + * This will also try to show buttons for deleting, and editing the organisation. + * 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 pageInfo(Request $request, Response $response, $args) + { + $organisation = $this->getOrganisationFromParams($args); + + // 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, 'uri_organisation', [ + 'organisation' => $organisation, + ])) { + throw new ForbiddenException(); + } + + // Determine fields that currentUser is authorized to view + $fieldNames = ['name', 'slug', 'description']; + + // Generate form + $fields = [ + 'hidden' => [], + ]; + + foreach ($fieldNames as $field) { + if (!$authorizer->checkAccess($currentUser, 'view_organisation_field', [ + 'organisation' => $organisation, + 'property' => $field, + ])) { + $fields['hidden'][] = $field; + } + } + + // Determine buttons to display + $editButtons = [ + 'hidden' => [], + ]; + + if (!$authorizer->checkAccess($currentUser, 'update_organisation_field', [ + 'organisation' => $organisation, + 'fields' => ['name', 'slug', 'description'], + ])) { + $editButtons['hidden'][] = 'edit'; + } + + if (!$authorizer->checkAccess($currentUser, 'delete_organisation', [ + 'organisation' => $organisation, + ])) { + $editButtons['hidden'][] = 'delete'; + } + + return $this->ci->view->render($response, 'pages/organisation.html.twig', [ + 'organisation' => $organisation, + 'fields' => $fields, + 'tools' => $editButtons, + 'delete_redirect' => $this->ci->router->pathFor('uri_organisations'), + ]); + } /** * Renders the organisation listing page. diff --git a/src/Database/Models/Organisation.php b/src/Database/Models/Organisation.php index 3ed5f41..62faa80 100644 --- a/src/Database/Models/Organisation.php +++ b/src/Database/Models/Organisation.php @@ -148,19 +148,37 @@ class Organisation extends Model */ public function scopeJoinMemberCounts($query) { - $memberCounts = DB::table('organisation_members') - ->selectRaw('organisation_id, count(*) as member_count') + $memberCountsInner = DB::table('organisation_members') + ->selectRaw('organisation_id, COUNT(*) AS member_count') ->groupBy('organisation_id'); + $memberCounts = DB::table('organisations') + ->leftJoinSub($memberCountsInner, 'member_counts_inner', function ($join) { + $join->on('member_counts_inner.organisation_id', '=', 'organisations.id'); + }) + ->select('id AS organisation_id') + ->selectRaw('COALESCE(member_count, 0) AS member_count'); - $adminCounts = DB::table('organisation_members') - ->selectRaw('organisation_id, count(*) as admin_count') + $adminCountsInner = DB::table('organisation_members') + ->selectRaw('organisation_id, COUNT(*) AS admin_count') ->where('flag_admin', true) ->groupBy('organisation_id'); + $adminCounts = DB::table('organisations') + ->leftJoinSub($adminCountsInner, 'admin_counts_inner', function ($join) { + $join->on('admin_counts_inner.organisation_id', '=', 'organisations.id'); + }) + ->select('id AS organisation_id') + ->selectRaw('COALESCE(admin_count, 0) AS admin_count'); - $nonAdminCounts = DB::table('organisation_members') - ->selectRaw('organisation_id, count(*) as non_admin_count') + $nonAdminCountsInner = DB::table('organisation_members') + ->selectRaw('organisation_id, COUNT(*) AS non_admin_count') ->where('flag_admin', false) ->groupBy('organisation_id'); + $nonAdminCounts = DB::table('organisations') + ->leftJoinSub($nonAdminCountsInner, 'non_admin_counts_inner', function ($join) { + $join->on('non_admin_counts_inner.organisation_id', '=', 'organisations.id'); + }) + ->select('id AS organisation_id') + ->selectRaw('COALESCE(non_admin_count, 0) AS non_admin_count'); return $query ->leftJoinSub($memberCounts, 'member_counts', function ($join) { diff --git a/src/Database/Seeds/OrganisationPermissions.php b/src/Database/Seeds/OrganisationPermissions.php index 6659de1..9bd25f3 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.', ]), + 'view_organisation_field' => new Permission([ + 'slug' => 'view_organisation_field', + 'name' => 'View organisation', + 'conditions' => "in(property,['name',slug','description','members','admins'])", + 'description' => 'View certain properties of any organisation.', + ]), 'update_organisation_field' => new Permission([ 'slug' => 'update_organisation_field', 'name' => 'Edit organisation', @@ -61,6 +67,12 @@ class OrganisationPermissions extends BaseSeed 'conditions' => 'always()', 'description' => 'Delete an organisation.', ]), + 'uri_organisation' => new Permission([ + 'slug' => 'uri_organisation', + 'name' => 'View organisation', + 'conditions' => 'always()', + 'description' => 'View the organisation page of any organisation.', + ]), 'uri_organisations' => new Permission([ 'slug' => 'uri_organisations', 'name' => 'Organisation management page', diff --git a/templates/pages/organisation.html.twig b/templates/pages/organisation.html.twig new file mode 100644 index 0000000..a8ef588 --- /dev/null +++ b/templates/pages/organisation.html.twig @@ -0,0 +1,109 @@ +{% extends "pages/abstract/dashboard.html.twig" %} + +{% block stylesheets_page %} + + {{ assets.css('css/form-widgets') | raw }} +{% endblock %} + +{# Overrides blocks in head of base template #} +{% block page_title %}{{ translate("ORGANISATION", 2) }} | {{organisation.name}}{% endblock %} + +{% block page_description %}{{ translate("ORGANISATION.INFO_PAGE", {name: organisation.name}) }}{% endblock %} + +{% block body_matter %} +
+
+
+
+

{{translate('ORGANISATION.SUMMARY')}}

+ {% if 'tools' not in tools.hidden %} +
+
+ + +
+
+ {% endif %} +
+
+

{{organisation.name}}

+ + {% if 'description' not in fields.hidden %} +

+ {{organisation.description}} +

+ {% endif %} + {% if 'members' not in fields.hidden %} +
+ {{ translate('ADMIN', 2)}} +

+ {{organisation.admin_count}} +

+
+
+ {{ translate('MEMBER', 2)}} +

+ {{organisation.non_admin_count}} +

+ {% endif %} + {% block organisation_profile %}{% endblock %} +
+
+
+
+
+
+

{{translate('MEMBER', 2)}}

+ {% include "tables/table-tool-menu.html.twig" %} +
+
+ {% include "tables/users.html.twig" with { + "table" : { + "id" : "table-organisation-members" + } + } + %} +
+
+
+
+{% endblock %} +{% block scripts_page %} + + + + + {{ assets.js('js/form-widgets') | raw }} + + + {{ assets.js('js/pages/organisation') | raw }} +{% endblock %}