new: [org Groups] added

- Org Groups allow user co-management of sub communities
  - Create an org group with a set of organisations
  - assign administrators to an org group
  - org group admins can modify users of the group
refacto/CRUDComponent
iglocska 2023-09-12 09:26:07 +02:00
parent 01356824a2
commit 16a4a1cde5
No known key found for this signature in database
GPG Key ID: BEA224F1FEF113AC
31 changed files with 1161 additions and 50 deletions

View File

@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
use Migrations\AbstractMigration;
use Phinx\Db\Adapter\MysqlAdapter;
final class OrgGrouping extends AbstractMigration
{
public $autoId = false; // turn off automatic `id` column create. We want it to be `int(10) unsigned`
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change(): void
{
$exists = $this->hasTable('org_groups');
if (!$exists) {
$orgGroupsTable = $this->table('org_groups', [
'signed' => false,
'collation' => 'utf8mb4_unicode_ci'
]);
$orgGroupsTable
->addColumn('id', 'integer', [
'autoIncrement' => true,
'limit' => 10,
'signed' => false,
])
->addPrimaryKey('id')
->addColumn('uuid', 'string', [
'null' => false,
'limit' => 40,
'collation' => 'ascii_general_ci',
'encoding' => 'ascii',
])
->addColumn('name', 'string', [
'null' => false,
'limit' => 191,
'collation' => 'utf8mb4_unicode_ci',
'encoding' => 'utf8mb4',
])
->addColumn('description', 'text', [
'default' => null,
'null' => true
])
->addColumn('created', 'datetime', [
'null' => false
])
->addColumn('modified', 'datetime', [
'null' => false
])
->addIndex('name')
->addIndex('uuid', ['unique' => true]);
$orgGroupsTable->create();
}
$exists = $this->hasTable('org_groups_organisations');
if (!$exists) {
$orgGroupsTable = $this->table('org_groups_organisations', [
'signed' => false,
'collation' => 'utf8mb4_unicode_ci'
]);
$orgGroupsTable
->addColumn('id', 'integer', [
'autoIncrement' => true,
'limit' => 10,
'signed' => false,
])
->addPrimaryKey('id')
->addColumn('org_group_id', 'integer', [
'limit' => 10,
'signed' => false,
])
->addIndex('org_group_id')
->addColumn('organisation_id', 'integer', [
'limit' => 10,
'signed' => false,
])
->addIndex('organisation_id')
->addIndex(['org_group_id', 'organisation_id'], ['unique' => true]);
$orgGroupsTable->create();
}
$exists = $this->hasTable('org_groups_admins');
if (!$exists) {
$orgGroupsTable = $this->table('org_groups_admins', [
'signed' => false,
'collation' => 'utf8mb4_unicode_ci'
]);
$orgGroupsTable
->addColumn('id', 'integer', [
'autoIncrement' => true,
'limit' => 10,
'signed' => false,
])
->addPrimaryKey('id')
->addColumn('org_group_id', 'integer', [
'limit' => 10,
'signed' => false,
])
->addIndex('org_group_id')
->addColumn('user_id', 'integer', [
'limit' => 10,
'signed' => false,
])
->addIndex('user_id')
->addIndex(['org_group_id', 'user_id'], ['unique' => true]);
$orgGroupsTable->create();
}
}
}

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
use Migrations\AbstractMigration;
use Phinx\Db\Adapter\MysqlAdapter;
final class GroupAdminRole extends AbstractMigration
{
public $autoId = false; // turn off automatic `id` column create. We want it to be `int(10) unsigned`
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change(): void
{
$exists = $this->table('roles')->hasColumn('perm_group_admin');
if (!$exists) {
$this->table('roles')
->addColumn('perm_group_admin', 'boolean', [
'default' => 0,
'null' => false,
])
->addIndex('perm_group_admin')
->update();
}
}
}

View File

