Skip to content

CFM-135_Model_Index_Filtering_Infrastructure #24

Merged
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
16 changes: 16 additions & 0 deletions app/src/Controller/StandardController.php
Original file line number Diff line number Diff line change
Expand Up @@ -440,9 +440,17 @@ public function index() {
$searchableAttributes = $table->getSearchableAttributes();

if(!empty($searchableAttributes)) {
// Here we iterate over the attributes and we add a new where clause for each one
foreach(array_keys($searchableAttributes) as $attribute) {
if(!empty($this->request->getQuery($attribute))) {
$query = $table->whereFilter($query, $attribute, $this->request->getQuery($attribute));
} elseif (!empty($this->request->getQuery($attribute . "_starts_at"))
|| !empty($this->request->getQuery($attribute . "_ends_at"))) {
$search_date = [];
// We allow empty for dates since we might refer to infinity (from whenever or to always)
$search_date[] = $this->request->getQuery($attribute . "_starts_at") ?? "";
$search_date[] = $this->request->getQuery($attribute . "_ends_at") ?? "";
$query = $table->whereFilter($query, $attribute, $search_date);
}
}

Expand Down Expand Up @@ -572,6 +580,14 @@ protected function populateAutoViewVars(object $obj=null) {

$this->set($vvar, $query->toArray());
break;
case 'parent':
$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()));
break;
default:
// XXX I18n? and in match?
throw new \LogicException('Unknonwn Auto View Var Type {0}', [$avv['type']]);
Expand Down
160 changes: 67 additions & 93 deletions app/src/Lib/Traits/SearchFilterTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,37 +30,11 @@
namespace App\Lib\Traits;

use Cake\Utility\Inflector;
use Cake\I18n\FrozenTime;

trait SearchFilterTrait {
// Array (and configuration) of permitted search filters
private $searchFilters = array();

/**
* Determine the UI label for the specified attribute.
*
* @since COmanage Registry v5.0.0
* @param string $attribute Attribute
* @return string Label
* @todo Merge this with _column_key from index.ctp
*/

public function getLabel(string $attribute): string {
if(isset($this->searchFilters[$attribute]['label'])
&& $this->searchFilters[$attribute]['label'] !== null) {
return $this->searchFilters[$attribute]['label'];
}

// Try to construct a label from the language key.
$l = __d('field', $attribute);

if($l != $attribute) {
return $l;
}

// If we make it here, just return a pretty version of the $attribute name
return Inflector::humanize($attribute);
}

/**
* Obtain the set of permitted search attributes.
*
Expand All @@ -69,83 +43,83 @@ public function getLabel(string $attribute): string {
*/

public function getSearchableAttributes(): array {
// Not every configuration element is necessary for the search form, and
// some need to be calculated, so we do that work here.

$ret = [];

foreach(array_keys($this->searchFilters) as $attr) {
$ret[ $attr ] = [
'label' => $this->getLabel($attr)
foreach ($this->filterMetadataFields() as $column => $type) {
// If the column is an array then we are accessing the Metadata fields. Skip
if(is_array($type)) {
continue;
}
$this->searchFilters[$column] = [
'type' => $type,
'label' => (__d('field', $column) ?? Inflector::humanize($column))
];

// For the date fields we search ranges
if($type === 'timestamp') {
$this->searchFilters[$column]['alias'][] = $column . '_starts_at';
$this->searchFilters[$column]['alias'][] = $column . '_ends_at';
}
}

return $ret;
}

/**
* Add a permitted search filters.
*
* @since COmanage Registry v5.0.0
* @param string $attribute Attribute that filtering is permitted on (database name)
* @param bool $caseSensitive Whether this attribute is case sensitive
* @param string $label Label for this search field, or null to autocalculate
* @param bool $substring Whether substring searching is permitted for this attribute
*/

public function setSearchFilter(string $attribute,
bool $caseSensitive=false,
string $label=null,
bool $substring=true): void {
$this->searchFilters[$attribute] = compact('caseSensitive', 'label', 'substring');

return $this->searchFilters ?? [];
}

/**
* Build a query where() clause for the configured attribute.
*
* @since COmanage Registry v5.0.0
* @param \Cake\ORM\Query $query Cake ORM Query object
* @param string $attribute Attribute to filter on (database name)
* @param string $q Value to filter on
* @param \Cake\ORM\Query $query Cake ORM Query object
* @param string $attribute Attribute to filter on (database name)
* @param string|array $q Value to filter on
*
* @return \Cake\ORM\Query Cake ORM Query object
* @since COmanage Registry v5.0.0
*/

public function whereFilter(\Cake\ORM\Query $query, string $attribute, string $q): object {
if(!empty($this->searchFilters[$attribute])) {
$cs = (isset($this->searchFilters[$attribute]['caseSensitive'])
&& $this->searchFilters[$attribute]['caseSensitive']);

$sub = (isset($this->searchFilters[$attribute]['substring'])
&& $this->searchFilters[$attribute]['substring']);

$search = $q;

if($sub) {
// Substring
// note, for now at least, a user may infix their own %
$search .= "%";
}

if($cs) {
// Case sensitive
$query->where([$attribute => $search]);
} else {
// Case insensitive
$query->where(function (\Cake\Database\Expression\QueryExpression $exp, \Cake\ORM\Query $query) use ($attribute, $search, $sub) {
$lower = $query->func()->lower([
// https://book.cakephp.org/3/en/orm/query-builder.html#function-arguments
$attribute => 'identifier'
]);
if($sub) {
return $exp->like($lower, strtolower($search));
} else {
return $exp->eq($lower, strtolower($search));
}
public function whereFilter(\Cake\ORM\Query $query, string $attribute, string|array $q): object {
// not a permitted attribute
if(empty($this->searchFilters[$attribute])) {
return $query;
}

$search = $q;
$sub = false;
// Primitive types
$search_types = ['integer', 'boolean'];
if( $this->searchFilters[$attribute]['type'] == "string") {
$search = "%" . $search . "%";
$sub = true;
} elseif(in_array($this->searchFilters[$attribute]['type'], $search_types, true)) {
return $query->where([$attribute => $search]);
} elseif( $this->searchFilters[$attribute]['type'] == "timestamp") {
// Date between dates
if(!empty($search[0])
&& !empty($search[1])) {
return $query->where(function (\Cake\Database\Expression\QueryExpression $exp, \Cake\ORM\Query $query) use ($attribute, $search) {
return $exp->between($attribute, "'" . $search[0] . "'", "'" . $search[1] . "'");
});
// The starts at is non empty. So the data should be greater than the starts_at date
} elseif(!empty($search[0])
&& empty($search[1])) {
return $query->where(function (\Cake\Database\Expression\QueryExpression $exp, \Cake\ORM\Query $query) use ($attribute, $search) {
return $exp->gte("'" . FrozenTime::parse($search[0]) . "'", $attribute);
});
// The ends at is non-empty. So the data should be less than the ends at date
} elseif(!empty($search[1])
&& empty($search[0])) {
return $query->where(function (\Cake\Database\Expression\QueryExpression $exp, \Cake\ORM\Query $query) use ($attribute, $search) {
return $exp->lte("'" . FrozenTime::parse($search[1]) . "'", $attribute);
});
} else {
// We return everything
return $query;
}

}
// else not a permitted attribute

return $query;

// String values
return $query->where(function (\Cake\Database\Expression\QueryExpression $exp, \Cake\ORM\Query $query) use ($attribute, $search, $sub) {
$lower = $query->func()->lower([$attribute => 'identifier']);
return ($sub) ? $exp->like($lower, strtolower($search))
: $exp->eq($lower, strtolower($search));
});
}
}
65 changes: 65 additions & 0 deletions app/src/Lib/Traits/TableMetaTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@

namespace App\Lib\Traits;

use Cake\Utility\Inflector;

trait TableMetaTrait {
// Does this Table represent Registry objects or configuration?
private $confTable = false;
Expand All @@ -54,4 +56,67 @@ public function getIsConfigurationTable() {
public function setIsConfigurationTable(bool $confTable) {
$this->confTable = $confTable;
}


/**
* Filter metadata fields.
*
* @since COmanage Registry v5.0.0
* @return array An array of columns distinguished in metadata and non-metadata
*/

protected function filterMetadataFields() {
// Get the list of columns
$coltype = $this->getSchema()->typeMap();
$entity = $this->getEntityClass();
$entity_namespace = explode('\\', $entity);
$modelName = end($entity_namespace);

// Get the list of belongs_to associations and construct an exclude array
$assc_keys = [];
foreach ($this->associations() as $assc) {
if($assc->type() === "manyToOne") {
$assc_keys[] = Inflector::underscore(Inflector::classify($assc->getClassName())) . "_id";
}
}
// Map the model (eg: Person) to the changelog key (person_id)
$mfk = Inflector::underscore($modelName) . "_id";


$meta_fields = [
...$assc_keys,
$mfk,
'actor_identifier',
// 'provisioning_target_id',
'created', // todo: I might need to revisit this. We might want to filter according to date in some occassions. Like petitions
'deleted',
'id',
'modified',
'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'
// 'source_ad_hoc_attribute_id',
// 'source_address_id',
// 'source_email_address_id',
// 'source_identifier_id',
// 'source_name_id',
// 'source_external_identity_id',
// 'source_telephone_number_id',
];

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

return $newa ?? [];
}
}
1 change: 1 addition & 0 deletions app/src/Model/Table/AdHocAttributesTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class AdHocAttributesTable extends Table {
use \App\Lib\Traits\PrimaryLinkTrait;
use \App\Lib\Traits\TableMetaTrait;
use \App\Lib\Traits\ValidationTrait;
use \App\Lib\Traits\SearchFilterTrait;

/**
* Perform Cake Model initialization.
Expand Down
1 change: 1 addition & 0 deletions app/src/Model/Table/AddressesTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class AddressesTable extends Table {
use \App\Lib\Traits\TableMetaTrait;
use \App\Lib\Traits\TypeTrait;
use \App\Lib\Traits\ValidationTrait;
use \App\Lib\Traits\SearchFilterTrait;

// Default "out of the box" types for this model. Entries here should be
// given a default localization in app/resources/locales/*/defaultType.po
Expand Down
3 changes: 2 additions & 1 deletion app/src/Model/Table/ApiUsersTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ class ApiUsersTable extends Table {
use \App\Lib\Traits\PrimaryLinkTrait;
use \App\Lib\Traits\TableMetaTrait;
use \App\Lib\Traits\ValidationTrait;

use \App\Lib\Traits\SearchFilterTrait;

/**
* Perform Cake Model initialization.
*
Expand Down
12 changes: 7 additions & 5 deletions app/src/Model/Table/CousTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,6 @@ public function initialize(array $config): void {

$this->setPrimaryLink('co_id');
$this->setRequiresCO(true);

// Set up the fields that may be filtered in the index view
$this->setSearchFilter('name', false, null, true);
$this->setSearchFilter('parent_id', true, null, false);
$this->setSearchFilter('description', false, null, true);

$this->setPermissions([
// Actions that operate over an entity (ie: require an $id)
Expand All @@ -91,6 +86,12 @@ public function initialize(array $config): void {
'index' => ['platformAdmin', 'coAdmin']
]
]);

$this->setAutoViewVars([
'parent_ids' => [
'type' => 'parent'
]
]);
}

/**
Expand Down Expand Up @@ -122,6 +123,7 @@ public function buildTableRules(RulesChecker $rules): RulesChecker {
* @param int $id COU ID to determine potential parents of, or null for any (or a new) COU
* @param bool $hierarchy Render the hierarchy in the name
* @return Array Array of COU IDs and COU Names
* @todo Make a TreeTrait and move the function there
*/

public function potentialParents(int $coId, int $id=null, bool $hierarchy=false) {
Expand Down
1 change: 1 addition & 0 deletions app/src/Model/Table/EmailAddressesTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class EmailAddressesTable extends Table {
use \App\Lib\Traits\TableMetaTrait;
use \App\Lib\Traits\TypeTrait;
use \App\Lib\Traits\ValidationTrait;
use \App\Lib\Traits\SearchFilterTrait;

// Default "out of the box" types for this model. Entries here should be
// given a default localization in app/resources/locales/*/defaultType.po
Expand Down
1 change: 1 addition & 0 deletions app/src/Model/Table/ExternalIdentitiesTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class ExternalIdentitiesTable extends Table {
use \App\Lib\Traits\QueryModificationTrait;
use \App\Lib\Traits\TableMetaTrait;
use \App\Lib\Traits\ValidationTrait;
use \App\Lib\Traits\SearchFilterTrait;

/**
* Perform Cake Model initialization.
Expand Down
1 change: 1 addition & 0 deletions app/src/Model/Table/ExternalIdentityRolesTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class ExternalIdentityRolesTable extends Table {
use \App\Lib\Traits\QueryModificationTrait;
use \App\Lib\Traits\TableMetaTrait;
use \App\Lib\Traits\ValidationTrait;
use \App\Lib\Traits\SearchFilterTrait;

/**
* Perform Cake Model initialization.
Expand Down
1 change: 1 addition & 0 deletions app/src/Model/Table/HistoryRecordsTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class HistoryRecordsTable extends Table {
use \App\Lib\Traits\QueryModificationTrait;
use \App\Lib\Traits\TableMetaTrait;
use \App\Lib\Traits\ValidationTrait;
use \App\Lib\Traits\SearchFilterTrait;

/**
* Perform Cake Model initialization.
Expand Down
1 change: 1 addition & 0 deletions app/src/Model/Table/IdentifiersTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class IdentifiersTable extends Table {
use \App\Lib\Traits\TableMetaTrait;
use \App\Lib\Traits\TypeTrait;
use \App\Lib\Traits\ValidationTrait;
use \App\Lib\Traits\SearchFilterTrait;

// Default "out of the box" types for this model. Entries here should be
// given a default localization in app/resources/locales/*/defaultType.po
Expand Down
Loading