diff --git a/asset-bundles.json b/asset-bundles.json index eca6eaf..cb1b91c 100644 --- a/asset-bundles.json +++ b/asset-bundles.json @@ -1,5 +1,20 @@ { "bundle": { + "js/admin": { + "scripts": [ + "vendor/moment/moment.js", + "userfrosting/js/handlebars-helpers.js", + "vendor/tablesorter/dist/js/jquery.tablesorter.js", + "vendor/tablesorter/dist/js/jquery.tablesorter.widgets.js", + "userfrosting/js/tablesorter/widget-sort2Hash.js", + "vendor/tablesorter/dist/js/widgets/widget-columnSelector.min.js", + "vendor/tablesorter/dist/js/widgets/widget-reflow.min.js", + "vendor/tablesorter/dist/js/widgets/widget-pager.min.js", + "userfrosting/js/query-string.js", + "userfrosting/js/uf-table.js", + "avsdev/js/sidebar.js" + ] + }, "js/pages/organisation": { "scripts": [ "userfrosting/js/widgets/users.js", diff --git a/assets/avsdev/js/sidebar.js b/assets/avsdev/js/sidebar.js new file mode 100644 index 0000000..a763fee --- /dev/null +++ b/assets/avsdev/js/sidebar.js @@ -0,0 +1,23 @@ + +$(document).ready(function() { + $('[data-toggle="tooltip"]').tooltip(); + $('.sidebar-menu .collapse, .sidebar-menu .collapsing').on('hide.bs.collapse', function() { + $(this).prev().find('.fa').eq(1).removeClass('fa-angle-right').addClass('fa-angle-down'); + var states = JSON.parse(localStorage.getItem('sidebar-states') || {}); + states[this.id] = 0; + localStorage.setItem('sidebar-states', JSON.stringify(states)); + }); + $('.sidebar-menu .collapse, sidebar-menu .collapsing').on('show.bs.collapse', function() { + $(this).prev().find('.fa').eq(1).removeClass('fa-angle-down').addClass('fa-angle-right'); + var states = JSON.parse(localStorage.getItem('sidebar-states') || {}); + states[this.id] = 1; + localStorage.setItem('sidebar-states', JSON.stringify(states)); + }); + + var states = JSON.parse(localStorage.getItem('sidebar-states') || {}); + Object.getOwnPropertyNames(states).forEach((elid) => { + if (states[elid] === 1) { + $('#' + elid).collapse('show'); + } + }); +}) diff --git a/locale/en_US/messages.php b/locale/en_US/messages.php index 61505b1..cb8426c 100644 --- a/locale/en_US/messages.php +++ b/locale/en_US/messages.php @@ -36,9 +36,11 @@ return [ 'DELETE_YES' => 'Yes, delete organisation', 'DELETION_SUCCESSFUL' => 'Successfully deleted organisation {{name}}', - 'MEMBER_COUNT' => '# Members (excl admins)', + 'MEMBER_COUNT' => '# Members (excl admins)', 'ADMIN_COUNT' => '# Admins', + 'SELF' => 'My Organisations', + 'NAME' => [ 1 => 'Organisation name', diff --git a/src/Controller/OrganisationMembersController.php b/src/Controller/OrganisationMembersController.php index 683fff1..b91e492 100644 --- a/src/Controller/OrganisationMembersController.php +++ b/src/Controller/OrganisationMembersController.php @@ -74,10 +74,7 @@ class OrganisationMembersController extends SimpleController $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); - }); + return $query->where('organisation_id', $organisation->id); }); // Be careful how you consume this data - it has not been escaped and contains untrusted user-supplied content. diff --git a/src/Database/Models/User.php b/src/Database/Models/User.php new file mode 100644 index 0000000..d3ad146 --- /dev/null +++ b/src/Database/Models/User.php @@ -0,0 +1,76 @@ +classMapper; + + return $this->belongsToMany($classMapper->getClassMapping('organisation'), 'organisation_members', 'user_id', 'organisation_id'); + } + + /** + * Delete this member from the database, along with any links to organisations. + * + * @param bool $hardDelete Set to true to completely remove the member and all associated objects. + * + * @return bool true if the deletion was successful, false otherwise. + */ + public function delete($hardDelete = false) + { + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = static::$ci->classMapper; + + if ($hardDelete) { + // TODO: remove user from organisations? + } + + return parent::delete(); + } + + /** + * Joins the user's organisations directly, so we can do things like sort, search, paginate, etc. + * + * @param Builder $query + * + * @return Builder + */ + public function scopeJoinOrganisations($query) + { + $query = $query->leftJoin('organisation_members', 'organisation_members.user_id', '=', 'users.id'); + $query = $query->leftJoin('organisations', 'organisations.id', '=', 'organisation_members.organisation_id'); + + return $query; + } +} diff --git a/src/Database/Seeds/OrganisationPermissions.php b/src/Database/Seeds/OrganisationPermissions.php index 4868aac..f5ad470 100644 --- a/src/Database/Seeds/OrganisationPermissions.php +++ b/src/Database/Seeds/OrganisationPermissions.php @@ -27,6 +27,7 @@ class OrganisationPermissions extends BaseSeed public function run() { // We require the default roles + Seeder::execute('DefaultPermissions'); Seeder::execute('DefaultRoles'); Seeder::execute('OrganisationRoles'); @@ -56,6 +57,12 @@ class OrganisationPermissions extends BaseSeed 'conditions' => "in(property,['name','slug','description','members'])", 'description' => 'View certain properties of any organisation.', ]), + 'view_organisation_field_own' => new Permission([ + 'slug' => 'view_organisation_field', + 'name' => 'View own organisation', + 'conditions' => "is_organisation_member(self.id,organisation.id) & in(property,['name','slug','description','members'])", + 'description' => 'View certain properties of own organisation.', + ]), 'update_organisation_field' => new Permission([ 'slug' => 'update_organisation_field', 'name' => 'Edit organisation', @@ -80,6 +87,12 @@ class OrganisationPermissions extends BaseSeed 'conditions' => 'always()', 'description' => 'View the organisation page of any organisation.', ]), + 'uri_organisation_own' => new Permission([ + 'slug' => 'uri_organisation', + 'name' => 'View own organisation', + 'conditions' => 'is_organisation_member(self.id,organisation.id)', + 'description' => 'View the organisation page of an organisation you are a member of.', + ]), 'uri_organisations' => new Permission([ 'slug' => 'uri_organisations', 'name' => 'Organisation management page', @@ -144,6 +157,13 @@ class OrganisationPermissions extends BaseSeed $permissions['uri_organisation']->id, ]); } + + $roleUser = Role::where('slug', 'user')->first(); + if ($roleUser) { + $roleUser->permissions()->syncWithoutDetaching([ + $permissions['uri_organisation_own']->id, + $permissions['view_organisation_field_own']->id, + ]); } } } diff --git a/src/ServicesProvider/ServicesProvider.php b/src/ServicesProvider/ServicesProvider.php index 22eea78..8d80a81 100644 --- a/src/ServicesProvider/ServicesProvider.php +++ b/src/ServicesProvider/ServicesProvider.php @@ -9,6 +9,7 @@ namespace UserFrosting\Sprinkle\Organisations\ServicesProvider; +use Illuminate\Database\Capsule\Manager as Capsule; use Psr\Container\ContainerInterface; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; @@ -38,10 +39,49 @@ class ServicesProvider $container->extend('classMapper', function ($classMapper, $c) { $classMapper->setClassMapping('organisation', 'UserFrosting\Sprinkle\Organisations\Database\Models\Organisation'); $classMapper->setClassMapping('organisation_sprunje', 'UserFrosting\Sprinkle\Organisations\Sprunje\OrganisationSprunje'); + $classMapper->setClassMapping('user', 'UserFrosting\Sprinkle\Organisations\Database\Models\User'); + $classMapper->setClassMapping('user_sprunje', 'UserFrosting\Sprinkle\Organisations\Sprunje\UserSprunje'); return $classMapper; }); + /* + * Extend the 'authorizer' service to add extra access condition callbacks. + * + * @return \UserFrosting\Sprinkle\Core\Util\ClassMapper + */ + $container->extend('authorizer', function ($authorizer, $c) { + /* + * Check if all $user is a member of $organisation. + * + * @param int $user_id the id of the requesting user (normally currentUser->id). + * @param int $organisation_id the id of the target organisation. + * @return bool true if $user is a member of $organisation. + */ + $authorizer->addCallback('is_organisation_member', function ($user_id, $organisation_id) { + return Capsule::table('organisation_members') + ->where('user_id', $user_id) + ->where('organisation_id', $organisation_id) + ->count() > 0; + }); + + /* + * Check if all $user is an administrator of $organisation. + * + * @param int $user_id the id of the requesting user (normally currentUser->id). + * @param int $organisation_id the id of the target organisation. + * @return bool true if $user is an administrator of $organisation. + */ + $authorizer->addCallback('is_organisation_admin', function ($user_id, $organisation_id) { + return Capsule::table('organisation_members') + ->where('user_id', $user_id) + ->where('organisation_id', $organisation_id) + ->where('flag_admin', true) + ->count() > 0; + }); + + return $authorizer; + }); /* * Returns a callback that handles merging any organisation objects. @@ -79,6 +119,5 @@ class ServicesProvider }; }; - } } diff --git a/src/Sprunje/UserSprunje.php b/src/Sprunje/UserSprunje.php new file mode 100644 index 0000000..e697ad5 --- /dev/null +++ b/src/Sprunje/UserSprunje.php @@ -0,0 +1,85 @@ +joinOrganisations()->with('organisations'); + } + + /** + * Sort based on organisations names. + * + * @param Builder $query + * @param mixed $value + * + * @return self + */ + protected function sortOrganisations($query, $direction) + { + $query->orderBy('organisations.name', $direction); + + return $this; + } + + /** + * Filter LIKE the last activity description. + * + * @param Builder $query + * @param mixed $value + * + * @return self + */ + protected function filterOrganisations($query, $value) + { + // Split value on separator for OR queries + $values = explode($this->orSeparator, $value); + $query->where(function ($query) use ($values) { + foreach ($values as $value) { + $query->orLike('organisations.name', $value); + } + }); + + return $this; + } +} diff --git a/templates/navigation/sidebar-menu.html.twig b/templates/navigation/sidebar-menu.html.twig index 573bb76..c5d480b 100644 --- a/templates/navigation/sidebar-menu.html.twig +++ b/templates/navigation/sidebar-menu.html.twig @@ -40,4 +40,16 @@ {{ translate("ORGANISATION", 2) }} {% endif %} + {% if current_user.organisations.count() > 0 %} +