@ -161,6 +161,23 @@ class ACLComponent extends Component
'MetaTemplateNameDirectory' => [
'index' => ['perm_admin'],
],
'OrgGroups' => [
'add' => ['perm_admin'],
'delete' => ['perm_admin'],
'edit' => ['perm_admin'],
'index' => ['*'],
'view' => ['*'],
'filtering' => ['*'],
'tag' => ['perm_admin'],
'untag' => ['perm_admin'],
'viewTags' => ['*'],
'listAdmins' => ['*'],
'listOrgs' => ['*'],
'assignAdmin' => ['perm_admin'],
'removeAdmin' => ['perm_admin'],
'attachOrg' => ['perm_group_admin'],
'detachOrg' => ['perm_group_admin']
],
'Organisations' => [
'add' => ['perm_admin'],
'delete' => ['perm_admin'],
@ -335,10 +352,19 @@ class ACLComponent extends Component
if (empty($user) || empty($currentUser)) {
return false;
}
if ($user['id'] === $currentUser['id']) {
return true;
}
if (!$currentUser['role']['perm_admin']) {
if ($user['role']['perm_admin']) {
return false; // org_admins cannot edit admins
}
if ($currentUser['role']['perm_group_admin']) {
$this->OrgGroup = TableRegistry::get('OrgGroup');
if ($this->OrgGroup->checkIfUserBelongsToGroupAdminsGroup($currentUser, $user)) {
return true;
}
}
if (!$currentUser['role']['perm_org_admin']) {
return false;
} else {

View File

@ -1711,4 +1711,132 @@ class CRUDComponent extends Component
}
return true;
}
public function linkObjects(string $functionName, int $id1, string $model1, string $model2, array $params = []): void
{
$this->Controller->loadModel($model1);
$this->Controller->loadModel($model2);
if ($this->request->is(['post', 'put'])) {
$input = $this->request->getData();
if (empty($input['id'])) {
throw new MethodNotAllowedException(__('No ID of target to attach defined.'));
}
$id2 = $input['id'];
$obj1 = $this->Table->get($id1);
$obj2 = $this->Table->$model2->get($id2);
$data = [
[
'id' => $id1,
'model' => $model1
],
[
'id' => $id2,
'model' => $model2
]
];
try {
$savedData = $this->Table->$model2->link($obj1, [$obj2]);
} catch (Exception $e) {
$savedData = null;
}
if (!empty($savedData)) {
$message = __('{0} attached to {1}.', $model1, $model2);
if ($this->Controller->ParamHandler->isRest()) {
$this->Controller->restResponsePayload = $this->RestResponse->viewData($message, 'json');
} else if ($this->Controller->ParamHandler->isAjax()) {
if (!empty($params['displayOnSuccess'])) {
$displayOnSuccess = $this->renderViewInVariable($params['displayOnSuccess'], ['entity' => $data]);
$this->Controller->ajaxResponsePayload = $this->RestResponse->ajaxSuccessResponse($model1, 'index', $obj1, $message, ['displayOnSuccess' => $displayOnSuccess]);
} else {
$this->Controller->ajaxResponsePayload = $this->RestResponse->ajaxSuccessResponse($model1, 'index', $obj1, $message);
}
} else {
$this->Controller->Flash->success($message);
if (empty($params['redirect'])) {
$this->Controller->redirect(['action' => 'view', $id1]);
} else {
$this->Controller->redirect($params['redirect']);
}
}
} else {
$this->Controller->isFailResponse = true;
$message = __(
'{0} could not be attached to {1}.',
$model1,
$model2
);
if ($this->Controller->ParamHandler->isRest()) {
$this->Controller->restResponsePayload = $this->RestResponse->viewData($message, 'json');
} else if ($this->Controller->ParamHandler->isAjax()) {
$this->Controller->ajaxResponsePayload = $this->RestResponse->ajaxFailResponse($model1, $functionName, $data, $message, []);
} else {
$this->Controller->Flash->error($message);
}
}
}
}
public function unlinkObjects(string $functionName, int $id1, int $id2, string $model1, string $model2, array $params = []): void
{
$this->Controller->loadModel($model1);
$this->Controller->loadModel($model2);
$obj1 = $this->Table->get($id1);
$obj2 = $this->Table->$model2->get($id2);
$data = [
[
'id' => $id1,
'model' => $model1
],
[
'id' => $id2,
'model' => $model2
]
];
$this->Controller->set('data', $data);
if ($this->request->is(['post', 'put'])) {
$input = $this->request->getData();
try {
$savedData = $this->Table->$model2->unlink($obj1, [$obj2]);
} catch (Exception $e) {
$savedData = null;
}
if (!empty($savedData)) {
$message = __('{0} detached from {1}.', $model1, $model2);
if ($this->Controller->ParamHandler->isRest()) {
$this->Controller->restResponsePayload = $this->RestResponse->viewData($message, 'json');
} else if ($this->Controller->ParamHandler->isAjax()) {
if (!empty($params['displayOnSuccess'])) {
$displayOnSuccess = $this->renderViewInVariable($params['displayOnSuccess'], ['entity' => $data]);
$this->Controller->ajaxResponsePayload = $this->RestResponse->ajaxSuccessResponse($model1, 'index', $obj1, $message, ['displayOnSuccess' => $displayOnSuccess]);
} else {
$this->Controller->ajaxResponsePayload = $this->RestResponse->ajaxSuccessResponse($model1, 'index', $obj1, $message);
}
} else {
$this->Controller->Flash->success($message);
if (empty($params['redirect'])) {
$this->Controller->redirect(['action' => 'view', $id1]);
} else {
$this->Controller->redirect($params['redirect']);
}
}
} else {
$this->Controller->isFailResponse = true;
$message = __(
'{0} could not be detached from {1}.',
$model1,
$model2
);
if ($this->Controller->ParamHandler->isRest()) {
$this->Controller->restResponsePayload = $this->RestResponse->viewData($message, 'json');
} else if ($this->Controller->ParamHandler->isAjax()) {
$this->Controller->ajaxResponsePayload = $this->RestResponse->ajaxFailResponse($model1, $functionName, $data, $message, []);
} else {
$this->Controller->Flash->error($message);
}
}
}
}
}

