Merge branch 'main' into develop

refacto/CRUDComponent
iglocska 2023-05-26 16:15:58 +02:00
commit 6e33d78996
No known key found for this signature in database
GPG Key ID: BEA224F1FEF113AC
21 changed files with 711 additions and 6 deletions

View File

@ -0,0 +1,116 @@
<?php
declare(strict_types=1);
use Migrations\AbstractMigration;
use Phinx\Db\Adapter\MysqlAdapter;
final class Enumerations 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('enumeration_collections');
if (!$exists) {
$enumerationCollectionsTable = $this->table('enumeration_collections', [
'signed' => false,
'collation' => 'utf8mb4_unicode_ci'
]);
$enumerationCollectionsTable
->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('target_model', 'string', [
'null' => false,
'limit' => 255,
'collation' => 'ascii_general_ci',
'encoding' => 'ascii',
])
->addColumn('target_field', 'string', [
'null' => false,
'limit' => 255,
'collation' => 'ascii_general_ci',
'encoding' => 'ascii',
])
->addColumn('enabled', 'boolean', [
'default' => 0,
])
->addColumn('deleted', 'boolean', [
'default' => 0,
])
->addColumn('created', 'datetime', [
'null' => false
])
->addColumn('modified', 'datetime', [
'null' => false
])
->addIndex('name')
->addIndex('target_model')
->addIndex('target_field')
->addIndex('uuid', ['unique' => true]);
$enumerationCollectionsTable->create();
}
$exists = $this->hasTable('enumerations');
if (!$exists) {
$enumerationsTable = $this->table('enumerations', [
'signed' => false,
'collation' => 'utf8mb4_unicode_ci'
]);
$enumerationsTable
->addColumn('id', 'integer', [
'autoIncrement' => true,
'limit' => 10,
'signed' => false,
])
->addPrimaryKey('id')
->addColumn('value', 'string', [
'null' => false,
'limit' => 191,
'collation' => 'utf8mb4_unicode_ci',
'encoding' => 'utf8mb4',
])
->addColumn('enumeration_collection_id', 'integer', [
'limit' => 10,
'signed' => false,
'null' => false
])
->addIndex('value')
->addIndex('enumeration_collection_id');
$enumerationsTable->create();
}
}
}

View File

@ -75,6 +75,17 @@ class ACLComponent extends Component
'delete' => ['*'],
'index' => ['*']
],
'Enumerations' => [
'delete' => ['perm_admin'],
'index' => ['*']
],
'EnumerationCollections' => [
'view' => ['*'],
'add' => ['perm_admin'],
'edit' => ['perm_admin'],
'delete' => ['perm_admin'],
'index' => ['*']
],
'Inbox' => [
'createEntry' => ['OR' => ['perm_admin', 'perm_sync']],
'delete' => ['perm_admin'],

View File

@ -50,7 +50,6 @@ class CRUDComponent extends Component
if ($this->taggingSupported()) {
$options['filters'][] = 'filteringTags';
}
$optionFilters = [];
$optionFilters += empty($options['filters']) ? [] : $options['filters'];
foreach ($optionFilters as $i => $filter) {
@ -85,9 +84,22 @@ class CRUDComponent extends Component
$this->Controller->paginate['order'] = $options['order'];
}
}
if (!empty($this->request->getQuery('sort'))) {
$sort = $this->request->getQuery('sort');
$direction = $this->request->getQuery('direction');
if ($this->_validOrderFields($sort) && ($direction === 'asc' || $direction === 'desc')) {
$sort = explode('.', $sort);
if (count($sort) > 1) {
$sort[0] = Inflector::camelize(Inflector::pluralize($sort[0]));
}
$sort = implode('.', $sort);
$query->order($sort . ' ' . $direction);
}
}
if ($this->metaFieldsSupported() && !$this->Controller->ParamHandler->isRest()) {
$query = $this->includeRequestedMetaFields($query);
}
if (!$this->Controller->ParamHandler->isRest()) {
$this->setRequestedEntryAmount();
}
@ -387,6 +399,14 @@ class CRUDComponent extends Component
if (!empty($params['fields'])) {
$this->Controller->set('fields', $params['fields']);
}
$EnumerationCollections = TableRegistry::getTableLocator()->get('EnumerationCollections');
$modelAlias = $this->Table->getAlias();
if (in_array($this->Table->getAlias(), $EnumerationCollections->getValidModelList())) {
$enumerations = $EnumerationCollections->getFieldValues($modelAlias);
if (!empty($enumerations)) {
$this->Controller->set('enumerations', $enumerations);
}
}
$this->Controller->entity = $data;
$this->Controller->set('entity', $data);
}
@ -1673,7 +1693,7 @@ class CRUDComponent extends Component
return false;
}
} else {
$association = $this->Table->associations()->get($model);
$association = $this->Table->associations()->get(Inflector::camelize(Inflector::pluralize($model)));
$associatedTable = $association->getTarget();
if (empty($associatedTable->getSchema()->typeMap()[$subField])) {
return false;

View File

@ -128,6 +128,11 @@ class Sidemenu {
'url' => '/permissionLimitations/index',
'icon' => 'jedi',
],
'Enumerations' => [
'label' => __('Collections'),
'url' => '/enumerationCollections/index',
'icon' => 'list',
],
]
],
'API' => [

View File

@ -0,0 +1,98 @@
<?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 EnumerationCollectionsController extends AppController
{
public $filterFields = ['name', 'target_model', 'target_field'];
public $quickFilterFields = ['name', 'target_model', 'target_field'];
public $containFields = [];
public function index()
{
$this->CRUD->index([
'filters' => $this->filterFields,
'quickFilters' => $this->quickFilterFields,
'contain' => ['Enumerations'],
'afterFind' => function($data) {
$data->value_count = isset($data->enumerations) ? count($data->enumerations) : 0;
$data->values = Hash::extract($data, 'enumerations.{n}.value');
unset($data->enumerations);
return $data;
}
]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
}
$this->set('metaGroup', 'Enumerations');
}
public function add()
{
$this->CRUD->add([
'afterSave' => function($data) {
$this->EnumerationCollections->captureValues($data);
}
]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
}
$this->set(compact('enumerations'));
$this->set('metaGroup', 'Enumerations');
}
public function view($id)
{
$this->CRUD->view($id);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
}
$this->set('metaGroup', 'Enumerations');
}
public function edit($id)
{
$this->CRUD->edit($id, [
'afterSave' => function($data) {
$this->EnumerationCollections->purgeValues($data);
$this->EnumerationCollections->captureValues($data);
},
'contain' => ['Enumerations'],
'afterFind' => function($data) {
$values = [];
foreach ($data['enumerations'] as $enumeration) {
$values[] = $enumeration['value'];
}
$data->values = implode(PHP_EOL, $values);
return $data;
}
]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
}
$this->set('metaGroup', 'Enumerations');
$this->render('add');
}
public function delete($id)
{
$this->CRUD->delete($id);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
}
$this->set('metaGroup', 'Enumerations');
}
}

