From 4390face107157f4ca15a4bbd33b8a037a68ad9b Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Tue, 9 Jan 2024 10:32:18 +0100 Subject: [PATCH 1/9] chg: [component:CRUD] Mirrored changes done in MISP3's CRUD component --- src/Controller/Component/CRUDComponent.php | 632 ++++++++++++++------- 1 file changed, 433 insertions(+), 199 deletions(-) diff --git a/src/Controller/Component/CRUDComponent.php b/src/Controller/Component/CRUDComponent.php index 4922209..3c51bef 100644 --- a/src/Controller/Component/CRUDComponent.php +++ b/src/Controller/Component/CRUDComponent.php @@ -7,6 +7,7 @@ use Cake\Error\Debugger; use Cake\Utility\Hash; use Cake\Utility\Inflector; use Cake\Utility\Text; +use Cake\Validation\Validation; use Cake\View\ViewBuilder; use Cake\ORM\TableRegistry; use Cake\ORM\Query; @@ -37,6 +38,7 @@ class CRUDComponent extends Component $embedInModal = !empty($this->request->getQuery('embedInModal', false)); $excludeStats = !empty($this->request->getQuery('excludeStats', false)); $skipTableToolbar = !empty($this->request->getQuery('skipTableToolbar', false)); + $wrapResponse = !empty($options['wrapResponse']); if (!empty($options['quickFilters'])) { if (empty($options['filters'])) { @@ -114,34 +116,46 @@ class CRUDComponent extends Component $totalCount = $this->Controller->getRequest()->getAttribute('paging')[$this->TableAlias]['count']; if ($isRestOrCSV) { if (isset($options['hidden'])) { - $data->each(function($value, $key) use ($options) { - $hidden = is_array($options['hidden']) ? $options['hidden'] : [$options['hidden']]; - $value->setHidden($hidden); - return $value; - }); + $data->each( + function ($value, $key) use ($options) { + $hidden = is_array($options['hidden']) ? $options['hidden'] : [$options['hidden']]; + $value->setHidden($hidden); + return $value; + } + ); } if (isset($options['afterFind'])) { $function = $options['afterFind']; if (is_callable($function)) { - $data = $data->map(function($value, $key) use ($function) { - return $function($value); - })->filter(function ($value) { - return $value !== false; - }); + $data = $data->map( + function ($value, $key) use ($function) { + return $function($value); + } + )->filter( + function ($value) { + return $value !== false; + } + ); } else { $t = $this->Table; - $data = $data->map(function($value, $key) use ($t, $function) { - return $t->$function($value); - })->filter(function ($value) { - return $value !== false; - }); + $data = $data->map( + function ($value, $key) use ($t, $function) { + return $t->$function($value); + } + )->filter( + function ($value) { + return $value !== false; + } + ); } } if ($this->metaFieldsSupported()) { $metaTemplates = $this->getMetaTemplates()->toArray(); - $data = $data->map(function($value, $key) use ($metaTemplates) { - return $this->attachMetaTemplatesIfNeeded($value, $metaTemplates); - }); + $data = $data->map( + function ($value, $key) use ($metaTemplates) { + return $this->attachMetaTemplatesIfNeeded($value, $metaTemplates); + } + ); } if ($this->request->is('csv')) { require_once(ROOT . '/src/Lib/Tools/CsvConverter.php'); @@ -154,27 +168,43 @@ class CRUDComponent extends Component 'X-Total-Count' => $totalCount, ]); } else { - $this->Controller->restResponsePayload = $this->RestResponse->viewData($data, 'json', false, false, false, [ - 'X-Total-Count' => $totalCount, - ]); + $this->Controller->restResponsePayload = $this->RestResponse->viewData( + $data, + 'json', + false, + false, + false, + [ + 'X-Total-Count' => $totalCount, + ], + $wrapResponse + ); } } else { $this->Controller->setResponse($this->Controller->getResponse()->withHeader('X-Total-Count', $totalCount)); if (isset($options['afterFind'])) { $function = $options['afterFind']; if (is_callable($function)) { - $data = $data->map(function($value, $key) use ($function) { - return $function($value); - })->filter(function($value) { - return $value !== false; - }); + $data = $data->map( + function ($value, $key) use ($function) { + return $function($value); + } + )->filter( + function ($value) { + return $value !== false; + } + ); } else { $t = $this->Table; - $data = $data->map(function($value, $key) use ($t, $function) { - return $t->$function($value); - })->filter(function ($value) { - return $value !== false; - }); + $data = $data->map( + function ($value, $key) use ($t, $function) { + return $t->$function($value); + } + )->filter( + function ($value) { + return $value !== false; + } + ); } } $this->setFilteringContext($options['contextFilters'] ?? [], $params); @@ -185,9 +215,15 @@ class CRUDComponent extends Component $data[$i] = $this->attachMetaTemplatesIfNeeded($row, $metaTemplates); } $this->Controller->set('meta_templates', $metaTemplates); - $this->Controller->set('meta_templates_enabled', array_filter($metaTemplates, function($template) { - return $template['enabled']; - })); + $this->Controller->set( + 'meta_templates_enabled', + array_filter( + $metaTemplates, + function ($template) { + return $template['enabled']; + } + ) + ); } if (empty($excludeStats)) { // check if stats are requested $modelStatistics = []; @@ -235,7 +271,7 @@ class CRUDComponent extends Component } } - public function filtering(): void + public function filtering(array $options = []): void { if ($this->taggingSupported()) { $this->Controller->set('taggingEnabled', true); @@ -256,7 +292,7 @@ class CRUDComponent extends Component } else { $this->Controller->set('metaFieldsEnabled', false); } - $filtersConfigRaw= !empty($this->Controller->filterFields) ? $this->Controller->filterFields : []; + $filtersConfigRaw = !empty($this->Controller->filterFields) ? $this->Controller->filterFields : []; $filtersConfig = []; foreach ($filtersConfigRaw as $fieldConfig) { if (is_array($fieldConfig)) { @@ -279,9 +315,18 @@ class CRUDComponent extends Component $this->Table->getSchema()->typeMap(), $associatedtypeMap ); - $typeMap = array_filter($typeMap, function ($field) use ($filtersName) { - return in_array($field, $filtersName); - }, ARRAY_FILTER_USE_KEY); + $typeMap = array_filter( + $typeMap, + function ($field) use ($filtersName) { + return in_array($field, $filtersName); + }, + ARRAY_FILTER_USE_KEY + ); + if (!empty($options['afterFind'])) { + $overrides = $options['afterFind']($filtersConfig, $typeMap); + $filtersConfig = $overrides['filtersConfig']; + $typeMap = $overrides['typeMap']; + } $this->Controller->set('typeMap', $typeMap); $this->Controller->set('filters', $filtersName); $this->Controller->set('filtersConfig', $filtersConfig); @@ -292,12 +337,42 @@ class CRUDComponent extends Component public function getFilterFieldsName(): array { $filters = !empty($this->Controller->filterFields) ? $this->Controller->filterFields : []; - $filters = array_map(function($item) { - return is_array($item) ? $item['name'] : $item; - }, $filters); + $filters = array_map( + function ($item) { + return is_array($item) ? $item['name'] : $item; + }, + $filters + ); return $filters; } + /** + * From the provided fields and type mapping, convert any boolean field from the type `checkhox` to the type `radio`. + * + * @param array $fieldsConfig The array of fields to transform + * @param array $typeMap The mapping from the DB to the field type + * @param boolean $includeAny Should an `Any` option be included + * @param boolean $includeBoth Shoud a `Both` option be included + * @return array + */ + public function transformBooleanFieldsIntoRadio(array $fieldsConfig, array $typeMap, bool $includeAny = true, bool $isInline = true): array + { + foreach ($typeMap as $fieldname => $type) { + if ($type == 'boolean') { + $fieldsConfig[$fieldname]['type'] = 'radio'; + $fieldsConfig[$fieldname]['inline'] = $isInline; + $fieldsConfig[$fieldname]['default'] = 'any'; + $fieldsConfig[$fieldname]['options'] = []; + if ($includeAny) { + $fieldsConfig[$fieldname]['options'][] = ['value' => 'any', 'text' => __('Any'),]; + } + $fieldsConfig[$fieldname]['options'][] = ['value' => 1, 'text' => __('Enabled'),]; + $fieldsConfig[$fieldname]['options'][] = ['value' => 0, 'text' => __('Disabled'),]; + } + } + return $fieldsConfig; + } + /** * getResponsePayload Returns the adaquate response payload based on the request context * @@ -313,7 +388,7 @@ class CRUDComponent extends Component return false; } - private function getMetaTemplates(array $metaTemplateConditions=[]) + private function getMetaTemplates(array $metaTemplateConditions = []) { $metaTemplates = []; if (!$this->metaFieldsSupported()) { @@ -323,19 +398,25 @@ class CRUDComponent extends Component $metaQuery = $this->MetaTemplates->find(); $metaQuery ->order(['is_default' => 'DESC']) - ->where(array_merge( - $metaTemplateConditions, - ['scope' => $metaFieldsBehavior->getScope(), ] - )) + ->where( + array_merge( + $metaTemplateConditions, + ['scope' => $metaFieldsBehavior->getScope(),] + ) + ) ->contain('MetaTemplateFields') - ->formatResults(function (\Cake\Collection\CollectionInterface $metaTemplates) { // Set meta-template && meta-template-fields indexed by their ID - return $metaTemplates - ->map(function ($metaTemplate) { - $metaTemplate->meta_template_fields = Hash::combine($metaTemplate->meta_template_fields, '{n}.id', '{n}'); - return $metaTemplate; - }) - ->indexBy('id'); - }); + ->formatResults( + function (\Cake\Collection\CollectionInterface $metaTemplates) { + // Set meta-template && meta-template-fields indexed by their ID + return $metaTemplates->map( + function ($metaTemplate) { + $metaTemplate->meta_template_fields = Hash::combine($metaTemplate->meta_template_fields, '{n}.id', '{n}'); + return $metaTemplate; + } + ) + ->indexBy('id'); + } + ); $metaTemplates = $metaQuery->all(); return $metaTemplates; } @@ -483,7 +564,7 @@ class CRUDComponent extends Component } // prune empty values and marshall fields - public function massageMetaFields($entity, $input, $allMetaTemplates=[]) + public function massageMetaFields($entity, $input, $allMetaTemplates = []) { if (empty($input['MetaTemplates']) || !$this->metaFieldsSupported()) { return ['entity' => $entity, 'metafields_to_delete' => []]; @@ -511,16 +592,19 @@ class CRUDComponent extends Component foreach ($new_meta_fields as $new_value) { if (!empty($new_value)) { $metaField = $metaFieldsTable->newEmptyEntity(); - $metaFieldsTable->patchEntity($metaField, [ - 'value' => $new_value, - 'scope' => $this->Table->getBehavior('MetaFields')->getScope(), - 'field' => $rawMetaTemplateField->field, - 'meta_template_id' => $rawMetaTemplateField->meta_template_id, - 'meta_template_field_id' => $rawMetaTemplateField->id, - 'meta_template_directory_id' => $allMetaTemplates[$template_id]->meta_template_directory_id, - 'parent_id' => $entity->id, - 'uuid' => Text::uuid(), - ]); + $metaFieldsTable->patchEntity( + $metaField, + [ + 'value' => $new_value, + 'scope' => $this->Table->getBehavior('MetaFields')->getScope(), + 'field' => $rawMetaTemplateField->field, + 'meta_template_id' => $rawMetaTemplateField->meta_template_id, + 'meta_template_field_id' => $rawMetaTemplateField->id, + 'meta_template_directory_id' => $allMetaTemplates[$template_id]->meta_template_directory_id, + 'parent_id' => $entity->id, + 'uuid' => Text::uuid(), + ] + ); $entity->meta_fields[] = $metaField; $entity->MetaTemplates[$template_id]->meta_template_fields[$meta_template_field_id]->metaFields[] = $metaField; } @@ -531,9 +615,13 @@ class CRUDComponent extends Component if (isset($metaFieldsIndex[$meta_field_id])) { $index = $metaFieldsIndex[$meta_field_id]; if ($entity->meta_fields[$index]->value != $new_value) { // nothing to do, value hasn't changed - $metaFieldsTable->patchEntity($entity->meta_fields[$index], [ - 'value' => $new_value, 'meta_template_field_id' => $rawMetaTemplateField->id - ], ['value']); + $metaFieldsTable->patchEntity( + $entity->meta_fields[$index], + [ + 'value' => $new_value, 'meta_template_field_id' => $rawMetaTemplateField->id + ], + ['value'] + ); $metaFieldsTable->patchEntity( $entity->MetaTemplates[$template_id]->meta_template_fields[$meta_template_field_id]->metaFields[$meta_field_id], ['value' => $new_value, 'meta_template_field_id' => $rawMetaTemplateField->id], @@ -542,16 +630,19 @@ class CRUDComponent extends Component } } else { // metafield comes from a second post where the temporary entity has already been created $metaField = $metaFieldsTable->newEmptyEntity(); - $metaFieldsTable->patchEntity($metaField, [ - 'value' => $new_value, - 'scope' => $this->Table->getBehavior('MetaFields')->getScope(), // get scope from behavior - 'field' => $rawMetaTemplateField->field, - 'meta_template_id' => $rawMetaTemplateField->meta_template_id, - 'meta_template_field_id' => $rawMetaTemplateField->id, - 'meta_template_directory_id' => $template->meta_template_directory_id, - 'parent_id' => $entity->id, - 'uuid' => Text::uuid(), - ]); + $metaFieldsTable->patchEntity( + $metaField, + [ + 'value' => $new_value, + 'scope' => $this->Table->getBehavior('MetaFields')->getScope(), // get scope from behavior + 'field' => $rawMetaTemplateField->field, + 'meta_template_id' => $rawMetaTemplateField->meta_template_id, + 'meta_template_field_id' => $rawMetaTemplateField->id, + 'meta_template_directory_id' => $template->meta_template_directory_id, + 'parent_id' => $entity->id, + 'uuid' => Text::uuid(), + ] + ); $entity->meta_fields[] = $metaField; $entity->MetaTemplates[$template_id]->meta_template_fields[$meta_template_field_id]->metaFields[] = $metaField; } @@ -608,12 +699,12 @@ class CRUDComponent extends Component $params['contain'] = [$params['contain'], 'MetaFields']; } } - $query = $this->Table->find()->where(["{$this->TableAlias}.id" => $id]); + $query = $this->Table->find()->where(["{$this->Table->getAlias()}.id" => $id]); if (!empty($params['contain'])) { $query->contain($params['contain']); } if (!empty($params['conditions'])) { - $query->where($params['conditions']); + $query->where($params['conditions']); } $data = $query->first(); if ($this->metaFieldsSupported()) { @@ -711,9 +802,14 @@ class CRUDComponent extends Component } $metaFieldScope = $this->Table->getBehavior('MetaFields')->getScope(); $query = $this->MetaTemplates->find()->where(['MetaTemplates.scope' => $metaFieldScope]); - $query->contain(['MetaTemplateFields.MetaFields' => function ($q) use ($id, $metaFieldScope) { - return $q->where(['MetaFields.scope' => $metaFieldScope, 'MetaFields.parent_id' => $id]); - }]); + $query->contain( + [ + 'MetaTemplateFields.MetaFields' => function ($q) use ($id, $metaFieldScope) { + return $q->where(['MetaFields.scope' => $metaFieldScope, 'MetaFields.parent_id' => $id]); + } + + ] + ); $query ->order(['MetaTemplates.is_default' => 'DESC']) ->order(['MetaTemplates.name' => 'ASC']); @@ -756,7 +852,7 @@ class CRUDComponent extends Component return $data; } - public function attachMetaTemplates($data, $metaTemplates, $pruneEmptyDisabled=true) + public function attachMetaTemplates($data, $metaTemplates, $pruneEmptyDisabled = true) { $this->MetaTemplates = TableRegistry::getTableLocator()->get('MetaTemplates'); $metaFields = []; @@ -811,11 +907,13 @@ class CRUDComponent extends Component } } $this->Controller->set('requestedMetaFields', $requestedMetaFields); - return $query->contain([ - 'MetaFields' => [ - 'conditions' => $containConditions + return $query->contain( + [ + 'MetaFields' => [ + 'conditions' => $containConditions + ] ] - ]); + ); } protected function setRequestedEntryAmount() @@ -824,7 +922,7 @@ class CRUDComponent extends Component $tableSettings = IndexSetting::getTableSetting($user, $this->Table); if (!empty($tableSettings['number_of_element'])) { if ($tableSettings['number_of_element'] === 'all') { - $tableSettings['number_of_element'] = 10000; // Even with all, sure not to return too much data + $tableSettings['number_of_element'] = 10000; // Even with all selecect, make sure not to return too much data for the browser } $this->Controller->paginate['limit'] = intval($tableSettings['number_of_element']); } @@ -848,15 +946,29 @@ class CRUDComponent extends Component } } - $data = $this->Table->get($id, $params); + $data = false; + if (Validation::uuid($id) && $this->Table->getSchema()->hasColumn('uuid')) { + $data = $this->Table->find()->where( + [ + "{$this->Table->getAlias()}.uuid" => $id, + ] + )->first(); + } else { + $data = $this->Table->get($id, $params); + } + if (empty($data)) { throw new NotFoundException(__('Invalid {0}.', $this->ObjectAlias)); } if ($this->metaFieldsSupported() && !empty($data['meta_fields'])) { $usedMetaTemplateIDs = array_values(array_unique(Hash::extract($data['meta_fields'], '{n}.meta_template_id'))); - $data = $this->attachMetaTemplatesIfNeeded($data, null, [ - 'id IN' => $usedMetaTemplateIDs - ]); + $data = $this->attachMetaTemplatesIfNeeded( + $data, + null, + [ + 'id IN' => $usedMetaTemplateIDs + ] + ); } if (isset($params['afterFind'])) { $data = $params['afterFind']($data); @@ -870,7 +982,7 @@ class CRUDComponent extends Component $this->Controller->set('entity', $data); } - public function attachMetaTemplatesIfNeeded($data, array $metaTemplates = null, array $metaTemplateConditions=[]) + public function attachMetaTemplatesIfNeeded($data, array $metaTemplates = null, array $metaTemplateConditions = []) { if (!$this->metaFieldsSupported()) { return $data; @@ -878,11 +990,14 @@ class CRUDComponent extends Component if (!is_null($metaTemplates)) { // We might be in the case where $metaTemplates gets re-used in a while loop // We deep copy the meta-template so that the data attached is not preserved for the next iteration - $metaTemplates = array_map(function ($metaTemplate) { - $tmpEntity = $this->MetaTemplates->newEntity($metaTemplate->toArray()); - $tmpEntity['meta_template_fields'] = Hash::combine($tmpEntity['meta_template_fields'], '{n}.id', '{n}'); // newEntity resets array indexing, see https://github.com/cakephp/cakephp/blob/32e3c532fea8abe2db8b697f07dfddf4dfc134ca/src/ORM/Marshaller.php#L369 - return $tmpEntity; - }, $metaTemplates); + $metaTemplates = array_map( + function ($metaTemplate) { + $tmpEntity = $this->MetaTemplates->newEntity($metaTemplate->toArray()); + $tmpEntity['meta_template_fields'] = Hash::combine($tmpEntity['meta_template_fields'], '{n}.id', '{n}'); // newEntity resets array indexing, see https://github.com/cakephp/cakephp/blob/32e3c532fea8abe2db8b697f07dfddf4dfc134ca/src/ORM/Marshaller.php#L369 + return $tmpEntity; + }, + $metaTemplates + ); } else { $metaTemplates = $this->getMetaTemplates($metaTemplateConditions)->toArray(); } @@ -890,59 +1005,49 @@ class CRUDComponent extends Component return $data; } - public function delete($id=false, $params=[]): void + public function delete($id = false, $params = []): void { + $ids = $this->getIds($id); + $query = $this->prepareQueryForIDOrUUID($id); + if (!empty($params['conditions'])) { + $query->where($params['conditions']); + } + if (!empty($params['contain'])) { + $query->contain($params['contain']); + } + $entities = $query->all()->toList(); + if (isset($params['afterFind'])) { + foreach ($entities as $i => $entity) { + $entities[$i] = $params['afterFind']($entity, $params); + } + } if ($this->request->is('get')) { - if(!empty($id)) { - $query = $this->Table->find()->where([$this->Table->getAlias() . '.id' => $id]); - if (!empty($params['conditions'])) { - $query->where($params['conditions']); - } - if (!empty($params['contain'])) { - $query->contain($params['contain']); - } - $data = $query->first(); - if (isset($params['afterFind'])) { - $data = $params['afterFind']($data, $params); - } - if (empty($data)) { - throw new NotFoundException(__('Invalid {0}.', $this->ObjectAlias)); - } - $this->Controller->set('id', $data['id']); - $this->Controller->set('data', $data); - $this->Controller->set('bulkEnabled', false); + $this->Controller->set('entities', $entities); + $this->Controller->set('tableFields', $params['tableFields'] ?? []); + $this->Controller->viewBuilder()->setLayout('ajax'); + if (count($entities) > 1) { + $this->Controller->set('ids', $ids); + $this->Controller->render('/genericTemplates/massDelete'); } else { - $this->Controller->set('bulkEnabled', true); + $this->Controller->set('id', $id); + $this->Controller->render('/genericTemplates/delete'); } } else if ($this->request->is('post') || $this->request->is('delete')) { - $ids = $this->getIdsOrFail($id); $isBulk = count($ids) > 1; $bulkSuccesses = 0; - foreach ($ids as $id) { - $query = $this->Table->find()->where([$this->Table->getAlias() . '.id' => $id]); - if (!empty($params['conditions'])) { - $query->where($params['conditions']); - } - if (!empty($params['contain'])) { - $query->contain($params['contain']); - } - $data = $query->first(); - if (isset($params['afterFind'])) { - $data = $params['afterFind']($data, $params); - } + foreach ($entities as $i => $entity) { if (isset($params['beforeSave'])) { try { - $data = $params['beforeSave']($data); - if ($data === false) { + $entity = $params['beforeSave']($entity); + if ($entity === false) { throw new NotFoundException(__('Could not save {0} due to the input failing to meet expectations. Your input is bad and you should feel bad.', $this->ObjectAlias)); } - } catch (Exception $e) { - $data = false; + } catch (\Exception $e) { + $entity = false; } } - if (!empty($data)) { - $success = $this->Table->delete($data); - $success = true; + if (!empty($entity)) { + $success = $this->Table->delete($entity); } else { $success = false; } @@ -954,7 +1059,7 @@ class CRUDComponent extends Component $bulkSuccesses == count($ids), $isBulk, __('{0} deleted.', $this->ObjectAlias), - __('All {0} have been deleted.', Inflector::pluralize($this->ObjectAlias)), + __('All selected {0} have been deleted.', Inflector::pluralize($this->ObjectAlias)), __('Could not delete {0}.', $this->ObjectAlias), __( '{0} / {1} {2} have been deleted.', @@ -963,21 +1068,14 @@ class CRUDComponent extends Component Inflector::pluralize($this->ObjectAlias) ) ); - $additionalData = []; - if ($bulkSuccesses > 0) { - $additionalData['redirect'] = Router::url(['controller' => $this->Controller->getName(), 'action' => 'index']); - } - $this->setResponseForController('delete', $bulkSuccesses, $message, $data, null, $additionalData); + $this->setResponseForController('massToggle', $bulkSuccesses == count($ids), $message, [], [], []); } - $this->Controller->set('scope', 'users'); - $this->Controller->viewBuilder()->setLayout('ajax'); - $this->Controller->render('/genericTemplates/delete'); } public function tag($id = false): void { if (!$this->taggingSupported()) { - throw new Exception("Table {$this->TableAlias} does not support tagging"); + throw new \Exception("Table {$this->TableAlias} does not support tagging"); } if ($this->request->is('get')) { $this->setAllTags(); @@ -1037,7 +1135,7 @@ class CRUDComponent extends Component public function untag($id = false): void { if (!$this->taggingSupported()) { - throw new Exception("Table {$this->TableAlias} does not support tagging"); + throw new \Exception("Table {$this->TableAlias} does not support tagging"); } if ($this->request->is('get')) { $this->setAllTags(); @@ -1064,9 +1162,12 @@ class CRUDComponent extends Component $input = $this->request->getData(); $tagsToRemove = json_decode($input['tag_list']); // patching will mirror tag in the DB, however, we only want to remove tags - $input['tags'] = array_filter($entity->tags, function ($existingTag) use ($tagsToRemove) { - return !in_array($existingTag->name, $tagsToRemove); - }); + $input['tags'] = array_filter( + $entity->tags, + function ($existingTag) use ($tagsToRemove) { + return !in_array($existingTag->name, $tagsToRemove); + } + ); $patchEntityParams = [ 'fields' => ['tags'], ]; @@ -1099,7 +1200,7 @@ class CRUDComponent extends Component public function viewTags(int $id, array $params = []): void { if (!$this->taggingSupported()) { - throw new Exception("Table {$this->TableAlias} does not support tagging"); + throw new \Exception("Table {$this->TableAlias} does not support tagging"); } if (empty($id)) { throw new NotFoundException(__('Invalid {0}.', $this->ObjectAlias)); @@ -1135,6 +1236,7 @@ class CRUDComponent extends Component } } else { if ($this->Controller->ParamHandler->isRest()) { + $data = $data ?? $message; $this->Controller->restResponsePayload = $this->RestResponse->viewData($data, 'json'); } elseif ($this->Controller->ParamHandler->isAjax()) { if (!empty($additionalData['redirect'])) { // If a redirection occurs, we need to make sure the flash message gets displayed @@ -1166,23 +1268,27 @@ class CRUDComponent extends Component * @throws NotFoundException when no ID could be found */ public function getIdsOrFail($id = false): array + { + $ids = $this->getIds($id); + if (empty($ids)) { + throw new NotFoundException(__('Invalid {0}.', $this->ObjectAlias)); + } + return $ids; + } + + public function getIds($id): array { $params = $this->Controller->ParamHandler->harvestParams(['ids']); if (!empty($params['ids'])) { $params['ids'] = json_decode($params['ids']); } $ids = []; - if (empty($id)) { - if (empty($params['ids'])) { - throw new NotFoundException(__('Invalid {0}.', $this->ObjectAlias)); - } + if (empty($id) && !empty($params['ids'])) { $ids = $params['ids']; } else { $id = $this->getInteger($id); if (!is_null($id)) { $ids = [$id]; - } else { - throw new NotFoundException(__('Invalid {0}.', $this->ObjectAlias)); } } return $ids; @@ -1239,10 +1345,13 @@ class CRUDComponent extends Component $quickFilterFields = $options['quickFilters']; $this->Controller->set('quickFilter', empty($quickFilterFields) ? [] : $quickFilterFields); if ($this->metaFieldsSupported() && !empty($options['quickFilterForMetaField']['enabled'])) { - $this->Controller->set('quickFilterForMetaField', [ - 'enabled' => $options['quickFilterForMetaField']['enabled'] ?? false, - 'wildcard_search' => $options['quickFilterForMetaField']['enabled'] ?? false, - ]); + $this->Controller->set( + 'quickFilterForMetaField', + [ + 'enabled' => $options['quickFilterForMetaField']['enabled'] ?? false, + 'wildcard_search' => $options['quickFilterForMetaField']['enabled'] ?? false, + ] + ); } if (!empty($params['quickFilter']) && !empty($quickFilterFields)) { $this->Controller->set('quickFilterValue', $params['quickFilter']); @@ -1304,6 +1413,7 @@ class CRUDComponent extends Component continue; } $activeFilters[$filter] = $filterValue; + $filterValue = $this->convertBooleanAnyAndBothIfNeeded($filter, $filterValue); if (is_array($filterValue)) { $query = $this->setInCondition($query, $filter, $filterValue); } else { @@ -1349,10 +1459,13 @@ class CRUDComponent extends Component protected function setTagFilters($query, $tags) { $modelAlias = $this->Table->getAlias(); - $subQuery = $this->Table->find('tagged', [ - 'name' => $tags, - 'OperatorAND' => true - ])->select($modelAlias . '.id'); + $subQuery = $this->Table->find( + 'tagged', + [ + 'name' => $tags, + 'OperatorAND' => true + ] + )->select($modelAlias . '.id'); return $query->where([$modelAlias . '.id IN' => $subQuery]); } @@ -1367,23 +1480,27 @@ class CRUDComponent extends Component $query = $this->setRelatedCondition($query, $modelName, $fieldName, $filterValue); } else { $filterParts = array_slice($filterParts, 1); - $query = $query->matching($modelName, function (\Cake\ORM\Query $q) use ($filterParts, $filterValue) { - return $this->setNestedRelatedCondition($q, $filterParts, $filterValue); - }); + $query = $query->matching( + $modelName, + function (\Cake\ORM\Query $q) use ($filterParts, $filterValue) { + return $this->setNestedRelatedCondition($q, $filterParts, $filterValue); + } + ); } return $query; } protected function setRelatedCondition($query, $modelName, $fieldName, $filterValue) { - return $query->matching($modelName, function (\Cake\ORM\Query $q) use ($fieldName, $filterValue) { - if (is_array($filterValue)) { - $query = $this->setInCondition($q, $fieldName, $filterValue); - } else { - $query = $this->setValueCondition($q, $fieldName, $filterValue); + return $query->matching( + $modelName, + function (\Cake\ORM\Query $q) use ($fieldName, $filterValue) { + if (is_array($filterValue)) { + return $this->setInCondition($q, $fieldName, $filterValue); + } + return $this->setValueCondition($q, $fieldName, $filterValue); } - return $query; - }); + ); } protected function setValueCondition($query, $fieldName, $value) @@ -1391,9 +1508,11 @@ class CRUDComponent extends Component if (strlen(trim($value, '%')) === strlen($value)) { return $query->where([$fieldName => $value]); } else { - return $query->where(function ($exp, \Cake\ORM\Query $q) use ($fieldName, $value) { - return $exp->like($fieldName, $value); - }); + return $query->where( + function ($exp, \Cake\ORM\Query $q) use ($fieldName, $value) { + return $exp->like($fieldName, $value); + } + ); } } @@ -1408,13 +1527,17 @@ class CRUDComponent extends Component $operator = $split[1]; } if ($operator == '=') { - return $query->where(function (QueryExpression $exp, Query $q) use ($field, $values) { - return $exp->in($field, $values); - }); + return $query->where( + function (QueryExpression $exp, Query $q) use ($field, $values) { + return $exp->in($field, $values); + } + ); } else if ($operator == '!=') { - return $query->where(function (QueryExpression $exp, Query $q) use ($field, $values) { - return $exp->notIn($field, $values); - }); + return $query->where( + function (QueryExpression $exp, Query $q) use ($field, $values) { + return $exp->notIn($field, $values); + } + ); } } @@ -1478,10 +1601,13 @@ class CRUDComponent extends Component public function setParentConditionsForMetaFields($query, array $metaConditions) { - $metaTemplates = $this->MetaFields->MetaTemplates->find('list', [ - 'keyField' => 'name', - 'valueField' => 'id' - ])->where(['name IN' => array_keys($metaConditions)])->all()->toArray(); + $metaTemplates = $this->MetaFields->MetaTemplates->find( + 'list', + [ + 'keyField' => 'name', + 'valueField' => 'id' + ] + )->where(['name IN' => array_keys($metaConditions)])->all()->toArray(); $fieldsConditions = []; foreach ($metaConditions as $templateName => $templateConditions) { $metaTemplateID = isset($metaTemplates[$templateName]) ? $metaTemplates[$templateName] : -1; @@ -1497,7 +1623,7 @@ class CRUDComponent extends Component private function getParentIDQueryForMetaANDConditions(array $metaANDConditions) { if (empty($metaANDConditions)) { - throw new Exception('Invalid passed conditions'); + throw new \Exception('Invalid passed conditions'); } foreach ($metaANDConditions as $i => $conditions) { $metaANDConditions[$i]['scope'] = $this->Table->getBehavior('MetaFields')->getScope(); @@ -1527,6 +1653,17 @@ class CRUDComponent extends Component return $prefixedConditions; } + protected function convertBooleanAnyAndBothIfNeeded(string $filterName, $filterValue) + { + $typeMap = $this->Table->getSchema()->typeMap(); + if (!empty($typeMap[$filterName]) && $typeMap[$filterName] === 'boolean') { + if ($filterValue === 'any') { + return [true, false]; + } + } + return $filterValue; + } + public function taggingSupported() { return $this->Table->behaviors()->has('Tag'); @@ -1608,7 +1745,78 @@ class CRUDComponent extends Component $this->Controller->render('/genericTemplates/toggle'); } - public function toggleEnabled(int $id, array $path, string $fieldName = 'enabled'): bool + public function massToggle($fieldName, $params): void + { + $ids = $this->getIdsOrFail(false); + $query = $this->Table->find()->where( + function (QueryExpression $exp) use ($ids) { + return $exp->in($this->Table->getAlias() . '.id', $ids); + } + ); + if (!empty($params['conditions'])) { + $query->where($params['conditions']); + } + if (!empty($params['contain'])) { + $query->contain($params['contain']); + } + $entities = $query->all()->toList(); + if ($this->request->is('get')) { + $this->Controller->set('ids', $ids); + $this->Controller->set('entities', $entities); + $this->Controller->set('fieldName', $fieldName); + $this->Controller->set('tableFields', $params['tableFields'] ?? []); + $this->Controller->viewBuilder()->setLayout('ajax'); + $this->Controller->set('fieldName', $fieldName); + $this->Controller->render('/genericTemplates/massToggle'); + } else if ($this->request->is('post') || $this->request->is('delete')) { + $bulkSuccesses = 0; + foreach ($entities as $entity) { + if (isset($params['afterFind'])) { + $entity = $params['afterFind']($entity, $params); + } + if (isset($params['beforeSave'])) { + try { + $entity = $params['beforeSave']($entity); + if ($entity === false) { + throw new NotFoundException(__('Could not save {0} due to the input failing to meet expectations. Your input is bad and you should feel bad.', $this->ObjectAlias)); + } + } catch (\Exception $e) { + $entity = false; + } + } + if (!empty($entity)) { + if (isset($params['force_state'])) { + $entity->{$fieldName} = $params['force_state']; + } else { + $entity->{$fieldName} = !$entity->{$fieldName}; + } + $savedData = $this->Table->save($entity); + $success = $savedData !== false; + } else { + $success = false; + } + if ($success) { + $bulkSuccesses++; + } + } + $message = $this->getMessageBasedOnResult( + $bulkSuccesses == count($ids), + true, + __('{0} updated.', $this->ObjectAlias), + __('All selected {0} have been updated.', Inflector::pluralize($this->ObjectAlias)), + __('Could not update {0}.', $this->ObjectAlias), + __( + '{0} / {1} {2} have been updated.', + $bulkSuccesses, + count($ids), + Inflector::pluralize($this->ObjectAlias) + ) + ); + $this->setResponseForController('massToggle', $bulkSuccesses == count($ids), $message, [], [], []); + } + } + + public function toggleEnabled(int $id, array $path, string $fieldName = 'enabled'): void { if (empty($id)) { throw new NotFoundException(__('Invalid {0}.', $this->ObjectAlias)); @@ -1644,15 +1852,16 @@ class CRUDComponent extends Component $query = $associatedTable->find()->rightJoin( [$this->Table->getAlias() => $this->Table->getTable()], [sprintf('%s.id = %s.%s', $this->Table->getAlias(), $associatedTable->getAlias(), $association->getForeignKey())] - ) - ->where([ - ["$field IS NOT" => NULL] - ]); + )->where( + [ + ["$field IS NOT" => null] + ] + ); } else if ($associationType == 'manyToOne') { $fieldToExtract = sprintf('%s.%s', Inflector::singularize(strtolower($model)), $subField); $query = $this->Table->find()->contain($model); } else { - throw new Exception("Association $associationType not supported in CRUD Component"); + throw new \Exception("Association $associationType not supported in CRUD Component"); } } else { $fieldToExtract = $field; @@ -1684,6 +1893,31 @@ class CRUDComponent extends Component return $filters; } + /** + * Prepare a find query for either the provided id/uuid or a list of json-encoded ids + * + * @param mixed $id Can be either ID, UUID or unset + * @return Query + * @throws NotFoundException + */ + public function prepareQueryForIDOrUUID($id = false): Query + { + $query = $this->Table->find(); + $ids = $this->getIds($id); + if (Validation::uuid($id) && $this->Table->getSchema()->hasColumn('uuid')) { + $query->where(["{$this->Table->getAlias()}.uuid" => $id,]); + } else if (count($ids) > 0) { + $query->where( + function (QueryExpression $exp) use ($ids) { + return $exp->in($this->Table->getAlias() . '.id', $ids); + } + ); + } else { + throw new NotFoundException(__('Invalid {0}.', $this->ObjectAlias)); + } + return $query; + } + private function renderViewInVariable($templateRelativeName, $data) { $builder = new ViewBuilder(); From e111dacf1700f0eae35e4bf4df2bc306159f8670 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Fri, 29 Mar 2024 16:32:50 +0100 Subject: [PATCH 2/9] fix: [app] Fixed some deprecation notices --- src/Controller/AppController.php | 4 ++++ src/Model/Behavior/MetaFieldsBehavior.php | 1 + 2 files changed, 5 insertions(+) diff --git a/src/Controller/AppController.php b/src/Controller/AppController.php index 238dc46..e095561 100644 --- a/src/Controller/AppController.php +++ b/src/Controller/AppController.php @@ -43,6 +43,10 @@ class AppController extends Controller public $breadcrumb = []; public $request_ip = null; + public $MetaFields = null; + public $MetaTemplates = null; + public $Users = null; + /** * Initialization hook method. * diff --git a/src/Model/Behavior/MetaFieldsBehavior.php b/src/Model/Behavior/MetaFieldsBehavior.php index 173439d..504d3ef 100644 --- a/src/Model/Behavior/MetaFieldsBehavior.php +++ b/src/Model/Behavior/MetaFieldsBehavior.php @@ -45,6 +45,7 @@ class MetaFieldsBehavior extends Behavior private $aliasScope = null; private $typeHandlers = []; + private $_metaTemplateFieldTable; public function initialize(array $config): void { From 3a3494df8c9428ba6a87142ae1c55f65cf8f793e Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Fri, 29 Mar 2024 16:33:33 +0100 Subject: [PATCH 3/9] fix: [encryptionKeys:ui] Aligned UI with what users can actually do --- src/Controller/EncryptionKeysController.php | 24 +++------ src/Model/Table/EncryptionKeysTable.php | 54 +++++++++++++++++++++ templates/EncryptionKeys/index.php | 14 +++++- 3 files changed, 73 insertions(+), 19 deletions(-) diff --git a/src/Controller/EncryptionKeysController.php b/src/Controller/EncryptionKeysController.php index a01adf7..0bbc74e 100644 --- a/src/Controller/EncryptionKeysController.php +++ b/src/Controller/EncryptionKeysController.php @@ -21,6 +21,7 @@ class EncryptionKeysController extends AppController public function index() { + $currentUser = $this->ACL->getUser(); $this->EncryptionKeys->initializeGpg(); $Model = $this->EncryptionKeys; $this->CRUD->index([ @@ -33,7 +34,7 @@ class EncryptionKeysController extends AppController ], 'contain' => $this->containFields, 'statisticsFields' => $this->statisticsFields, - 'afterFind' => function($data) use ($Model) { + 'afterFind' => function($data) use ($Model, $currentUser) { if ($data['type'] === 'pgp') { $keyInfo = $Model->verifySingleGPG($data); $data['status'] = __('OK'); @@ -45,6 +46,7 @@ class EncryptionKeysController extends AppController $data['fingerprint'] = $keyInfo[4]; } } + $data['_canBeEdited'] = $Model->canEdit($currentUser, $data); return $data; } ]); @@ -96,24 +98,12 @@ class EncryptionKeysController extends AppController } $params['beforeSave'] = function($entity) use($currentUser) { if ($entity['owner_model'] === 'organisation') { - if ($entity['owner_id'] !== $currentUser['organisation_id']) { + if (!$this->EncryptionKeys->canEditForOrganisation($currentUser, $entity)) { throw new MethodNotAllowedException(__('Selected organisation cannot be linked by the current user.')); } - } else { - if ($currentUser['role']['perm_org_admin']) { - $this->loadModel('Alignments'); - $validIndividuals = $this->Alignments->find('list', [ - 'keyField' => 'individual_id', - 'valueField' => 'id', - 'conditions' => ['organisation_id' => $currentUser['organisation_id']] - ])->toArray(); - if (!isset($validIndividuals[$entity['owner_id']])) { - throw new MethodNotAllowedException(__('Selected individual cannot be linked by the current user.')); - } - } else { - if ($entity['owner_id'] !== $currentUser['id']) { - throw new MethodNotAllowedException(__('Selected individual cannot be linked by the current user.')); - } + } else if ($entity['owner_model'] === 'individual') { + if (!$this->EncryptionKeys->canEditForIndividual($currentUser, $entity)) { + throw new MethodNotAllowedException(__('Selected individual cannot be linked by the current user.')); } } return $entity; diff --git a/src/Model/Table/EncryptionKeysTable.php b/src/Model/Table/EncryptionKeysTable.php index 0015e1d..5f4d54a 100644 --- a/src/Model/Table/EncryptionKeysTable.php +++ b/src/Model/Table/EncryptionKeysTable.php @@ -3,6 +3,7 @@ namespace App\Model\Table; use App\Model\Table\AppTable; +use Cake\ORM\TableRegistry; use Cake\ORM\Table; use Cake\Validation\Validator; use Cake\Event\EventInterface; @@ -147,4 +148,57 @@ class EncryptionKeysTable extends AppTable return null; } } + + public function canEdit($user, $entity): bool + { + if ($entity['owner_model'] === 'organisation') { + return $this->canEditForOrganisation($user, $entity); + } else if ($entity['owner_model'] === 'individual') { + return $this->canEditForIndividual($user, $entity); + } + return false; + } + + public function canEditForOrganisation($user, $entity): bool + { + if ($entity['owner_model'] !== 'organisation') { + return false; + } + if (!empty($user['role']['perm_admin'])) { + return true; + } + if ( + $user['role']['perm_org_admin'] && + $entity['owner_id'] === $user['organisation_id'] + ) { + return true; + } + return false; + } + + public function canEditForIndividual($user, $entity): bool + { + if ($entity['owner_model'] !== 'individual') { + return false; + } + if (!empty($user['role']['perm_admin'])) { + return true; + } + if ($user['role']['perm_org_admin']) { + $this->Alignments = TableRegistry::get('Alignments'); + $validIndividuals = $this->Alignments->find('list', [ + 'keyField' => 'individual_id', + 'valueField' => 'id', + 'conditions' => ['organisation_id' => $user['organisation_id']] + ])->toArray(); + if (isset($validIndividuals[$entity['owner_id']])) { + return true; + } + } else { + if ($entity['owner_id'] === $user['id']) { + return true; + } + } + return false; + } } diff --git a/templates/EncryptionKeys/index.php b/templates/EncryptionKeys/index.php index c9ca457..8a1cd67 100644 --- a/templates/EncryptionKeys/index.php +++ b/templates/EncryptionKeys/index.php @@ -80,12 +80,22 @@ echo $this->element('genericElements/IndexTable/index_table', [ [ 'open_modal' => '/encryptionKeys/edit/[onclick_params_data_path]', 'modal_params_data_path' => 'id', - 'icon' => 'edit' + 'icon' => 'edit', + 'complex_requirement' => [ + 'function' => function ($row, $options) { + return $row['_canBeEdited']; + } + ] ], [ 'open_modal' => '/encryptionKeys/delete/[onclick_params_data_path]', 'modal_params_data_path' => 'id', - 'icon' => 'trash' + 'icon' => 'trash', + 'complex_requirement' => [ + 'function' => function ($row, $options) { + return $row['_canBeEdited']; + } + ] ], ] ] From 91cd9eb1806ddbdf7eb97bb5ef5e3cf25285e771 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Fri, 29 Mar 2024 16:46:47 +0100 Subject: [PATCH 4/9] fix: [encryptionKeys:add] Make sure to compare owner_id with individual_id --- src/Model/Table/EncryptionKeysTable.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Model/Table/EncryptionKeysTable.php b/src/Model/Table/EncryptionKeysTable.php index 5f4d54a..00b9ab4 100644 --- a/src/Model/Table/EncryptionKeysTable.php +++ b/src/Model/Table/EncryptionKeysTable.php @@ -195,7 +195,7 @@ class EncryptionKeysTable extends AppTable return true; } } else { - if ($entity['owner_id'] === $user['id']) { + if ($entity['owner_id'] === $user['individual_id']) { return true; } } From f4f17a1fa0c7d0d1e7940ac7d3955957caa39c48 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Tue, 2 Apr 2024 11:40:54 +0200 Subject: [PATCH 5/9] fix: [genericTemplates:filters] Fixed encoding typo --- templates/genericTemplates/filters.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/genericTemplates/filters.php b/templates/genericTemplates/filters.php index 6117b9e..e166019 100644 --- a/templates/genericTemplates/filters.php +++ b/templates/genericTemplates/filters.php @@ -102,7 +102,7 @@ $filteringTags = ''; if ($taggingEnabled) { $helpText = $this->Bootstrap->node('sup', [ 'class' => ['ms-1 fa fa-info'], - 'title' => __('Supports negation matches (with the `!` character) and LIKE matches (with the `%` character). Example: `!exportable`, `%able`'), + 'title' => __('Supports negation matches (with the `!` character) and LIKE matches (with the `%` character). Example: `!exportable`, `%able`'), 'data-bs-toggle' => 'tooltip', ]); $filteringTags = $this->Bootstrap->node('h5', [ From acb8fa21d3d06db8127d4609248becdada2f2f02 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Tue, 2 Apr 2024 13:51:56 +0200 Subject: [PATCH 6/9] chg: [users:index] Added confirmation modal before changing disabled state for users --- templates/Users/index.php | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/templates/Users/index.php b/templates/Users/index.php index 13b434d..f6d12bf 100644 --- a/templates/Users/index.php +++ b/templates/Users/index.php @@ -54,7 +54,27 @@ echo $this->element('genericElements/IndexTable/index_table', [ return true; }, ], - 'skip_full_reload' => true + 'skip_full_reload' => true, + 'confirm' => [ + 'enable' => [ + 'titleHtml' => __('Confirm disabling the user?'), + 'type' => 'confirm-warning', + 'bodyHtml' => __('You\'re about to change the state of the user {{0}}.'), + 'confirmText' => __('Disable user'), + 'arguments' => [ + 'bodyHtml' => ['individual.email'], + ] + ], + 'disable' => [ + 'titleHtml' => __('Confirm enabling the user?'), + 'type' => 'confirm-success', + 'bodyHtml' => __('You\'re about to change the state of the user {{0}}.'), + 'confirmText' => __('Enable user'), + 'arguments' => [ + 'bodyHtml' => ['individual.email'], + ] + ] + ] ] ], [ @@ -168,5 +188,4 @@ echo $this->element('genericElements/IndexTable/index_table', [ ] ] ]); -echo ''; ?> From 1111290cea4bc7d63290bdb772fd42d06420dacc Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Tue, 2 Apr 2024 14:02:03 +0200 Subject: [PATCH 7/9] chg: [individuals:filtering] Added dropdown for organisation filtering --- src/Controller/IndividualsController.php | 10 +++++++++- src/Model/Table/IndividualsTable.php | 8 ++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/Controller/IndividualsController.php b/src/Controller/IndividualsController.php index 47a9957..776f75f 100644 --- a/src/Controller/IndividualsController.php +++ b/src/Controller/IndividualsController.php @@ -14,7 +14,15 @@ use Cake\ORM\TableRegistry; class IndividualsController extends AppController { public $quickFilterFields = ['uuid', ['email' => true], ['first_name' => true], ['last_name' => true], 'position']; - public $filterFields = ['uuid', 'email', 'first_name', 'last_name', 'position', 'Organisations.id', 'Alignments.type']; + public $filterFields = [ + 'uuid', + 'email', + 'first_name', + 'last_name', + 'position', + 'Alignments.type', + ['name' => 'Organisations.id', 'multiple' => true, 'options' => 'getAllOrganisations', 'select2' => true], + ]; public $containFields = ['Alignments' => 'Organisations']; public $statisticsFields = ['position']; diff --git a/src/Model/Table/IndividualsTable.php b/src/Model/Table/IndividualsTable.php index e3ab184..6e85a3b 100644 --- a/src/Model/Table/IndividualsTable.php +++ b/src/Model/Table/IndividualsTable.php @@ -3,6 +3,7 @@ namespace App\Model\Table; use App\Model\Table\AppTable; +use Cake\Utility\Hash; use Cake\ORM\Table; use Cake\Validation\Validator; use Cake\ORM\Query; @@ -137,4 +138,11 @@ class IndividualsTable extends AppTable )->all()->extract('individual_id')->toArray(); return $validIndividualIds; } + + public function getAllOrganisations($currentUser): array + { + $this->Organisations = \Cake\ORM\TableRegistry::getTableLocator()->get('Organisations'); + $orgs = $this->Organisations->find()->select(['id', 'name'])->all()->toList(); + return Hash::combine($orgs, '{n}.id', '{n}.name'); + } } From 96d53f4b6a556d4c443b1c58958f6265f0db41d3 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Tue, 2 Apr 2024 14:06:25 +0200 Subject: [PATCH 8/9] chg: [users:filtering] Added dropdown for users filtering --- src/Controller/UsersController.php | 10 +++++++++- src/Model/Table/UsersTable.php | 5 +++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Controller/UsersController.php b/src/Controller/UsersController.php index 6fdbcdc..863af88 100644 --- a/src/Controller/UsersController.php +++ b/src/Controller/UsersController.php @@ -11,7 +11,15 @@ use Cake\Http\Exception\NotFoundException; class UsersController extends AppController { - public $filterFields = ['Individuals.uuid', 'username', 'Individuals.email', 'Individuals.first_name', 'Individuals.last_name', 'Organisations.name', 'Organisations.nationality']; + public $filterFields = [ + 'Individuals.uuid', + 'username', + 'Individuals.email', + 'Individuals.first_name', + 'Individuals.last_name', + ['name' => 'Organisations.id', 'multiple' => true, 'options' => 'getAllOrganisations', 'select2' => true], + 'Organisations.nationality' + ]; public $quickFilterFields = ['Individuals.uuid', ['username' => true], ['Individuals.first_name' => true], ['Individuals.last_name' => true], 'Individuals.email']; public $containFields = ['Individuals', 'Roles', 'UserSettings', 'Organisations', 'OrgGroups']; diff --git a/src/Model/Table/UsersTable.php b/src/Model/Table/UsersTable.php index e7bcb1f..4cb2dde 100644 --- a/src/Model/Table/UsersTable.php +++ b/src/Model/Table/UsersTable.php @@ -292,4 +292,9 @@ class UsersTable extends AppTable } return true; } + + public function getAllOrganisations($currentUser) { + $this->Individuals = TableRegistry::get('Individuals'); + return $this->Individuals->getAllOrganisations($currentUser); + } } From 305274f6369112b0cb40e3b2e348d8a0f6513a44 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Tue, 2 Apr 2024 14:15:47 +0200 Subject: [PATCH 9/9] fix: [component:CRUD] Make sure to append model alias when ordering results --- src/Controller/Component/CRUDComponent.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Controller/Component/CRUDComponent.php b/src/Controller/Component/CRUDComponent.php index 3c51bef..da00194 100644 --- a/src/Controller/Component/CRUDComponent.php +++ b/src/Controller/Component/CRUDComponent.php @@ -97,6 +97,8 @@ class CRUDComponent extends Component if ($sort[0] != $this->Table->getAlias()) { $sort[0] = Inflector::camelize(Inflector::pluralize($sort[0])); } + } else { + array_unshift($sort, $this->Table->getAlias()); } $sort = implode('.', $sort); $query->order($sort . ' ' . $direction);