View File

@ -27,6 +27,11 @@ class Sidemenu {
'icon' => $this->iconTable['Organisations'],
'url' => '/organisations/index',
],
'OrgGroups' => [
'label' => __('Organisation Groups'),
'icon' => $this->iconTable['OrgGroups'],
'url' => '/orgGroups/index',
],
'EncryptionKeys' => [
'label' => __('Encryption keys'),
'icon' => $this->iconTable['EncryptionKeys'],

View File

@ -23,6 +23,7 @@ class NavigationComponent extends Component
public $iconToTableMapping = [
'Individuals' => 'address-book',
'Organisations' => 'building',
'OrgGroups' => 'city',
'EncryptionKeys' => 'key',
'SharingGroups' => 'user-friends',
'MailingLists' => 'mail-bulk',

View File

@ -0,0 +1,256 @@
<?php
namespace App\Controller;
use App\Controller\AppController;
use Cake\Utility\Hash;
use Cake\Utility\Text;
use Cake\Database\Expression\QueryExpression;
use Cake\Http\Exception\NotFoundException;
use Cake\Http\Exception\MethodNotAllowedException;
use Cake\Http\Exception\ForbiddenException;
class OrgGroupsController extends AppController
{
public $quickFilterFields = [['name' => true], 'uuid'];
public $filterFields = ['name', 'uuid'];
public $containFields = ['Organisations'];
public $statisticsFields = [];
public function index()
{
$additionalContainFields = [];
$this->CRUD->index([
'filters' => $this->filterFields,
'quickFilters' => $this->quickFilterFields,
'quickFilterForMetaField' => ['enabled' => true, 'wildcard_search' => true],
'contain' => $this->containFields,
'statisticsFields' => $this->statisticsFields,
]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
}
$this->set('metaGroup', 'ContactDB');
}
public function filtering()
{
$this->CRUD->filtering();
}
public function add()
{
$this->CRUD->add();
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
}
}
public function view($id)
{
$this->CRUD->view($id, ['contain' => ['Organisations']]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
}
$this->set('canEdit', $this->canEdit($id));
}
public function edit($id)
{
if (!$this->canEdit($id)) {
throw new MethodNotAllowedException(__('You cannot modify that group.'));
}
$this->CRUD->edit($id);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
}
$this->set('metaGroup', 'ContactDB');
$this->render('add');
}
public function delete($id)
{
$this->CRUD->delete($id);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
}
$this->set('metaGroup', 'ContactDB');
}
public function tag($id)
{
if (!$this->canEdit($id)) {
throw new MethodNotAllowedException(__('You cannot tag that organisation.'));
}
$this->CRUD->tag($id);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
}
}
public function untag($id)
{
if (!$this->canEdit($id)) {
throw new MethodNotAllowedException(__('You cannot untag that organisation.'));
}
$this->CRUD->untag($id);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
}
}
public function viewTags($id)
{
$this->CRUD->viewTags($id);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
}
}
private function canEdit($groupId): bool
{
$currentUser = $this->ACL->getUser();
if ($currentUser['role']['perm_admin']) {
return true;
}
if ($currentUser['role']['perm_group_admin']) {
$orgGroup = $this->OrgGroups->get($groupId, ['contain' => 'Users']);
$found = false;
foreach ($orgGroup['users'] as $admin) {
if ($admin['id'] == $currentUser['id']) {
$found = true;
}
}
return $found;
}
return false;
}
// Listing should be available to all, it's purely informational
public function listAdmins($groupId)
{
if (empty($groupId)) {
throw new NotFoundException(__('Invalid {0}.', 'OrgGroup'));
}
$orgGroup = $this->OrgGroups->get($groupId, ['contain' => ['Users' => ['Individuals', 'Organisations']]]);
$this->set('data', $orgGroup['users']);
$this->set('canEdit', $this->ACL->getUser()['role']['perm_admin']);
$this->set('groupId', $groupId);
}
// Listing should be available to all, it's purely informational
public function listOrgs($groupId)
{
if (empty($groupId)) {
throw new NotFoundException(__('Invalid {0}.', 'OrgGroup'));
}
$orgGroup = $this->OrgGroups->get($groupId, ['contain' => 'Organisations']);
$this->set('data', $orgGroup['organisations']);
$this->set('canEdit', $this->canEdit($groupId));
$this->set('groupId', $groupId);
}
public function assignAdmin($groupId)
{
if (!$this->ACL->getUser()['role']['perm_admin']) {
throw new MethodNotAllowedException(__('You do not have permission to edit this group.'));
}
$this->CRUD->linkObjects(__FUNCTION__, $groupId, 'OrgGroups', 'Users', ['redirect' => '/orgGroups/listAdmins/' . $groupId]);
if ($this->request->is('post')) {
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
}
}
$orgGroup = $this->OrgGroups->get($groupId, ['contain' => 'Users']);
$this->loadModel('Users');
$this->loadModel('Roles');
$validRoles = $this->Roles->find('list')->disableHydration()->select(
['id', 'name']
)->where(
['OR' => ['perm_admin' => 1, 'perm_group_admin' => 1]]
)->toArray();
$admins = $this->Users->find('list')->disableHydration()->select(['id', 'username'])->where(['Users.role_id IN' => array_keys($validRoles)])->toArray();
asort($admins, SORT_STRING | SORT_FLAG_CASE);
if (!empty($orgGroup['users'])) {
foreach ($orgGroup['users'] as $admin) {
if (isset($admins[$admin['id']])) {
unset($admins[$admin['id']]);
}
}
}
$dropdownData = [
'admins' => $admins
];
$this->set(compact('dropdownData'));
}
public function removeAdmin($groupId, $adminId)
{
if (!$this->ACL->getUser()['role']['perm_admin']) {
throw new MethodNotAllowedException(__('You do not have permission to edit this group.'));
}
$this->CRUD->unlinkObjects(__FUNCTION__, $groupId, $adminId, 'OrgGroups', 'Users');
if ($this->request->is('post')) {
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
}
}
$this->viewBuilder()->setLayout('ajax');
$this->render('/genericTemplates/detach');
}
public function attachOrg($groupId)
{
if (!$this->OrgGroups->checkIfGroupAdmin($groupId, $this->ACL->getUser())) {
throw new MethodNotAllowedException(__('You do not have permission to edit this group.'));
}
$this->CRUD->linkObjects(__FUNCTION__, $groupId, 'OrgGroups', 'Organisations', ['redirect' => '/orgGroups/listOrgs/' . $groupId]);
if ($this->request->is('post')) {
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
}
}
$orgGroup = $this->OrgGroups->get($groupId, ['contain' => 'Organisations']);
$this->loadModel('Organisations');
$orgs = $this->Organisations->find('list')->disableHydration()->select(['id', 'name'])->toArray();
asort($orgs, SORT_STRING | SORT_FLAG_CASE);
foreach ($orgGroup['organisations'] as $organisation) {
if (isset($orgs[$organisation['id']])) {
unset($orgs[$organisation['id']]);
}
}
$dropdownData = [
'orgs' => $orgs
];
$this->set(compact('dropdownData'));
}
public function detachOrg($groupId, $orgId)
{
if (!$this->OrgGroups->checkIfGroupAdmin($groupId, $this->ACL->getUser())) {
throw new MethodNotAllowedException(__('You do not have permission to edit this group.'));
}
$this->CRUD->unlinkObjects(__FUNCTION__, $groupId, $orgId, 'OrgGroups', 'Organisations');
if ($this->request->is('post')) {
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
}
}
$this->viewBuilder()->setLayout('ajax');
$this->render('/genericTemplates/detach');
}
}

