Skip to content

Commit

Permalink
Join Model associations when using filtering (#132)
Browse files Browse the repository at this point in the history
* Join Nodel associations when using filtering

* Return distinct records

* add field ordering. Fix labeling lexicals.
  • Loading branch information
Ioannis authored and arlen committed Oct 6, 2023
1 parent 6ca2ac7 commit 847de53
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 48 deletions.
4 changes: 3 additions & 1 deletion app/src/Controller/PeopleController.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
// XXX not doing anything with Log yet
use Cake\Log\Log;
use Cake\ORM\TableRegistry;
use http\QueryString;

class PeopleController extends StandardController {
public $paginate = [
Expand All @@ -49,7 +50,8 @@ class PeopleController extends StandardController {
'sortableFields' => [
'PrimaryName.given',
'PrimaryName.family'
]
],
'finder' => 'indexed'
];

/**
Expand Down
10 changes: 7 additions & 3 deletions app/src/Controller/StandardController.php
Original file line number Diff line number Diff line change
Expand Up @@ -534,9 +534,13 @@ public function index() {
if(!empty($link->attr)) {
// If a link attribute is defined but no value is provided, then query
// where the link attribute is NULL
$query = $table->find()->where([$table->getAlias().'.'.$link->attr => $link->value]);
// "all" is the default finder. But since we are utilizing the paginator here, we will check the configuration
// for any custom finder.
$query = $table->find(
$this->paginate['finder'] ?? "all"
)->where([$table->getAlias().'.'.$link->attr => $link->value]);
} else {
$query = $table->find();
$query = $table->find($this->paginate['finder'] ?? "all");
}

// QueryModificationTrait
Expand All @@ -551,7 +555,7 @@ public function index() {

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) {
foreach($searchableAttributes as $attribute => $options) {
if(!empty($this->request->getQuery($attribute))) {
$query = $table->whereFilter($query, $attribute, $this->request->getQuery($attribute));
} elseif (!empty($this->request->getQuery($attribute . "_starts_at"))
Expand Down
51 changes: 39 additions & 12 deletions app/src/Lib/Traits/SearchFilterTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ public function getSearchableAttributes(string $controller, string $vv_tz=null):
$this->searchFilters[$field] = [
'type' => 'string', // XXX for now - this needs to be looked up.
'label' => \App\Lib\Util\StringUtilities::columnKey($fieldName, $field, $vv_tz, true),
'active' => isset($f['active']) ? $f['active'] : true
'active' => isset($f['active']) ? $f['active'] : true,
'model' => $f['model'],
'order' => $f['order']
];
}
}
Expand All @@ -92,7 +94,8 @@ public function getSearchableAttributes(string $controller, string $vv_tz=null):
$this->searchFilters[$column] = [
'type' => $type,
'label' => \App\Lib\Util\StringUtilities::columnKey($modelname, $column, $vv_tz, true),
'active' => $fieldIsActive
'active' => $fieldIsActive,
'order' => 99 // this is the default
];

// For the date fields we search ranges
Expand Down Expand Up @@ -132,36 +135,60 @@ public function whereFilter(\Cake\ORM\Query $query, string $attribute, string|ar
return $query;
}

if(isset($this->searchFilters[$attribute]['model'])) {
$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']);
$query->join([$mtable_alias => [
'table' => $mtable_name,
'conditions' => [
$mtable_alias . '.' . $fk . '=' . $this->_alias . '.id',
$mtable_alias . '.' . 'deleted IS NOT TRUE',
$mtable_alias . '.' . $changelog_fk . ' IS NULL'
],
'type' => 'INNER'
]]);
}

// Prepend the Model name to the attribute
$attributeWithModelPrefix = isset($this->searchFilters[$attribute]['model']) ?
Inflector::pluralize($this->searchFilters[$attribute]['model']) . '.' . $attribute :
$this->_alias . '.' . $attribute;

$search = $q;
$sub = false;
// Primitive types
$search_types = ['integer', 'boolean'];
if( $this->searchFilters[$attribute]['type'] == "string") {
$search = "%" . $search . "%";
$sub = true;
// Search type
} elseif(in_array($this->searchFilters[$attribute]['type'], $search_types, true)) {
return $query->where([$attribute => $search]);
return $query->where([$attributeWithModelPrefix => $search]);
// Date
} elseif($this->searchFilters[$attribute]['type'] == "date") {
// Parse the date string with FrozenTime to improve error handling
return $query->where([$attribute => FrozenTime::parseDate($search, 'y-M-d')]);
return $query->where([$attributeWithModelPrefix => FrozenTime::parseDate($search, 'y-M-d')]);
// Timestamp
} 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] . "'");
return $query->where(function (\Cake\Database\Expression\QueryExpression $exp, \Cake\ORM\Query $query) use ($attributeWithModelPrefix, $search) {
return $exp->between($attributeWithModelPrefix, "'" . $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);
return $query->where(function (\Cake\Database\Expression\QueryExpression $exp, \Cake\ORM\Query $query) use ($attributeWithModelPrefix, $search) {
return $exp->gte("'" . FrozenTime::parse($search[0]) . "'", $attributeWithModelPrefix);
});
// 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);
return $query->where(function (\Cake\Database\Expression\QueryExpression $exp, \Cake\ORM\Query $query) use ($attributeWithModelPrefix, $search) {
return $exp->lte("'" . FrozenTime::parse($search[1]) . "'", $attributeWithModelPrefix);
});
} else {
// We return everything
Expand All @@ -171,8 +198,8 @@ public function whereFilter(\Cake\ORM\Query $query, string $attribute, string|ar
}

// 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 $query->where(function (\Cake\Database\Expression\QueryExpression $exp, \Cake\ORM\Query $query) use ($attributeWithModelPrefix, $search, $sub) {
$lower = $query->func()->lower([$attributeWithModelPrefix => 'identifier']);
return ($sub) ? $exp->like($lower, strtolower($search))
: $exp->eq($lower, strtolower($search));
});
Expand Down
8 changes: 7 additions & 1 deletion app/src/Lib/Util/StringUtilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,16 @@ public static function classNameToForeignKey(string $className): string {

public static function columnKey($modelsName, $c, $tz=null, $useCustomClMdlLabel=false): string {
if(strpos($c, "_id", strlen($c)-3)) {
$postfix = "";
if($c == "parent_id") {
// This means we are working with a model that implements a Tree behavior
$postfix = " ({$modelsName})";
}

// Key is of the form field_id, use .ct label instead
$k = self::foreignKeyToClassName($c);

return __d('controller', $k, [1]);
return __d('controller', $k, [1]) . $postfix;
}

// Look for a model specific key first
Expand Down
59 changes: 44 additions & 15 deletions app/src/Model/Table/PeopleTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ public function initialize(array $config): void {
->setDependent(true)
->setCascadeCallbacks(true);
$this->hasMany('GroupOwners')
->setClassName('GroupMembers')
->setDependent(true)
->setCascadeCallbacks(true);
$this->hasMany('HistoryRecords')
Expand Down Expand Up @@ -158,27 +159,32 @@ public function initialize(array $config): void {
$this->setFilterConfig([
'family' => [
'type' => 'relatedModel',
'model' => 'names',
'active' => true
'model' => 'Name',
'active' => true,
'order' => 2
],
'given' => [
'type' => 'relatedModel',
'model' => 'names',
'active' => true
'model' => 'Name',
'active' => true,
'order' => 1
],
'mail' => [
'type' => 'relatedModel',
'model' => 'email_addresses',
'active' => true
'model' => 'EmailAddress',
'active' => true,
'order' => 3
],
'identifier' => [
'type' => 'relatedModel',
'model' => 'identifiers',
'active' => true
'model' => 'Identifier',
'active' => true,
'order' => 4
],
'timezone' => [
'type' => 'field',
'active' => false
'active' => false,
'order' => 99
]
]);

Expand Down Expand Up @@ -249,6 +255,29 @@ public function beforeDelete(\Cake\Event\Event $event, $entity, \ArrayObject $op
return true;
}

/**
* Customized finder for the Index Population View
*
* @param Query $query Cake ORM Query
* @param array $options Cake ORM Query options
*
* @return CakeORMQuery Cake ORM Query
* @since COmanage Registry v5.0.0
*/
public function findIndexed(Query $query, array $options): Query {
return $query->select([
'People.id',
'PrimaryName.given',
'PrimaryName.family',
'People.status',
'People.created',
'People.modified',
'People.timezone',
'People.date_of_birth'
])
->distinct();
}

/**
* Table specific logic to generate a display field.
*
Expand Down Expand Up @@ -413,9 +442,9 @@ public function marshalProvisioningData(int $id): array {

$identifiers = [];

foreach($ret['data']->identifiers as $id) {
if($id->status == SuspendableStatusEnum::Active) {
$identifiers[] = $id;
foreach($ret['data']->identifiers as $ident) {
if($ident->status == SuspendableStatusEnum::Active) {
$identifiers[] = $ident;
}
}

Expand All @@ -438,9 +467,9 @@ public function marshalProvisioningData(int $id): array {

$identifiers = [];

foreach($ret['data']->identifiers as $id) {
if($id->status == SuspendableStatusEnum::Active) {
$identifiers[] = $id;
foreach($ret['data']->identifiers as $ident) {
if($ident->status == SuspendableStatusEnum::Active) {
$identifiers[] = $ident;
}
}

Expand Down
34 changes: 18 additions & 16 deletions app/templates/element/filter.php
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@
<button class="top-filters-active-filter deletebutton spin btn btn-default btn-sm" data-identifier="<?= $data_identifier ?>" type="button" aria-controls="<?php print $aria_controls; ?>" title="<?= __d('operation', 'clear.filters',[2]); ?>">
<em class="material-icons" aria-hidden="true">cancel</em>
<span class="top-filters-active-filter-title">
<?= !empty($columns[$key]['label']) ? $columns[$key]['label'] : $vv_searchable_attributes[$key]['label'] ?>
<?= Inflector::humanize(Inflector::underscore($vv_searchable_attributes[$key]['label'] ?? $columns[$key]['label'])) ?>
</span>
<?php if($vv_searchable_attributes[$key]['type'] != 'boolean'): ?>
<span class="top-filters-active-filter-value">
Expand All @@ -148,16 +148,15 @@
$inactiveFiltersCount = 0; // for re-balancing the columns and submit buttons

if(!empty($columns)) {
// To make our filters consistently ordered with the index columns, sort the $vv_searchable_attributes
// by the keys of columns.inc $indexColumns (passed in to this View element as $indexColumns and referenced
// as "$columns"). The fields found in $columns will be placed first in the resulting array.
// The result should only include fields that exist in the original $vv_searchable_attributes array.

// XXX Turn this off. We should have more arbitrary control over the order of the filters, and for the moment
// it is better (at least for now) to let the explicitly defined filters come first.
// $vv_searchable_attributes = array_intersect_key(array_replace(array_flip(array_keys($columns)), $vv_searchable_attributes), $vv_searchable_attributes);
// The searchable attributes will be sorted first alphabetically
asort($vv_searchable_attributes);
// Sort the order attribute
uasort($vv_searchable_attributes, function ($item1, $item2) {
if ($item1['order'] == $item2['order']) return 0;
return $item1['order'] < $item2['order'] ? -1 : 1;
});
}

foreach($vv_searchable_attributes as $key => $options) {
if($options['type'] == 'boolean') {
$field_booleans_columns[$key] = $options;
Expand All @@ -166,7 +165,13 @@
$field_datetime_columns[$key] = $options;
continue;
}


$label = Inflector::humanize(
Inflector::underscore(
$options['label'] ?? $columns[$key]['label']
)
);

if($options['type'] == 'date') {
// Date fields use a date picker (e.g. DOB)
// (Note that timestamps are handled specially. See below.)
Expand Down Expand Up @@ -202,15 +207,15 @@

// Create a text field to hold our date value.
print '<div class="top-filters-fields-date ' . $wrapperCssClass . '">';
print $this->Form->label($key, !empty($columns[$key]['label']) ? $columns[$key]['label'] : $options['label']);
print $this->Form->label($key, $label);
print '<div class="d-flex">';
print $this->Form->text($key, $opts) . $this->element('datePicker', $date_args);
print '</div>';
print '</div>';
} else {
// text input
$formParams = [
'label' => !empty($columns[$key]['label']) ? $columns[$key]['label'] : $options['label'],
'label' => $label,
// The default type is text, but we might convert to select below
'type' => 'text',
'value' => (!empty($query[$key]) ? $query[$key] : ''),
Expand Down Expand Up @@ -406,6 +411,3 @@

<?= $this->Form->end(); ?>

<!--<pre>-->
<!-- --><?//= print_r($vv_searchable_attributes) ?>
<!--</pre>-->

0 comments on commit 847de53

Please sign in to comment.