From d4001fab18ed0939807e5a2d550157fc1aeb7165 Mon Sep 17 00:00:00 2001 From: mokaddem Date: Fri, 26 Feb 2021 10:36:06 +0100 Subject: [PATCH] chg: [CRUD] Improved metaFields filtering capabilities --- src/Controller/Component/CRUDComponent.php | 146 ++++++++++++++---- src/Controller/OrganisationsController.php | 41 ++++- src/Model/Table/MetaFieldsTable.php | 2 + src/Model/Table/OrganisationsTable.php | 2 +- templates/Organisations/index.php | 4 + .../ListTopBar/group_context_filters.php | 14 +- 6 files changed, 174 insertions(+), 35 deletions(-) diff --git a/src/Controller/Component/CRUDComponent.php b/src/Controller/Component/CRUDComponent.php index 5bd1a5a..7b29d0e 100644 --- a/src/Controller/Component/CRUDComponent.php +++ b/src/Controller/Component/CRUDComponent.php @@ -30,9 +30,10 @@ class CRUDComponent extends Component } $options['filters'][] = 'quickFilter'; } + $options['filters'][] = 'filteringLabel'; $params = $this->Controller->ParamHandler->harvestParams(empty($options['filters']) ? [] : $options['filters']); $query = $this->Table->find(); - $query = $this->setFilters($params, $query); + $query = $this->setFilters($params, $query, $options); $query = $this->setQuickFilters($params, $query, empty($options['quickFilters']) ? [] : $options['quickFilters']); if (!empty($options['contain'])) { $query->contain($options['contain']); @@ -243,22 +244,35 @@ class CRUDComponent extends Component if (empty($this->Table->metaFields)) { return $data; } - $query = $this->MetaFields->MetaTemplates->find(); - $metaFields = $this->Table->metaFields; - $query->contain('MetaTemplateFields', function ($q) use ($id, $metaFields) { - return $q->innerJoinWith('MetaFields') - ->where(['MetaFields.scope' => $metaFields, 'MetaFields.parent_id' => $id]); - }); - $query->innerJoinWith('MetaTemplateFields', function ($q) { - return $q->contain('MetaFields')->innerJoinWith('MetaFields'); - }); - $query->group(['MetaTemplates.id', 'MetaTemplates.scope', 'MetaTemplates.name', 'MetaTemplates.namespace', 'MetaTemplates.description', 'MetaTemplates.version', 'MetaTemplates.uuid', 'MetaTemplates.source', 'MetaTemplates.enabled', 'MetaTemplates.is_default']) - ->order(['MetaTemplates.is_default' => 'DESC']); - $metaTemplates = $query->all(); + $metaFieldScope = $this->Table->metaFields; + $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 + ->order(['MetaTemplates.is_default' => 'DESC']) + ->order(['MetaTemplates.name' => 'ASC']); + $metaTemplates = $query->all()->toArray(); + $metaTemplates = $this->pruneEmptyMetaTemplates($metaTemplates); $data['metaTemplates'] = $metaTemplates; return $data; } + public function pruneEmptyMetaTemplates($metaTemplates) + { + foreach ($metaTemplates as $i => $metaTemplate) { + foreach ($metaTemplate['meta_template_fields'] as $j => $metaTemplateField) { + if (empty($metaTemplateField['meta_fields'])) { + unset($metaTemplates[$i]['meta_template_fields'][$j]); + } + } + if (empty($metaTemplates[$i]['meta_template_fields'])) { + unset($metaTemplates[$i]); + } + } + return $metaTemplates; + } + public function getMetaFields($id, $data) { if (empty($this->Table->metaFields)) { @@ -361,28 +375,51 @@ class CRUDComponent extends Component return $query; } - protected function setFilters($params, \Cake\ORM\Query $query): \Cake\ORM\Query + protected function setFilters($params, \Cake\ORM\Query $query, array $options): \Cake\ORM\Query { - $params = $this->massageFilters($params); - $conditions = array(); - if (!empty($params['simpleFilters'])) { - foreach ($params['simpleFilters'] as $filter => $filterValue) { - if ($filter === 'quickFilter') { - continue; - } - if (is_array($filterValue)) { - $query->where([($filter . ' IN') => $filterValue]); - } else { - $query = $this->setValueCondition($query, $filter, $filterValue); + $filteringLabel = !empty($params['filteringLabel']) ? $params['filteringLabel'] : ''; + unset($params['filteringLabel']); + $customFilteringFunction = ''; + $chosenFilter = ''; + if (!empty($options['contextFilters']['custom'])) { + foreach ($options['contextFilters']['custom'] as $filter) { + if ($filter['label'] == $filteringLabel) { + $customFilteringFunction = $filter; + $chosenFilter = $filter; + break; } } } - if (!empty($params['relatedFilters'])) { - foreach ($params['relatedFilters'] as $filter => $filterValue) { - $filterParts = explode('.', $filter); - $query = $this->setNestedRelatedCondition($query, $filterParts, $filterValue); + + if (!empty($customFilteringFunction['filterConditionFunction'])) { + $query = $customFilteringFunction['filterConditionFunction']($query); + } else { + if (!empty($chosenFilter)) { + $params = $this->massageFilters($chosenFilter['filterCondition']); + } else { + $params = $this->massageFilters($params); + } + $conditions = array(); + if (!empty($params['simpleFilters'])) { + foreach ($params['simpleFilters'] as $filter => $filterValue) { + if ($filter === 'quickFilter') { + continue; + } + if (is_array($filterValue)) { + $query->where([($filter . ' IN') => $filterValue]); + } else { + $query = $this->setValueCondition($query, $filter, $filterValue); + } + } + } + if (!empty($params['relatedFilters'])) { + foreach ($params['relatedFilters'] as $filter => $filterValue) { + $filterParts = explode('.', $filter); + $query = $this->setNestedRelatedCondition($query, $filterParts, $filterValue); + } } } + return $query; } @@ -447,6 +484,57 @@ class CRUDComponent extends Component $this->Controller->set('filteringContexts', $filteringContexts); } + public function getParentsForMetaFields($query, array $metaConditions) + { + $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; + foreach ($templateConditions as $conditions) { + $conditions['meta_template_id'] = $metaTemplateID; + $fieldsConditions[] = $conditions; + } + } + $matchingMetaQuery = $this->getParentIDQueryForMetaANDConditions($fieldsConditions); + return $query->where(['id IN' => $matchingMetaQuery]); + } + + private function getParentIDQueryForMetaANDConditions(array $metaANDConditions) + { + if (empty($metaANDConditions)) { + throw new Exception('Invalid passed conditions'); + } + foreach ($metaANDConditions as $i => $conditions) { + $metaANDConditions[$i]['scope'] = $this->Table->metaFields; + } + $firstCondition = $this->prefixConditions('MetaFields', $metaANDConditions[0]); + $conditionsToJoin = array_slice($metaANDConditions, 1); + $query = $this->MetaFields->find() + ->select('parent_id') + ->where($firstCondition); + foreach ($conditionsToJoin as $i => $conditions) { + $joinedConditions = $this->prefixConditions("m{$i}", $conditions); + $joinedConditions[] = "m{$i}.parent_id = MetaFields.parent_id"; + $query->rightJoin( + ["m{$i}" => 'meta_fields'], + $joinedConditions + ); + } + return $query; + } + + private function prefixConditions(string $prefix, array $conditions) + { + $prefixedConditions = []; + foreach ($conditions as $condField => $condValue) { + $prefixedConditions["${prefix}.${condField}"] = $condValue; + } + return $prefixedConditions; + } + public function toggle(int $id, string $fieldName = 'enabled', array $params = []): void { if (empty($id)) { diff --git a/src/Controller/OrganisationsController.php b/src/Controller/OrganisationsController.php index 9a599c3..c3b8e0e 100644 --- a/src/Controller/OrganisationsController.php +++ b/src/Controller/OrganisationsController.php @@ -15,8 +15,45 @@ class OrganisationsController extends AppController public function index() { $this->CRUD->index([ - 'filters' => ['name', 'uuid', 'nationality', 'sector', 'type', 'url', 'Alignments.id'], - 'quickFilters' => ['name', 'uuid', 'nationality', 'sector', 'type', 'url'], + 'filters' => ['name', 'uuid', 'nationality', 'sector', 'type', 'url', 'Alignments.id', 'MetaFields.field', 'MetaFields.value', 'MetaFields.MetaTemplates.name'], + 'quickFilters' => [['name' => true], 'uuid', 'nationality', 'sector', 'type', 'url'], + 'contextFilters' => [ + 'custom' => [ + [ + 'label' => __('ENISA Accredited'), + 'filterCondition' => [ + 'MetaFields.field' => 'enisa-tistatus', + 'MetaFields.value' => 'Accredited', + 'MetaFields.MetaTemplates.name' => 'ENISA CSIRT Network' + ] + ], + [ + 'label' => __('ENISA not-Accredited'), + 'filterCondition' => [ + 'MetaFields.field' => 'enisa-tistatus', + 'MetaFields.value !=' => 'Accredited', + 'MetaFields.MetaTemplates.name' => 'ENISA CSIRT Network' + ] + ], + [ + 'label' => __('ENISA CSIRT Network (GOV)'), + 'filterConditionFunction' => function($query) { + return $this->CRUD->getParentsForMetaFields($query, [ + 'ENISA CSIRT Network' => [ + [ + 'field' => 'constituency', + 'value LIKE' => '%Government%', + ], + [ + 'field' => 'csirt-network-status', + 'value' => 'Member', + ], + ] + ]); + } + ] + ], + ], 'contain' => ['Alignments' => 'Individuals'] ]); $responsePayload = $this->CRUD->getResponsePayload(); diff --git a/src/Model/Table/MetaFieldsTable.php b/src/Model/Table/MetaFieldsTable.php index d3cca97..ba4cc66 100644 --- a/src/Model/Table/MetaFieldsTable.php +++ b/src/Model/Table/MetaFieldsTable.php @@ -27,6 +27,8 @@ class MetaFieldsTable extends AppTable ->notEmptyString('meta_template_id') ->notEmptyString('meta_template_field_id') ->requirePresence(['scope', 'field', 'value', 'uuid', 'meta_template_id', 'meta_template_field_id'], 'create'); + + // add validation regex return $validator; } } diff --git a/src/Model/Table/OrganisationsTable.php b/src/Model/Table/OrganisationsTable.php index 5eeb756..446d3fa 100644 --- a/src/Model/Table/OrganisationsTable.php +++ b/src/Model/Table/OrganisationsTable.php @@ -38,7 +38,7 @@ class OrganisationsTable extends AppTable [ 'dependent' => true, 'foreignKey' => 'parent_id', - 'conditions' => ['scope' => 'organisation'] + 'conditions' => ['MetaFields.scope' => 'organisation'] ] ); $this->setDisplayField('name'); diff --git a/templates/Organisations/index.php b/templates/Organisations/index.php index 5358dd1..dd3d7b7 100644 --- a/templates/Organisations/index.php +++ b/templates/Organisations/index.php @@ -15,6 +15,10 @@ echo $this->element('genericElements/IndexTable/index_table', [ ] ] ], + [ + 'type' => 'context_filters', + 'context_filters' => $filteringContexts + ], [ 'type' => 'search', 'button' => __('Filter'), diff --git a/templates/element/genericElements/ListTopBar/group_context_filters.php b/templates/element/genericElements/ListTopBar/group_context_filters.php index 53e37e1..5a64a74 100644 --- a/templates/element/genericElements/ListTopBar/group_context_filters.php +++ b/templates/element/genericElements/ListTopBar/group_context_filters.php @@ -5,10 +5,11 @@ $urlParams = [ 'controller' => $this->request->getParam('controller'), 'action' => 'index', - '?' => $filteringContext['filterCondition'] + '?' => array_merge($filteringContext['filterCondition'], ['filteringLabel' => $filteringContext['label']]) ]; $currentQuery = $this->request->getQuery(); - unset($currentQuery['page'], $currentQuery['limit'], $currentQuery['sort']); + $filteringLabel = !empty($currentQuery['filteringLabel']) ? $currentQuery['filteringLabel'] : ''; + unset($currentQuery['page'], $currentQuery['limit'], $currentQuery['sort'], $currentQuery['filteringLabel']); if (!empty($filteringContext['filterCondition'])) { // PHP replaces `.` by `_` when fetching the request parameter $currentFilteringContext = []; foreach ($filteringContext['filterCondition'] as $currentFilteringContextKey => $value) { @@ -18,7 +19,14 @@ $currentFilteringContext = $filteringContext['filterCondition']; } $contextArray[] = [ - 'active' => $currentQuery == $currentFilteringContext, + 'active' => ( + ( + $currentQuery == $currentFilteringContext && // query conditions match + !isset($filteringContext['filterConditionFunction']) && // not a custom filtering + empty($filteringLabel) // do not check `All` by default + ) || + $filteringContext['label'] == $filteringLabel // labels should not be duplicated + ), 'isFilter' => true, 'onClick' => 'changeIndexContext', 'onClickParams' => [