View File

@ -0,0 +1,40 @@
<?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 EnumerationsController extends AppController
{
public $filterFields = ['value'];
public $quickFilterFields = ['value'];
public $containFields = [];
public function index()
{
$this->CRUD->index([
'filters' => $this->filterFields,
'quickFilters' => $this->quickFilterFields
]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
}
$this->set('metaGroup', 'Enumerations');
}
public function delete($id)
{
$this->CRUD->delete($id);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
}
$this->set('metaGroup', 'Enumerations');
}
}

View File

@ -11,7 +11,7 @@ use Cake\Http\Exception\NotFoundException;
class UsersController extends AppController
{
public $filterFields = ['Individuals.uuid', 'username', 'Individuals.email', 'Individuals.first_name', 'Individuals.last_name', 'Organisations.name'];
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'];

View File

@ -0,0 +1,11 @@
<?php
namespace App\Model\Entity;
use App\Model\Entity\AppModel;
use Cake\ORM\Entity;
class Enumeration extends AppModel
{
}

View File

@ -0,0 +1,11 @@
<?php
namespace App\Model\Entity;
use App\Model\Entity\AppModel;
use Cake\ORM\Entity;
class EnumerationCollection extends AppModel
{
}

View File

@ -0,0 +1,118 @@
<?php
namespace App\Model\Table;
use App\Model\Table\AppTable;
use Cake\ORM\Table;
use Cake\Validation\Validator;;
use Cake\Datasource\EntityInterface;
use Cake\Event\Event;
use Cake\Event\EventInterface;
use Cake\Utility\Text;
use ArrayObject;
class EnumerationCollectionsTable extends AppTable
{
private $fieldMapping = [
'Organisations' => [
'country',
'sector',
'type'
]
];
public function initialize(array $config): void
{
parent::initialize($config);
$this->addBehavior('UUID');
$this->addBehavior('AuditLog');
$this->addBehavior('Timestamp');
$this->hasMany(
'Enumerations',
[
'dependent' => true
]
);
$this->setDisplayField('name');
}
public function beforeMarshal(EventInterface $event, ArrayObject $data, ArrayObject $options)
{
if (empty($data['uuid'])) {
$data['uuid'] = Text::uuid();
}
return true;
}
public function validationDefault(Validator $validator): Validator
{
$validator
->notEmptyString('name')
->requirePresence(['name'], 'create')
->notEmptyString('uuid')
->requirePresence(['uuid'], 'create')
->notEmptyString('target_model')
->requirePresence(['target_model'], 'create')
->notEmptyString('target_field')
->requirePresence(['target_field'], 'create');
return $validator;
}
public function getValidFieldList(?string $model = null): array
{
if (!empty($model)) {
if (empty($this->fieldMapping[$model])) {
return [];
} else {
return $this->fieldMapping[$model];
}
} else {
return $this->fieldMapping;
}
}
public function getValidModelList(?string $model = null): array
{
return array_keys($this->fieldMapping);
}
public function getFieldValues($model): array
{
$collections = $this->find('all')->where(['target_model' => $model, 'enabled' => 1, 'deleted' => 0])->contain(['Enumerations'])->disableHydration()->all()->toArray();
$options = [];
foreach ($collections as $collection) {
if (empty($collection['target_field'])) {
$options[$collection['target_field']] = [];
}
foreach ($collection['enumerations'] as $enumeration) {
$options[$collection['target_field']][$enumeration['value']] = $enumeration['value'];
}
}
return $options;
}
public function purgeValues(\App\Model\Entity\EnumerationCollection $entity): void
{
$this->Enumerations->deleteAll([
'enumeration_collection_id' => $entity->id
]);
}
public function captureValues(\App\Model\Entity\EnumerationCollection $entity): void
{
if (!empty($entity->values)) {
$values = $entity->values;
$collection_id = $entity->id;
if (!is_array($values)) {
$values = explode("\n", $values);
}
foreach ($values as $value) {
$enumeration = $this->Enumerations->newEntity([
'value' => trim($value),
'enumeration_collection_id' => $entity->id
]);
$this->Enumerations->save($enumeration);
}
}
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Model\Table;
use App\Model\Table\AppTable;
use Cake\ORM\Table;
use Cake\Validation\Validator;
class EnumerationsTable extends AppTable
{
public function initialize(array $config): void
{
parent::initialize($config);
$this->belongsTo(
'EnumerationCollection'
);
$this->setDisplayField('value');
}
public function validationDefault(Validator $validator): Validator
{
$validator
->notEmptyString('value')
->requirePresence(['value'], 'create')
->notEmptyString('enumeration_collection_id')
->requirePresence(['enumeration_collection_id'], 'create');
return $validator;
}
}

View File

@ -47,7 +47,8 @@ class UsersTable extends AppTable
'Organisations',
[
'dependent' => false,
'cascadeCallbacks' => false
'cascadeCallbacks' => false,
'strategy' => 'join'
]
);
$this->hasMany(

View File

@ -0,0 +1,40 @@
<?php
echo $this->element('genericElements/Form/genericForm', [
'data' => [
'description' => __('Roles define global rules for a set of users, including first and foremost access controls to certain functionalities.'),
'model' => 'EnumerationCollections',
'fields' => [
[
'field' => 'name',
'label' => __('Name')
],
[
'field' => 'enabled',
'label' => __('Enabled'),
'type' => 'checkbox',
],
[
'field' => 'target_model',
'label' => __('Model'),
],
[
'field' => 'target_field',
'label' => __('Field'),
],
[
'field' => 'description',
'label' => __('Description'),
],
[
'field' => 'values',
'label' => __('Values'),
'type' => 'textarea'
],
],
'submit' => [
'action' => $this->request->getParam('action')
]
]
]);
?>
</div>

View File

@ -0,0 +1,96 @@
<?php
$topbarChildren = [];
if (!empty($loggedUser->role->perm_admin)) {
$topbarChildren[] = [
'type' => 'simple',
'children' => [
'data' => [
'type' => 'simple',
'text' => __('Add Enumeration Collection'),
'class' => 'btn btn-primary',
'popover_url' => '/enumerationCollections/add'
]
]
];
}
$topbarChildren[] = [
'type' => 'search',
'button' => __('Search'),
'placeholder' => __('Enter value to search'),
'data' => '',
'searchKey' => 'value'
];
echo $this->element('genericElements/IndexTable/index_table', [
'data' => [
'data' => $data,
'top_bar' => [
'children' => $topbarChildren,
],
'fields' => [
[
'name' => '#',
'sort' => 'id',
'data_path' => 'id',
],
[
'name' => __('Name'),
'sort' => 'name',
'data_path' => 'name',
],
[
'name' => __('Enabled'),
'sort' => 'enabled',
'data_path' => 'enabled',
],
[
'name' => __('UUID'),
'sort' => 'uuid',
'data_path' => 'uuid',
],
[
'name' => __('Model'),
'sort' => 'target_model',
'data_path' => 'target_model',
],
[
'name' => __('Field'),
'sort' => 'target_field',
'data_path' => 'target_field',
],
[
'name' => __('Values'),
'sort' => 'value_count',
'data_path' => 'value_count',
],
[
'name' => __('Description'),
'data_path' => 'description',
],
],
'title' => __('Enumeration Collections Index'),
'description' => __('A list collections that can be used to convert string input fields into selections wherever it makes sense.'),
'pull' => 'right',
'actions' => [
[
'url' => '/enumerationCollections/view',
'url_params_data_paths' => ['id'],
'icon' => 'eye'
],
[
'open_modal' => '/enumerationCollections/edit/[onclick_params_data_path]',
'modal_params_data_path' => 'id',
'icon' => 'edit',
'requirement' => !empty($loggedUser['role']['perm_admin'])
],
[
'open_modal' => '/enumerationCollections/delete/[onclick_params_data_path]',
'modal_params_data_path' => 'id',
'icon' => 'trash',
'requirement' => !empty($loggedUser['role']['perm_admin'])
],
]
]
]);
echo '</div>';
?>

View File

@ -0,0 +1,46 @@
<?php
echo $this->element(
'/genericElements/SingleViews/single_view',
[
'data' => $entity,
'fields' => [
[
'key' => __('ID'),
'path' => 'id'
],
[
'key' => __('Name'),
'path' => 'name'
],
[
'key' => __('Enabled'),
'path' => 'enabled',
'type' => 'boolean'
],
[
'key' => __('UUID'),
'path' => 'uuid'
],
[
'key' => __('Model'),
'path' => 'target_model'
],
[
'key' => __('Field'),
'path' => 'target_field'
],
[
'key' => __('Description'),
'path' => 'description'
],
],
'children' => [
[
'url' => '/Enumerations/index?EnumerationCollection.id={{0}}',
'url_params' => ['id'],
'title' => __('Values')
]
]
]
);

View File

@ -0,0 +1,43 @@
<?php
echo $this->element('genericElements/IndexTable/index_table', [
'data' => [
'data' => $data,
'top_bar' => [
'children' => [
[
'type' => 'search',
'button' => __('Search'),
'placeholder' => __('Enter value to search'),
'data' => '',
'searchKey' => 'value'
]
]
],
'fields' => [
[
'name' => '#',
'sort' => 'id',
'data_path' => 'id',
],
[
'name' => __('Value'),
'sort' => 'value',
'data_path' => 'value',
]
],
'title' => __('Enumerations Index'),
'description' => null,
'pull' => 'right',
'actions' => [
[
'open_modal' => '/enumerations/delete/[onclick_params_data_path]',
'modal_params_data_path' => 'id',
'icon' => 'trash',
'requirement' => !empty($loggedUser['role']['perm_admin'])
],
]
]
]);
echo '</div>';
?>

View File

@ -2,7 +2,7 @@
echo $this->element('genericElements/Form/genericForm', array(
'data' => array(
'description' => __('Organisations can be equivalent to legal entities or specific individual teams within such entities. Their purpose is to relate individuals to their affiliations and for release control of information using the Trust Circles.'),
'model' => 'Organisations',
'model' => 'Organisation',
'fields' => array(
array(
'field' => 'name'

View File

@ -1,7 +1,6 @@
<?php
use Cake\Core\Configure;
echo $this->element('genericElements/IndexTable/index_table', [
'data' => [
'data' => $data,
@ -88,6 +87,11 @@ echo $this->element('genericElements/IndexTable/index_table', [
'url' => '/roles/view/{{0}}',
'url_vars' => ['role.id']
],
[
'name' => __('Country'),
'sort' => 'organisation.nationality',
'data_path' => 'organisation.nationality'
],
[
'name' => __('# User Settings'),
'element' => 'count_summary',

View File

@ -44,6 +44,10 @@ $fields = [
'key' => __('Last name'),
'path' => 'individual.last_name'
],
[
'key' => __('Country'),
'path' => 'organisation.nationality'
],
[
'key' => __('Alignments'),
'type' => 'alignment',

View File

@ -1,5 +1,9 @@
<?php
if (is_array($fieldData)) {
// Don't barf if the model is not explicitly passed
$modelForForm = empty($data['model']) ?
h(\Cake\Utility\Inflector::singularize(\Cake\Utility\Inflector::classify($this->request->getParam('controller')))) :
h($data['model']);
$fieldTemplate = 'genericField';
if (!empty($fieldData['type'])) {
if (file_exists(ROOT . '/templates/element/genericElements/Form/Fields/' . $fieldData['type'] . 'Field.php')) {

View File

@ -26,6 +26,14 @@
}
$formRandomValue = Cake\Utility\Security::randomString(8);
$initSelect2 = false;
if (!empty($enumerations)) {
foreach ($data['fields'] as $k => $field) {
if (isset($enumerations[$field['field']])) {
$data['fields'][$k]['options'] = $enumerations[$field['field']];
}
}
}
$formCreate = $this->Form->create($entity, ['id' => 'form-' . $formRandomValue]);
$default_template = [
'inputContainer' => '<div class="row mb-3">{{content}}</div>',