Skip to content

CFM-291_Ability_to_filter_members #181

Merged
merged 5 commits into from
Apr 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions app/src/Controller/StandardController.php
Original file line number Diff line number Diff line change
Expand Up @@ -719,9 +719,7 @@ protected function populateAutoViewVars(object $obj=null) {
$modelsName = $this->name;
// $table = the actual table object
$table = $this->$modelsName;
// XXX We assume that all models that load the Tree behavior will
// implement a potentialParents method
$this->set($vvar, $table->potentialParents($this->getCOID()));
$this->set($vvar, $table->getParents($this->getCOID()));
break;
case 'plugin':
$PluginTable = $this->getTableLocator()->get('Plugins');
Expand Down
92 changes: 57 additions & 35 deletions app/src/Lib/Traits/IndexQueryTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,45 +57,65 @@ public function constructGetIndexContains(Query $query): object {
$containClause = $table->getIndexContains();
}

// Examples:
// 1. GET https://example.com/registry-pe/api/v2/people?co_id=2&limit=10&extended=PrimaryName,EmailAddresses,Identifiers
// 2. GET https://example.com/registry-pe/api/v2/people?co_id=2&limit=10&extended=on
// 2. GET https://example.com/registry-pe/api/v2/people?co_id=2&limit=10&extended=all
// 1. GET https://example.com/registry-pe/api/v2/people?co_id=2&limit=10
if($this->request->is('restful')|| $this->request->is('ajax')) {
// Restfull and ajax do not include the IndexContains by default.
$containClause = [];
// Set the extended query param to `on` in order to fetch the indexContains
if(
$this->request->getQuery('extended') &&
filter_var($this->request->getQuery('extended'), FILTER_VALIDATE_BOOLEAN)
) {
$containClause = $table->getIndexContains();
} elseif(
$this->request->getQuery('extended') &&
$this->request->getQuery('extended') === 'all'
) {
// Get all the associated models
$associations = $table->associations();
foreach($associations->getIterator() as $a) {
$containClause = $this->containClauseFromQueryParams();
}

return empty($containClause) ? $query : $query->contain($containClause);
}


/**
* Construct the Contain Clause from the query parameters of an AJAX or REST call
*
* Examples:
* 1. GET https://example.com/registry-pe/api/v2/people?co_id=2&limit=10&extended=PrimaryName,EmailAddresses,Identifiers
* 2. GET https://example.com/registry-pe/api/v2/people?co_id=2&limit=10&extended=on
* 3. GET https://example.com/registry-pe/api/v2/people?co_id=2&limit=10&extended=all
* 4. GET https://example.com/registry-pe/api/v2/people?co_id=2&limit=10
*
* @return array Contain Clause
* @since COmanage Registry v5.0.0
*/
public function containClauseFromQueryParams(): array
{
// $this->name = Models
$modelsName = $this->name;
// $table = the actual table object
$table = $this->$modelsName;

// Restfull and ajax do not include the IndexContains by default.
$containClause = [];
// Set the extended query param to `on` in order to fetch the indexContains
if(
$this->request->getQuery('extended') &&
filter_var($this->request->getQuery('extended'), FILTER_VALIDATE_BOOLEAN)
) {
$containClause = $table->getIndexContains();
} elseif(
$this->request->getQuery('extended') &&
$this->request->getQuery('extended') === 'all'
) {
// Get all the associated models
$associations = $table->associations();
foreach($associations->getIterator() as $a) {
$containClause[] = $a->getName();
}
} elseif (
$this->request->getQuery('extended')
&& \is_string($this->request->getQuery('extended'))
) {
// Get ONLY the associated models requested
$associations = $table->associations();
$containQueryList = str_getcsv($this->request->getQuery('extended'));
foreach($associations->getIterator() as $a) {
if(\in_array($a->getName(), $containQueryList, true)) {
$containClause[] = $a->getName();
}
} elseif (
$this->request->getQuery('extended')
&& \is_string($this->request->getQuery('extended'))
) {
// Get ONLY the associated models requested
$associations = $table->associations();
$containQueryList = str_getcsv($this->request->getQuery('extended'));
foreach($associations->getIterator() as $a) {
if(\in_array($a->getName(), $containQueryList, true)) {
$containClause[] = $a->getName();
}
}
}
}

return empty($containClause) ? $query : $query->contain($containClause);
return $containClause;
}

/**
Expand All @@ -115,7 +135,7 @@ public function getIndexQuery(bool $pickerMode = false, array $requestParams = [
// PrimaryLinkTrait
$link = $this->getPrimaryLink(true);
// Initialize the Query Object
$query = $table->find($this->paginate['finder'] ?? 'all');
$query = $table->find();
// Get a pointer to my expressions list
$newexp = $query->newExpr();
// The searchable attributes can have an AND or an OR conjunction. The first one is used from the filtering block
Expand All @@ -136,7 +156,8 @@ public function getIndexQuery(bool $pickerMode = false, array $requestParams = [
// Attributes to search for
if(method_exists($table, 'getSearchableAttributes')) {
$searchableAttributes = $table->getSearchableAttributes($this->name,
$this->viewBuilder()->getVar('vv_tz'));
$this->viewBuilder()
->getVar('vv_tz'));

if(!empty($searchableAttributes)) {
$this->set('vv_searchable_attributes', $searchableAttributes);
Expand Down Expand Up @@ -165,6 +186,7 @@ public function getIndexQuery(bool $pickerMode = false, array $requestParams = [
}

// Filter results that will occur from the searchable attributes
// TODO: Move to its own function
if($pickerMode) {
// Get only the active People
// XXX Perhaps we need to make this a configuration
Expand Down
99 changes: 71 additions & 28 deletions app/src/Lib/Traits/SearchFilterTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@

namespace App\Lib\Traits;

use Bake\Utility\Model\AssociationFilter;
use Cake\Database\Expression\QueryExpression;
use Cake\Http\ServerRequest;
use Cake\ORM\Query;
Expand Down Expand Up @@ -60,21 +61,49 @@ public function addJoins(Query $query, string $attribute, ServerRequest $request
return $query;
}

$changelog_fk = strtolower(Inflector::underscore($this->searchFilters[$attribute]['model'])) . '_id';
$fk = strtolower(Inflector::underscore(Inflector::singularize($this->_alias))) . '_id';
$mtable_name = Inflector::tableize(Inflector::pluralize($this->searchFilters[$attribute]['model']));
$mtable_alias = Inflector::pluralize($this->searchFilters[$attribute]['model']);

return $query->join([$mtable_alias => [
'table' => $mtable_name,
'conditions' => [
$mtable_alias . '.' . $fk . '=' . $this->_alias . '.id',
// XXX Moved to changelong Behavior
// $mtable_alias . '.' . 'deleted IS NOT TRUE',
// $mtable_alias . '.' . $changelog_fk . ' IS NULL'
],
'type' => $joinType
]]);
$parentTable = $this->_alias;
$joinAssociations = [];
// Iterate over the dot notation and add the joins in the correct order
// People.Names
foreach (explode('.', $this->searchFilters[$attribute]['model']) as $associationsdModel) {
$mtable_name = Inflector::tableize(Inflector::pluralize($associationsdModel));
$mtable_alias = Inflector::pluralize($associationsdModel);

$AssociationFilter = new AssociationFilter();
$associatedModel = $AssociationFilter->filterAssociations($this->fetchTable($associationsdModel));
$relation = null;
$conditions = [];
if(isset($associatedModel['HasOne'])
&& !empty($associatedModel['HasOne'][$parentTable])
) {
$relation = $associatedModel['HasOne'][$parentTable];
$conditions[] = $relation['alias'] . '.' . $relation['foreignKey'] . '=' . $mtable_alias . '.id';
} elseif(isset($associatedModel['HasMany'])
&& !empty($associatedModel['HasMany'][$parentTable])
) {
$relation = $associatedModel['HasMany'][$parentTable];
$conditions[] = $relation['alias'] . '.' . $relation['foreignKey'] . '=' . $mtable_alias . '.id';
} elseif(isset($associatedModel['BelongsTo'])
&& !empty($associatedModel['BelongsTo'][$parentTable])
) {
$relation = $associatedModel['BelongsTo'][$parentTable];
$conditions[] = $relation['alias'] . '.id' . '=' . $mtable_alias . '.' . $relation['foreignKey'];
}

$joinAssociations[$mtable_alias] = [
'table' => $mtable_name,
'conditions' => $conditions,
'type' => $joinType
];

$parentTable = $associationsdModel;
}


return $query->join($joinAssociations);
// XXX We can not use the inenerJoinWith since it applies EagerLoading and includes all the fields which
// causes problems
// return $query->innerJoinWith($this->searchFilters[$attribute]['model']);
}

/**
Expand All @@ -93,14 +122,19 @@ public function constructDateComparisonClause(QueryExpression $exp, string $attr
if (empty($dates[0]) && empty($dates[1])) {
return $exp;
}
// The starts_at is non-empty. So the data should be greater than the starts_at date
if (!empty($dates[0]) && empty($dates[1])) {
return $exp->gte("'" . FrozenTime::parse($dates[0]) . "'", $attributeWithModelPrefix);
}
// The ends_at is non-empty. So the data should be less than the ends_at date
if (!empty($dates[1])
&& empty($dates[0])) {
return $exp->lte("'" . FrozenTime::parse($dates[1]) . "'", $attributeWithModelPrefix);

// The starts_at is empty or the ends_at is empty
if (
(!empty($dates[0]) && empty($dates[1]))
||
(empty($dates[0]) && !empty($dates[1]))
) {
$date = empty($dates[0]) ? $dates[1] : $dates[0];
if(str_contains($attributeWithModelPrefix, 'valid_from')) {
return $exp->gte($attributeWithModelPrefix, FrozenTime::parse($date));
} elseif(str_contains($attributeWithModelPrefix, 'valid_through')) {
return $exp->lte($attributeWithModelPrefix, FrozenTime::parse($date));
}
}

return $exp->between($attributeWithModelPrefix, "'" . $dates[0] . "'", "'" . $dates[1] . "'");
Expand All @@ -125,19 +159,28 @@ public function expressionsConstructor(Query $query, QueryExpression $exp, strin
}

// Prepend the Model name to the attribute
$attributeWithModelPrefix = isset($this->searchFilters[$attribute]['model']) ?
Inflector::pluralize($this->searchFilters[$attribute]['model']) . '.' . $attribute :
$this->_alias . '.' . $attribute;
$modelPrefix = $this->_alias;
if(isset($this->searchFilters[$attribute]['model'])) {
$associationNamesPath = explode('.', $this->searchFilters[$attribute]['model']);
$modelPrefix = Inflector::pluralize(end($associationNamesPath));
}

$attributeWithModelPrefix = $modelPrefix . '.' . $attribute;


$search = $q;
// Use the `lower` function to apply uniformity for the search
$lower = $query->func()->lower([$attributeWithModelPrefix => 'identifier']);

// XXX Strings and Enums are not treated the same. Enums require an exact match but strings
// are partially/non-case sensitive matched
return match ($this->searchFilters[$attribute]['type']) {
'string' => $exp->like($lower, strtolower('%' . $search . '%')),
'integer', 'boolean' => $exp->add([$attributeWithModelPrefix => $search]),
'integer',
'boolean',
'parent' => $exp->add([$attributeWithModelPrefix => $search]),
'date' => $exp->add([$attributeWithModelPrefix => FrozenTime::parseDate($search, 'y-M-d')]),
'timestamp' => $this->constructDateComparisonClause($search),
'timestamp' => $this->constructDateComparisonClause($exp, $attributeWithModelPrefix, $search),
default => $exp->eq($lower, strtolower($search))
};
}
Expand Down
19 changes: 15 additions & 4 deletions app/src/Lib/Traits/TableMetaTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ protected function filterMetadataFields() {
'revision',
'lft', // XXX For now i skip lft.rght column for tree structures
'rght',
// 'parent_id', // todo: We need to filter using the parent_id. This should be an enumerator and should apply for all the models that use TreeBehavior
'api_key',
// XXX maybe replace this with a regex, source_*_id?
'source_ad_hoc_attribute_id',
Expand All @@ -90,12 +89,24 @@ protected function filterMetadataFields() {

$newa = array();
foreach($coltype as $clmn => $type) {
if(in_array($clmn, $meta_fields,true)) {
// XXX We need to check if the type is an enum or plain string. The enum is a string
// but during filtering we do not use like but eq
// If required we can treat enum types as string types

$fType = $type;
// XXX Cakephp Inflector's camel-case function returns a Pascal case string while the variable function
// returns a camel-case string
$viewVarsKey = Inflector::variable(Inflector::pluralize($clmn));
if(isset($this->getAutoViewVars()[$viewVarsKey]['type'])) {
$fType = $this->getAutoViewVars()[$viewVarsKey]['type'];
}

if(\in_array($clmn, $meta_fields, true)) {
// Move the value to metadata
$newa['meta'][$clmn] = $type;
$newa['meta'][$clmn] = $fType;
} else {
// Just copy the value
$newa[$clmn] = $type;
$newa[$clmn] = $fType;
}
}

Expand Down
31 changes: 29 additions & 2 deletions app/src/Model/Table/CousTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@

namespace App\Model\Table;

use App\Lib\Enum\StatusEnum;
use Cake\Database\Expression\QueryExpression;
use Cake\ORM\Query;
use Cake\ORM\RulesChecker;
use Cake\ORM\Table;
Expand Down Expand Up @@ -92,8 +94,9 @@ public function initialize(array $config): void {
$this->setRequiresCO(true);

$this->setAutoViewVars([
'parent_ids' => [
'type' => 'parent'
'parentIds' => [
'type' => 'parent' // Even though the type is parent we refer to the parent_id
// which is an integer
]
]);

Expand Down Expand Up @@ -132,6 +135,30 @@ public function buildRules(RulesChecker $rules): RulesChecker {

return $rules;
}

/**
* Get the Parent COU list(Suitable for dropdown)
*
* @param int $coId CO ID
*
* @return array List of [id, name] Parent COUs
* @since COmanage Registry v5.0.0
*/
public function getParents(int $coId): array
{
$subquery = $this->find();
$subquery = $subquery->where(['co_id' => $coId])
->where(fn(QueryExpression $exp, Query $subquery) => $exp->isNotNull('parent_id'))
->select(['parent_id'])
->distinct();

$query = $this->find('list')
->where(fn(QueryExpression $exp, Query $query) => $exp->in('id', $subquery))
->distinct()
->select(['id', 'name']);
$results = $query->toArray();
return $results;
}

/**
* Callback after model save.
Expand Down
18 changes: 17 additions & 1 deletion app/src/Model/Table/GroupMembersTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ class GroupMembersTable extends Table {
use \App\Lib\Traits\QueryModificationTrait;
use \App\Lib\Traits\TableMetaTrait;
use \App\Lib\Traits\ValidationTrait;

use \App\Lib\Traits\SearchFilterTrait;

/**
* Provide the default layout
*
Expand Down Expand Up @@ -127,6 +128,21 @@ public function initialize(array $config): void {
'index' => ['platformAdmin', 'coAdmin']
]
]);

$this->setFilterConfig([
'family' => [
'type' => 'relatedModel',
'model' => 'People.Names',
'active' => true,
'order' => 2
],
'given' => [
'type' => 'relatedModel',
'model' => 'People.Names',
'active' => true,
'order' => 1
],
]);
}

/**
Expand Down
Loading