View File

@ -15,7 +15,7 @@ class OrganisationsController extends AppController
public $quickFilterFields = [['name' => true], 'uuid', 'nationality', 'sector', 'type', 'url'];
public $filterFields = ['name', 'uuid', 'nationality', 'sector', 'type', 'url', 'Alignments.id', 'MetaFields.field', 'MetaFields.value', 'MetaFields.MetaTemplates.name'];
public $containFields = ['Alignments' => 'Individuals'];
public $containFields = ['Alignments' => 'Individuals', 'OrgGroups'];
public $statisticsFields = ['nationality', 'sector'];
public function index()
@ -105,7 +105,7 @@ class OrganisationsController extends AppController
public function view($id)
{
$this->CRUD->view($id, ['contain' => ['Alignments' => 'Individuals']]);
$this->CRUD->view($id, ['contain' => ['Alignments' => 'Individuals', 'OrgGroups']]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;

View File

@ -13,7 +13,7 @@ class UsersController extends AppController
{
public $filterFields = ['Individuals.uuid', 'username', 'Individuals.email', 'Individuals.first_name', 'Individuals.last_name', 'Organisations.name', 'Organisation.nationality'];
public $quickFilterFields = ['Individuals.uuid', ['username' => true], ['Individuals.first_name' => true], ['Individuals.last_name' => true], 'Individuals.email'];
public $containFields = ['Individuals', 'Roles', 'UserSettings', 'Organisations'];
public $containFields = ['Individuals', 'Roles', 'UserSettings', 'Organisations', 'OrgGroups'];
public function index()
{
@ -177,7 +177,7 @@ class UsersController extends AppController
}
}
$this->CRUD->view($id, [
'contain' => ['Individuals' => ['Alignments' => 'Organisations'], 'Roles', 'Organisations'],
'contain' => ['Individuals' => ['Alignments' => 'Organisations'], 'Roles', 'Organisations', 'OrgGroups'],
'afterFind' => function($data) use ($keycloakUsersParsed, $currentUser) {
if (empty($currentUser['role']['perm_admin']) && $currentUser['organisation_id'] != $data['organisation_id']) {
throw new NotFoundException(__('Invalid User.'));
@ -329,6 +329,9 @@ class UsersController extends AppController
if (empty(Configure::read('user.allow-user-deletion'))) {
throw new MethodNotAllowedException(__('User deletion is disabled on this instance.'));
}
if (!$this->ACL->canEditUser($currentUser, $data)) {
throw new MethodNotAllowedException(__('You cannot edit the given user.'));
}
if (!$currentUser['role']['perm_admin']) {
if ($data['organisation_id'] !== $currentUser['organisation_id']) {
throw new MethodNotAllowedException(__('You do not have permission to delete the given user.'));

View File

@ -0,0 +1,26 @@
<?php
namespace App\Model\Entity;
use App\Model\Entity\AppModel;
use Cake\ORM\Entity;
class OrgGroup extends AppModel
{
protected $_accessible = [
'*' => true,
'id' => false,
'created' => false
];
protected $_accessibleOnNew = [
'created' => true
];
public function rearrangeForAPI(array $options = []): void
{
if (!empty($this->tags)) {
$this->tags = $this->rearrangeTags($this->tags);
}
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace App\Model\Table;
use App\Model\Table\AppTable;
use Cake\ORM\Table;
use Cake\Validation\Validator;
use Cake\Error\Debugger;
use App\Model\Entity\User;
class OrgGroupsTable extends AppTable
{
public function initialize(array $config): void
{
parent::initialize($config);
$this->addBehavior('UUID');
$this->addBehavior('Timestamp');
$this->addBehavior('Tags.Tag');
$this->addBehavior('AuditLog');
$this->belongsToMany('Organisations', [
'joinTable' => 'org_groups_organisations',
]);
$this->belongsToMany('Users', [
'joinTable' => 'org_groups_admins',
]);
$this->setDisplayField('name');
}
public function validationDefault(Validator $validator): Validator
{
$validator
->notEmptyString('name')
->notEmptyString('uuid')
->requirePresence(['name', 'uuid'], 'create');
return $validator;
}
public function checkIfGroupAdmin(int $groupId, mixed $user): bool
{
return true;
}
public function checkIfUserBelongsToGroupAdminsGroup(User $currentUser, User $userToCheck): bool
{
$managedGroups = $this->find('list')->where(['Users.id' => $currentUser['id']])->select(['id', 'uuid'])->disableHydration()->toArray();
return isset($managedGroups[$userToCheck['org_id']]);
}
}

View File

@ -34,6 +34,9 @@ class OrganisationsTable extends AppTable
'conditions' => ['owner_model' => 'organisation']
]
);
$this->belongsToMany('OrgGroups', [
'joinTable' => 'org_groups_organisations',
]);
$this->addBehavior('MetaFields');
$this->setDisplayField('name');
}

View File

@ -51,6 +51,9 @@ class UsersTable extends AppTable
'strategy' => 'join'
]
);
$this->belongsToMany('OrgGroups', [
'joinTable' => 'org_groups_admins',
]);
$this->hasMany(
'UserSettings',
[

View File

@ -0,0 +1,25 @@
<?php
echo $this->element('genericElements/Form/genericForm', array(
'data' => array(
'description' => __('OrgGroups are an administrative concept, multiple organisations can belong to a grouping that allows common management by so called "GroupAdmins". This helps grouping organisations by sector, country or other commonalities into co-managed sub-communities.'),
'model' => 'OrgGroup',
'fields' => array(
array(
'field' => 'name'
),
array(
'field' => 'uuid',
'label' => 'UUID',
'type' => 'uuid'
),
array(
'field' => 'description'
)
),
'submit' => array(
'action' => $this->request->getParam('action')
)
)
));
?>
</div>

View File

@ -0,0 +1,20 @@
<?php
echo $this->element('genericElements/Form/genericForm', array(
'data' => array(
'description' => __('Assign a user to be an administrator of the group.'),
'model' => null,
'fields' => array(
array(
'field' => 'id',
'label' => __('User'),
'type' => 'dropdown',
'options' => $dropdownData['admins']
)
),
'submit' => array(
'action' => $this->request->getParam('action')
)
)
));
?>
</div>

View File

@ -0,0 +1,20 @@
<?php
echo $this->element('genericElements/Form/genericForm', array(
'data' => array(
'description' => __('Assign an organisation to the group.'),
'model' => null,
'fields' => array(
array(
'field' => 'id',
'label' => __('Organisation'),
'type' => 'dropdown',
'options' => $dropdownData['orgs']
)
),
'submit' => array(
'action' => $this->request->getParam('action')
)
)
));
?>
</div>

View File

@ -0,0 +1,86 @@
<?php
echo $this->element('genericElements/IndexTable/index_table', [
'data' => [
'data' => $data,
'top_bar' => [
'children' => [
[
'type' => 'simple',
'children' => [
'data' => [
'type' => 'simple',
'text' => __('Add group'),
'class' => 'btn btn-primary',
'popover_url' => '/orgGroups/add'
]
]
],
[
'type' => 'search',
'button' => __('Search'),
'placeholder' => __('Enter value to search'),
'data' => '',
'searchKey' => 'value',
'allowFilering' => true
],
[
'type' => 'table_action',
'table_setting_id' => 'org_groups_index',
]
]
],
'fields' => [
[
'name' => '#',
'sort' => 'id',
'class' => 'short',
'data_path' => 'id',
],
[
'name' => __('Name'),
'class' => 'short',
'data_path' => 'name',
'sort' => 'name',
],
[
'name' => __('UUID'),
'sort' => 'uuid',
'class' => 'short',
'data_path' => 'uuid',
],
[
'name' => __('Description'),
'data_path' => 'description',
'sort' => 'description',
],
[
'name' => __('Tags'),
'data_path' => 'tags',
'element' => 'tags',
],
],
'title' => __('Organisation Groups Index'),
'description' => __('OrgGroups are an administrative concept, multiple organisations can belong to a grouping that allows common management by so called "GroupAdmins". This helps grouping organisations by sector, country or other commonalities into co-managed sub-communities.'),
'actions' => [
[
'url' => '/orgGroups/view',
'url_params_data_paths' => ['id'],
'icon' => 'eye'
],
[
'open_modal' => '/orgGroups/edit/[onclick_params_data_path]',
'modal_params_data_path' => 'id',
'icon' => 'edit',
'requirement' => $loggedUser['role']['perm_admin']
],
[
'open_modal' => '/orgGroups/delete/[onclick_params_data_path]',
'modal_params_data_path' => 'id',
'icon' => 'trash',
'requirement' => $loggedUser['role']['perm_admin']
],
]
]
]);
echo '</div>';
?>

View File

@ -0,0 +1,64 @@
<?php
echo $this->element('genericElements/IndexTable/index_table', [
'data' => [
'skip_pagination' => true,
'data' => $data,
'top_bar' => [
'children' => [
[
'type' => 'simple',
'children' => [
'data' => [
'type' => 'simple',
'text' => __('Add Group Administrator'),
'class' => 'btn btn-primary',
'popover_url' => '/orgGroups/assignAdmin/' . h($groupId),
'reload_url' => '/orgGroups/listAdmins/' . h($groupId),
],
],
'requirement' => $canEdit
]
]
],
'fields' => [
[
'name' => '#',
'sort' => 'id',
'class' => 'short',
'data_path' => 'id',
],
[
'name' => __('Username'),
'class' => 'short',
'data_path' => 'username',
'sort' => 'username',
],
[
'name' => __('Email'),
'class' => 'short',
'data_path' => 'individual.email',
'sort' => 'individual.email',
],
[
'name' => __('Organisation'),
'sort' => 'organisation.name',
'class' => 'short',
'data_path' => 'organisation.name',
]
],
'title' => null,
'description' => null,
'actions' => [
[
'open_modal' => '/orgGroups/removeAdmin/' . h($groupId) . '/[onclick_params_data_path]',
'reload_url' => '/orgGroups/listAdmins/' . h($groupId),
'modal_params_data_path' => 'id',
'icon' => 'unlink',
'title' => __('Remove the administrator from the group'),
'requirement' => $canEdit
],
]
]
]);
echo '</div>';
?>

View File

@ -0,0 +1,71 @@
<?php
echo $this->element('genericElements/IndexTable/index_table', [
'data' => [
'skip_pagination' => true,
'data' => $data,
'top_bar' => [
'children' => [
[
'type' => 'simple',
'children' => [
'data' => [
'type' => 'simple',
'text' => __('Add Organisation'),
'class' => 'btn btn-primary',
'popover_url' => '/orgGroups/attachOrg/' . h($groupId),
'reload_url' => '/orgGroups/listOrgs/' . h($groupId),
'requirement' => $canEdit
]
]
],
[
'type' => 'search',
'button' => __('Search'),
'placeholder' => __('Enter value to search'),
'data' => '',
'searchKey' => 'value',
'allowFilering' => true
]
]
],
'fields' => [
[
'name' => '#',
'sort' => 'id',
'class' => 'short',
'data_path' => 'id',
],
[
'name' => __('Name'),
'class' => 'short',
'data_path' => 'name',
'sort' => 'name',
],
[
'name' => __('UUID'),
'sort' => 'uuid',
'class' => 'short',
'data_path' => 'uuid',
]
],
'title' => null,
'description' => null,
'actions' => [
[
'url' => '/organisations/view',
'url_params_data_paths' => ['id'],
'icon' => 'eye'
],
[
'open_modal' => '/orgGroups/detachOrg/' . h($groupId) . '/[onclick_params_data_path]',
'reload_url' => '/orgGroups/listOrgs/' . h($groupId),
'modal_params_data_path' => 'id',
'icon' => 'unlink',
'title' => __('Remove organisation from the group'),
'requirement' => $canEdit
],
]
]
]);
echo '</div>';
?>

View File

@ -0,0 +1,44 @@
<?php
echo $this->element(
'/genericElements/SingleViews/single_view',
[
'title' => __('Organisation Group View'),
'data' => $entity,
'fields' => [
[
'key' => __('ID'),
'path' => 'id'
],
[
'key' => __('Name'),
'path' => 'name'
],
[
'key' => __('UUID'),
'path' => 'uuid'
],
[
'key' => __('Description'),
'path' => 'Description'
],
[
'key' => __('Tags'),
'type' => 'tags',
'editable' => $canEdit,
]
],
'combinedFieldsView' => false,
'children' => [
[
'url' => '/orgGroups/listAdmins/{{0}}',
'url_params' => ['id'],
'title' => __('Administrators')
],
[
'url' => '/orgGroups/listOrgs/{{0}}',
'url_params' => ['id'],
'title' => __('Organisations')
]
]
]
);

View File

@ -59,6 +59,14 @@ echo $this->element('genericElements/IndexTable/index_table', [
'url' => '/individuals/index/?Organisations.id={{url_data}}',
'url_data_path' => 'id'
],
[
'name' => __('Group memberships'),
'data_path' => 'org_groups',
'data_id_sub_path' => 'id',
'data_value_sub_path' => 'name',
'element' => 'link_list',
'url_pattern' => '/orgGroups/view/{{data_id}}'
],
[
'name' => __('URL'),
'sort' => 'url',

View File

@ -1,54 +1,67 @@
<?php
$fields = [
[
'key' => __('ID'),
'path' => 'id'
],
[
'key' => __('Name'),
'path' => 'name'
],
[
'key' => __('UUID'),
'path' => 'uuid'
],
[
'key' => __('URL'),
'path' => 'url'
],
[
'key' => __('Country'),
'path' => 'nationality'
],
[
'key' => __('Sector'),
'path' => 'sector'
],
[
'key' => __('Type'),
'path' => 'type'
],
[
'key' => __('Contacts'),
'path' => 'contacts'
],
[
'key' => __('Tags'),
'type' => 'tags',
'editable' => $canEdit,
],
[
'key' => __('Alignments'),
'type' => 'alignment',
'path' => '',
'scope' => 'organisations'
]
];
if (!empty($entity['org_groups'])) {
$fields[] = [
'type' => 'link_list',
'key' => __('Group memberships'),
'path' => 'org_groups',
'data_id_sub_path' => 'id',
'data_value_sub_path' => 'name',
'url_pattern' => '/orgGroups/view/{{data_id}}'
];
}
echo $this->element(
'/genericElements/SingleViews/single_view',
[
'title' => __('Organisation View'),
'data' => $entity,
'fields' => [
[
'key' => __('ID'),
'path' => 'id'
],
[
'key' => __('Name'),
'path' => 'name'
],
[
'key' => __('UUID'),
'path' => 'uuid'
],
[
'key' => __('URL'),
'path' => 'url'
],
[
'key' => __('Country'),
'path' => 'nationality'
],
[
'key' => __('Sector'),
'path' => 'sector'
],
[
'key' => __('Type'),
'path' => 'type'
],
[
'key' => __('Contacts'),
'path' => 'contacts'
],
[
'key' => __('Tags'),
'type' => 'tags',
'editable' => $canEdit,
],
[
'key' => __('Alignments'),
'type' => 'alignment',
'path' => '',
'scope' => 'organisations'
]
],
'fields' => $fields,
'combinedFieldsView' => false,
'children' => []
]

View File

@ -12,6 +12,11 @@
'type' => 'checkbox',
'label' => 'Full admin privilege'
],
[
'field' => 'perm_group_admin',
'type' => 'checkbox',
'label' => 'Organisation Group admin privilege'
],
[
'field' => 'perm_org_admin',
'type' => 'checkbox',

View File

@ -50,6 +50,12 @@ echo $this->element('genericElements/IndexTable/index_table', [
'data_path' => 'perm_admin',
'element' => 'boolean'
],
[
'name' => __('Group Admin'),
'sort' => 'perm_group_admin',
'data_path' => 'perm_group_admin',
'element' => 'boolean'
],
[
'name' => __('Org Admin'),
'sort' => 'perm_org_admin',

View File

@ -17,6 +17,11 @@ echo $this->element(
'path' => 'perm_admin',
'type' => 'boolean'
],
[
'key' => __('Organisation Group admin permission'),
'path' => 'perm_group_admin',
'type' => 'boolean'
],
[
'key' => __('Organisation admin permission'),
'path' => 'perm_org_admin',

View File

@ -63,6 +63,14 @@ echo $this->element('genericElements/IndexTable/index_table', [
'url' => '/organisations/view/{{0}}',
'url_vars' => ['organisation.id']
],
[
'name' => __('Administered Groups'),
'data_path' => 'org_groups',
'data_id_sub_path' => 'id',
'data_value_sub_path' => 'name',
'element' => 'link_list',
'url_pattern' => '/orgGroups/view/{{data_id}}'
],
[
'name' => __('Email'),
'sort' => 'individual.email',

View File

@ -55,6 +55,16 @@ $fields = [
'scope' => 'individuals'
]
];
if (!empty($entity['org_groups'])) {
$fields[] = [
'type' => 'link_list',
'key' => __('Administered Groups'),
'path' => 'org_groups',
'data_id_sub_path' => 'id',
'data_value_sub_path' => 'name',
'url_pattern' => '/orgGroups/view/{{data_id}}'
];
}
if ($keycloakConfig['enabled']) {
$fields[] = [
'key' => __('Keycloak status'),

View File

@ -0,0 +1,15 @@
<?php
$data = $this->Hash->extract($row, $field['data_path']);
$links = [];
foreach ($data as $object) {
$temp_id = h($this->Hash->extract($object, $field['data_id_sub_path'])[0]);
$temp_value = h($this->Hash->extract($object, $field['data_value_sub_path'])[0]);
$url = str_replace('{{data_id}}', $temp_id, $field['url_pattern']);
$links[] = sprintf(
'<a href="%s">%s</a>',
$url,
$temp_value
);
}
echo implode('<br />', $links);
?>

View File

@ -2,7 +2,9 @@
if (!isset($data['requirement']) || $data['requirement']) {
$elements = '';
foreach ($data['children'] as $element) {
$elements .= $this->element('/genericElements/ListTopBar/element_' . (empty($element['type']) ? 'simple' : h($element['type'])), array('data' => $element, 'tableRandomValue' => $tableRandomValue));
if (!isset($element['requirement']) || $element['requirement']) {
$elements .= $this->element('/genericElements/ListTopBar/element_' . (empty($element['type']) ? 'simple' : h($element['type'])), array('data' => $element, 'tableRandomValue' => $tableRandomValue));
}
}
echo sprintf(
'<div %s class="btn-group btn-group-sm me-2 flex-wrap" role="group" aria-label="button-group">%s</div>',

View File

@ -0,0 +1,15 @@
<?php
$data = $this->Hash->extract($data, $field['path']);
$links = [];
foreach ($data as $object) {
$temp_id = h($this->Hash->extract($object, $field['data_id_sub_path'])[0]);
$temp_value = h($this->Hash->extract($object, $field['data_value_sub_path'])[0]);
$url = str_replace('{{data_id}}', $temp_id, $field['url_pattern']);
$links[] = sprintf(
'<a href="%s">%s</a>',
$url,
$temp_value
);
}
echo implode('<br />', $links);
?>

View File

@ -0,0 +1,37 @@
<?php
$form = $this->element('genericElements/Form/genericForm', [
'entity' => null,
'ajax' => false,
'raw' => true,
'data' => [
'submit' => [
'action' => $this->request->getParam('action')
]
]
]);
$formHTML = sprintf('<div class="d-none">%s</div>', $form);
$bodyMessage = !empty($deletionText) ? h($deletionText) : __(
'Are you sure you want to detach {2} #{3} from {0} #{1}?',
h($data[0]['model']),
h($data[0]['id']),
h($data[1]['model']),
h($data[1]['id'])
);
$bodyHTML = sprintf('%s%s', $formHTML, $bodyMessage);
echo $this->Bootstrap->modal([
'size' => 'lg',
'title' => !empty($deletionTitle) ? $deletionTitle : __(
'Detach {0} from {1}',
h($data[0]['model']),
h($data[1]['model']),
),
'type' => 'confirm',
'confirmButton' => [
'text' => !empty($deletionConfirm) ? $deletionConfirm : __('Detach'),
'variant' => 'danger',
],
'bodyHtml' => $bodyHTML,
]);
?>