Skip to content

Feature cfm150 autocomplete #167

merged 38 commits into from Mar 21, 2024
Merged
Changes from 1 commit
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
1999c79
Autocomplete widget added for selecting a Group member (CFM-150) (#164)
arlen Mar 6, 2024
fc0fdc4
add primevue autocomplete library dependency
Ioannis Mar 10, 2024
b0068fe
Improve/cleanup backend block search functionality
Ioannis Mar 10, 2024
66839d8
Decloupe where clause construction from join clause construction
Ioannis Mar 10, 2024
2dbc1a2
Improve filtering.Dynamically construct where clause using QueryExpre…
Ioannis Mar 10, 2024
060dfe6
Introduce IndexQueryTrait
Ioannis Mar 10, 2024
aa73e23
fix broken mveaType query.Improve Changelog.
Ioannis Mar 10, 2024
1311315
first get request
Ioannis Mar 10, 2024
a8ba9c9
add CoSettings PeoplePicker Configuration
Ioannis Mar 11, 2024
c0305b9
fix condition
Ioannis Mar 12, 2024
8b061f1
Improve FieldHelper
Ioannis Mar 12, 2024
17f32c7
Properly align grouped html controls
Ioannis Mar 12, 2024
4a805f8
Add type to emails and identifiers
Ioannis Mar 12, 2024
72171d4
Add loader.Improve fetch response handling
Ioannis Mar 13, 2024
bf41aa5
Added loader. Fixed duplicates.
Ioannis Mar 13, 2024
0d06b3d
typo fix
Ioannis Mar 13, 2024
a18c993
add filtering with given and family name. Fetch only active users.
Ioannis Mar 13, 2024
0954ba4
refactor frontend error handling
Ioannis Mar 13, 2024
941ffd2
add subfield-col css rule
Ioannis Mar 13, 2024
86fe75b
Peoplepikcer enum
Ioannis Mar 14, 2024
c8a0104
Provide access to configurations locally and globally.Improve fetch i…
Ioannis Mar 15, 2024
238de1a
fix url string
Ioannis Mar 15, 2024
0b3dec0
fix provide/inject functionality
Ioannis Mar 15, 2024
4acb787
Add a subcomponent for item-with-type and style things for easier vis…
arlen Mar 15, 2024
567d656
highlight query string
Ioannis Mar 15, 2024
f4009bb
highlighting fix
Ioannis Mar 15, 2024
a0ab929
sort methods by name
Ioannis Mar 15, 2024
af96643
Add item-id to autocomplete label and update color file (CFM-150)
arlen Mar 15, 2024
f885ca9
Add pager link "show more" to autocomplete (CFM-150)
arlen Mar 15, 2024
f02eaee
Fix label id reference for autocomplete field (CFM-150)
arlen Mar 15, 2024
134c18f
Implement pagination
Ioannis Mar 15, 2024
04a8bb2
filter the active GroupMembers
Ioannis Mar 16, 2024
e5f710a
Refactor GroupMembersTable::isMember Query construction
Ioannis Mar 16, 2024
d8eae4f
Update Group Member listing to expose add / edit / view in a modal wi…
arlen Mar 19, 2024
a8c32b0
Move the person picker to the index of the Group Member list and laun…
arlen Mar 21, 2024
123702d
Add scrolling to the autocomplete people picker (CFM-150)
arlen Mar 21, 2024
2ee562c
removed trailing semi colon
Ioannis Mar 21, 2024
c1c0f69
Change default modal title (CFM-150)
arlen Mar 21, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Prev Previous commit
Next Next commit
Introduce IndexQueryTrait
Ioannis committed Mar 13, 2024
commit 060dfe65068a0ecfea9bace071c8d708b41f47ad
84 changes: 30 additions & 54 deletions app/src/Controller/ApiV2Controller.php
@@ -41,7 +41,8 @@

class ApiV2Controller extends AppController {
use \App\Lib\Traits\LabeledLogTrait;

use \App\Lib\Traits\IndexQueryTrait;

/**
* Perform Cake Controller initialization.
*
@@ -77,6 +78,8 @@ public function initialize(): void {
public function add() {
// $this->name = Models
$modelsName = $this->name;
// $table = the actual table object
$table = $this->$modelsName;
// $tableName = models
$tableName = $this->tableName;

@@ -144,15 +147,20 @@ public function beforeRender(\Cake\Event\EventInterface $event) {
public function delete($id) {
// $this->name = Models (ie: from ModelsTable)
$modelsName = $this->name;

// $table = the actual table object
$table = $this->$modelsName;
// $tableName = models
$tableName = $table->getTable();


// Make sure the requested object exists
try {
$obj = $this->$modelsName->findById($id)->firstOrFail();
$obj = $table->findById($id)->firstOrFail();

// XXX document AR-CO-1 when we implement hard delete/changelog
// note similar logic in StandardController
$this->$modelsName->deleteOrFail($obj);
$table->deleteOrFail($obj);

if(method_exists($obj, "isReadOnly") && $obj->isReadOnly()) {
throw new BadRequestException(__d('error', 'edit.readonly'));
}
@@ -184,10 +192,12 @@ public function delete($id) {
public function edit($id) {
// $this->name = Models (ie: from ModelsTable)
$modelsName = $this->name;
// $table = the actual table object
$table = $this->$modelsName;
// $tableName = models
$tableName = $this->$modelsName->getTable();
$tableName = $table->getTable();

$query = $this->$modelsName->findById($id);
$query = $table->findById($id);

try {
// Pull the current record
@@ -203,14 +213,14 @@ public function edit($id) {
throw new BadRequestException(__d('error', 'api.object', [$modelsName]));
}

$obj = $this->$modelsName->patchEntity($obj, $json[$modelsName]);
$obj = $table->patchEntity($obj, $json[$modelsName]);

$this->$modelsName->saveOrFail($obj);
$table->saveOrFail($obj);

// Trigger provisioning, letting errors bubble up (AR-GMR-5)
if(method_exists($this->$modelsName, "requestProvisioning")) {
if(method_exists($table, "requestProvisioning")) {
$this->llog('rule', "AR-GMR-5 Requesting provisioning for $modelsName " . $obj->id);
$this->$modelsName->requestProvisioning(id: $obj->id, context: ProvisioningContextEnum::Automatic);
$table->requestProvisioning(id: $obj->id, context: ProvisioningContextEnum::Automatic);
}

// Let the view render
@@ -224,7 +234,6 @@ public function edit($id) {
$err = $this->exceptionToError($e);

$this->llog('debug', $err);
$results[] = ['error' => $err];

throw new BadRequestException($this->exceptionToError($e));
}
@@ -293,47 +302,12 @@ public function generateApiKey(string $id) {
public function index() {
// $modelsName = Models
$modelsName = $this->name;

$query = $this->$modelsName->find();

// PrimaryLinkTrait
$link = $this->getPrimaryLink(true);

// We automatically allow API calls to be filtered on primary link
if(!empty($link->attr) && !empty($link->value)) {
$query = $query->where([$this->$modelsName->getAlias().'.'.$link->attr => $link->value]);
}
// $table = the actual table object
$table = $this->$modelsName;

// This will produce a nested object which is very useful for vue integration
if($this->request->getQuery('extended') !== null) {
$modelContain = [];
$associations = $this->$modelsName->associations();
foreach($associations->getByType(['BelongsTo']) as $a) {
$modelContain[] = $a->getClassName();
}

if(!empty($modelContain)) {
$query = $query->contain($modelContain);
}
}
// Construct the Query
$query = $this->getIndexQuery();

if($modelsName == 'AuthenticationEvents') {
// Special case for filtering on authenticated identifier. There is a
// similar filter in AuthenticationEventsController::beforeFilter.
// If other special cases show up this should get refactored into a trait
// populated by the table (or something similar).

if($this->getRequest()->getQuery('authenticated_identifier')) {
$query = $query->where(['authenticated_identifier' => \App\Lib\Util\StringUtilities::urlbase64decode($this->getRequest()->getQuery('authenticated_identifier'))]);
} else {
// We only allow unfiltered queries for platform users

if(!$this->RegistryAuth->isPlatformAdmin()) {
throw new \InvalidArgumentException(__d('error', 'input.notprov', 'authenticated_identifier'));
}
}
}

// This magically makes REST calls paginated... can use eg direction=,
// sort=, limit=, page=
$this->set($this->tableName, $this->paginate($query));
@@ -351,14 +325,16 @@ public function index() {
public function view($id = null) {
// $this->name = Models
$modelsName = $this->name;
// $table = the actual table object
$table = $this->$modelsName;
// $tableName = models
$tableName = $this->$modelsName->getTable();
$tableName = $table->getTable();

if(empty($id)) {
throw new InvalidArgumentException(__d('error', 'notprov', ['id']));
}

$obj = $this->$modelsName->findById($id)->firstOrFail();
$obj = $table->findById($id)->firstOrFail();

$this->set($tableName, [$obj]);

67 changes: 11 additions & 56 deletions app/src/Controller/StandardController.php
@@ -37,6 +37,8 @@
use \App\Lib\Util\StringUtilities;

class StandardController extends AppController {
use \App\Lib\Traits\IndexQueryTrait;

// Pagination defaults should be set in each controller
public $pagination = [];

@@ -565,64 +567,17 @@ public function index() {
$table = $this->$modelsName;
// $tableName = models
$tableName = $table->getTable();
// PrimaryLinkTrait
$link = $this->getPrimaryLink(true);

// AutoViewVarsTrait
$this->populateAutoViewVars();
// Initialize the Query Object
$query = $table->find($this->paginate['finder'] ?? 'all');
// Get a pointer to my expressions list
$newexp = $query->newExpr();

if(!empty($link->attr)) {
// If a link attribute is defined but no value is provided, then query
// where the link attribute is NULL
// "all" is the default finder. But since we are utilizing the paginator here, we will check the configuration
// for any custom finder.
$newexp->add([$table->getAlias().'.'.$link->attr => $link->value]);
}

// QueryModificationTrait
if(method_exists($table, 'getIndexContains')
&& $table->getIndexContains()) {
$query->contain($table->getIndexContains());
}

// SearchFilterTrait
if(method_exists($table, 'getSearchableAttributes')) {
$searchableAttributes = $table->getSearchableAttributes($this->name,
$this->viewBuilder()->getVar('vv_tz'));

if(!empty($searchableAttributes)) {
// Here we iterate over the attributes, and we add a new where clause for each one
foreach($searchableAttributes as $attribute => $options) {
// Add the Join Clauses
$query = $table->addJoins($query, $attribute, $this->request);

// Construct and apply the where Clause
if(!empty($this->request->getQuery($attribute))) {
$newexp = $table->expressionsConstructor($query, $newexp, $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') ?? '';
$newexp = $table->expressionsConstructor($query, $newexp, $attribute, $search_date);
}
}

$this->set('vv_searchable_attributes', $searchableAttributes);
}
}

$query = $query->where($newexp);
// Construct the Query
$query = $this->getIndexQuery();
// Fetch the data and paginate
$resultSet = $this->paginate($query);


// Pass vars to the View
$this->set($tableName, $resultSet);
$this->set('vv_permission_set', $this->RegistryAuth->calculatePermissionsForResultSet($resultSet));

// AutoViewVarsTrait
$this->populateAutoViewVars();

// Default index view title is model name
[$title, , ] = StringUtilities::entityAndActionToTitle($resultSet, $modelsName, 'index');
$this->set('vv_title', $title);
@@ -769,7 +724,7 @@ protected function populateAutoViewVars(object $obj=null) {
break;
default:
// XXX I18n? and in match?
throw new \LogicException('Unknonwn Auto View Var Type {0}', [$avv['type']]);
throw new \LogicException('Unknonwn Auto View Var Type {0}', $avv['type']);
break;
}
}
171 changes: 171 additions & 0 deletions app/src/Lib/Traits/IndexQueryTrait.php
@@ -0,0 +1,171 @@
<?php
/**
* COmanage Registry IndexQuery Trait
*
* Portions licensed to the University Corporation for Advanced Internet
* Development, Inc. ("UCAID") under one or more contributor license agreements.
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership.
*
* UCAID licenses this file to you under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with the
* License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @link https://www.internet2.edu/comanage COmanage Project
* @package registry
* @since COmanage Registry v5.0.0
* @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
*/

declare(strict_types = 1);

namespace App\Lib\Traits;

use Cake\ORM\Query;

trait IndexQueryTrait {
/**
* Construct the Index Contain array
*
* @param Query $query
*
* @return object Cake ORM Query object
* @since COmanage Registry v5.0.0
*/
public function constructGetIndexContains(Query $query): object {
// $this->name = Models
$modelsName = $this->name;
// $table = the actual table object
$table = $this->$modelsName;
// Initialize the containClause
$containClause = [];

// Get whatever the table configuration has
if(method_exists($table, 'getIndexContains')
&& $table->getIndexContains()) {
$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
// 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')
&& \is_string($this->request->getQuery('extended'))) {
// This is a string. We will parse the csv and continue
$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);
}

/**
* Build the Index Query
*
*
* @return object Cake ORM Query object
* @since COmanage Registry v5.0.0
*/
public function getIndexQuery(): object {
// $this->name = Models
$modelsName = $this->name;
// $table = the actual table object
$table = $this->$modelsName;
// PrimaryLinkTrait
$link = $this->getPrimaryLink(true);
// Initialize the Query Object
$query = $table->find($this->paginate['finder'] ?? 'all');
// Get a pointer to my expressions list
$newexp = $query->newExpr();
// The searchable attributes can have an AND or an OR conjuction. The first one is used from the filtering block
// while the second one from the picker vue module.
$newexp = $newexp->setConjunction(true ? 'AND' : 'OR');

if(!empty($link->attr) && !empty($link->value)) {
// If a link attribute is defined but no value is provided, then query
// where the link attribute is NULL
// "all" is the default finder. But since we are utilizing the paginator here, we will check the configuration
// for any custom finder.
$query = $query->where([$table->getAlias().'.'.$link->attr => $link->value]);
}

// Get Associated Model Data
$query = $this->constructGetIndexContains($query);

// SearchFilterTrait
if(method_exists($table, 'getSearchableAttributes')) {
$searchableAttributes = $table->getSearchableAttributes($this->name,
$this->viewBuilder()->getVar('vv_tz'));

if(!empty($searchableAttributes)) {
$this->set('vv_searchable_attributes', $searchableAttributes);

// Here we iterate over the attributes, and we add a new where clause for each one
foreach($searchableAttributes as $attribute => $options) {
// Add the Join Clauses
$query = $table->addJoins($query, $attribute, $this->request);

// Construct and apply the where Clause
if(!empty($this->request->getQuery($attribute))) {
$newexp = $table->expressionsConstructor($query, $newexp, $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') ?? '';
$newexp = $table->expressionsConstructor($query, $newexp, $attribute, $search_date);
}
}

// Append the new conditions
$query = $query->where($newexp);
}
}

// Special Authenticated Identifier filtering
if($modelsName == 'AuthenticationEvents') {
// Special case for filtering on authenticated identifier. There is a
// todo:
// similar filter in AuthenticationEventsController::beforeFilter.
// If other special cases show up this should get refactored into a trait
// populated by the table (or something similar).

if($this->getRequest()->getQuery('authenticated_identifier')) {
$query = $query->where(['authenticated_identifier' => \App\Lib\Util\StringUtilities::urlbase64decode($this->getRequest()->getQuery('authenticated_identifier'))]);
} else {
// We only allow unfiltered queries for platform users

if(!$this->RegistryAuth->isPlatformAdmin()) {
throw new \InvalidArgumentException(__d('error', 'input.notprov', 'authenticated_identifier'));
}
}
}

return $query;
}
}