chg: [CRUD] Improved metaFields filtering capabilities

pull/40/head
mokaddem 2021-02-26 10:36:06 +01:00
parent 5d1106e82a
commit d4001fab18
6 changed files with 174 additions and 35 deletions

View File

@ -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)) {

View File

@ -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();

View File

@ -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;
}
}

View File

@ -38,7 +38,7 @@ class OrganisationsTable extends AppTable
[
'dependent' => true,
'foreignKey' => 'parent_id',
'conditions' => ['scope' => 'organisation']
'conditions' => ['MetaFields.scope' => 'organisation']
]
);
$this->setDisplayField('name');

View File

@ -15,6 +15,10 @@ echo $this->element('genericElements/IndexTable/index_table', [
]
]
],
[
'type' => 'context_filters',
'context_filters' => $filteringContexts
],
[
'type' => 'search',
'button' => __('Filter'),

View File

@ -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' => [