Skip to content

Join Model associations when using filtering #132

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: 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>-->