From 66b4e7088fd44da3717a134050a93b770d3007df Mon Sep 17 00:00:00 2001 From: Ioannis Igoumenos Date: Tue, 23 Jan 2024 20:59:41 +0200 Subject: [PATCH] Utility that calculates the view and breadcrump title --- app/resources/locales/en_US/operation.po | 6 -- app/src/Controller/ApiUsersController.php | 7 +- .../Component/BreadcrumbComponent.php | 32 +++--- app/src/Controller/DashboardsController.php | 101 ++++++++++-------- .../ExternalIdentitySourcesController.php | 31 +++--- .../ProvisioningTargetsController.php | 20 ++-- app/src/Controller/StandardController.php | 76 +++++-------- .../Controller/StandardPluginController.php | 3 +- app/src/Lib/Util/StringUtilities.php | 76 +++++++++++++ 9 files changed, 212 insertions(+), 140 deletions(-) diff --git a/app/resources/locales/en_US/operation.po b/app/resources/locales/en_US/operation.po index e96526bfe..092608bc4 100644 --- a/app/resources/locales/en_US/operation.po +++ b/app/resources/locales/en_US/operation.po @@ -90,9 +90,6 @@ msgstr "Edit" msgid "edit.a" msgstr "Edit {0}" -msgid "edit.ai" -msgstr "Edit {0}" - msgid "ExternalIdentitySourceRecords.retrieve" msgstr "Retrieve from External Identity Source" @@ -186,6 +183,3 @@ msgstr "View" msgid "view.a" msgstr "View {0}" -msgid "view.ai" -msgstr "View {0}" - diff --git a/app/src/Controller/ApiUsersController.php b/app/src/Controller/ApiUsersController.php index 51ef711cd..25a690757 100644 --- a/app/src/Controller/ApiUsersController.php +++ b/app/src/Controller/ApiUsersController.php @@ -29,6 +29,8 @@ namespace App\Controller; +use App\Lib\Util\StringUtilities; + class ApiUsersController extends StandardController { public $paginate = [ 'order' => [ @@ -56,7 +58,10 @@ public function generate(string $id) { // Let the view render, but tell it to use a different fields file $this->set('vv_fields_inc', 'fields-generate.inc'); - $this->set('vv_title', __d('operation', 'api.key.generate')); + [$title, , ] = StringUtilities::entityAndActionToTitle(null, + 'api.key', + $this->request->getParam('action')); + $this->set('vv_title', $title); $this->render('/Standard/add-edit-view'); } diff --git a/app/src/Controller/Component/BreadcrumbComponent.php b/app/src/Controller/Component/BreadcrumbComponent.php index 7bd0399e5..3c4d994ae 100644 --- a/app/src/Controller/Component/BreadcrumbComponent.php +++ b/app/src/Controller/Component/BreadcrumbComponent.php @@ -186,6 +186,20 @@ public function injectPrimaryLink(object $link, bool $index=true, string $linkLa // Construct the getContains function name $requestAction = $this->getController()->getRequest()->getParam('action'); + // In the case we are dealing with non-standard actions we need to fallback to a standard one + // in order to get access to the contain array. We will use the permissions to decide which + // action to fallback to + if(!in_array($requestAction, [ + 'index', 'view', 'delete', 'add', 'edit' + ])) { + $permissionsArray = $this->getController()->RegistryAuth->calculatePermissionsForView($requestAction); + $id = $this->getController()->getRequest()->getParam('pass')[0] ?? null; + if (isset($id)) { + $requestAction = ( isset($permissionsArray['edit']) && $permissionsArray['edit'] ) ? 'edit' : 'view'; + } else { + $requestAction = 'index'; + } + } $containsList = "get" . ucfirst($requestAction) . "Contains"; $linkTable = TableRegistry::getTableLocator()->get($modelPath); @@ -226,21 +240,7 @@ public function injectPrimaryLink(object $link, bool $index=true, string $linkLa 'edit'; // The action in the following injectParents dictates the action here - // XXX This is a duplicate from StandardController. - if(method_exists($linkTable, 'generateDisplayField')) { - // We don't use a trait for this since each table will implement different logic - - $label = __d('operation', "{$breadcrumbAction}.ai", $linkTable->generateDisplayField($linkObj)); - } else { - // Default view title is edit object display field - $field = $linkTable->getDisplayField(); - - if(!empty($obj->$field)) { - $label = __d('operation', "{$breadcrumbAction}.ai", $obj->$field); - } else { - $label = __d('operation', "{$breadcrumbAction}.ai", __d('controller', $modelsName, [1])); - } - } + [$title,,] = StringUtilities::entityAndActionToTitle($linkObj, $modelPath, $breadcrumbAction); $this->injectParents[ $linkTable->getTable() . $linkObj->id ] = [ 'target' => [ @@ -249,7 +249,7 @@ public function injectPrimaryLink(object $link, bool $index=true, string $linkLa 'action' => $breadcrumbAction, $linkObj->id ], - 'label' => $linkLabel ?? $label + 'label' => $linkLabel ?? $title ]; } diff --git a/app/src/Controller/DashboardsController.php b/app/src/Controller/DashboardsController.php index 96b8902b1..0de92af39 100644 --- a/app/src/Controller/DashboardsController.php +++ b/app/src/Controller/DashboardsController.php @@ -30,6 +30,7 @@ namespace App\Controller; // XXX not doing anything with Log yet +use App\Lib\Util\StringUtilities; use Cake\Log\Log; use Cake\ORM\TableRegistry; use Cake\Utility\Hash; @@ -45,14 +46,14 @@ class DashboardsController extends StandardController { public function initialize(): void { parent::initialize(); - + // Configure breadcrumb rendering $this->Breadcrumb->skipConfig([ - '/^\/dashboards\/artifacts/', - '/^\/dashboards\/dashboard/', - '/^\/dashboards\/registries/', - '/^\/dashboards\/search/' - ]); + '/^\/dashboards\/artifacts/', + '/^\/dashboards\/dashboard/', + '/^\/dashboards\/registries/', + '/^\/dashboards\/search/' + ]); // There is currently no inventory of dashboards, so we skip parents // for configuration, dashboard, and registries actions $this->Breadcrumb->skipParents(['/^\/dashboards/']); @@ -63,19 +64,23 @@ public function initialize(): void { * * @since COmanage Registry v5.0.0 */ - + public function configuration() { $cur_co = $this->getCO(); - - $this->set('vv_title', __d('menu', 'co.features.all')); - + + [$title, , ] = StringUtilities::entityAndActionToTitle(null, + 'co.features', + 'all', + 'menu'); + $this->set('vv_title', $title); + // Construct the set of configuration items. For everything except CO Settings // we want to order by the localized text string. - + // We're assuming that the permission for each of these items is the same as for // configuration() itself, ie: CMP or CO Admin. But plausibly some of this stuff // could be delegated to (eg) a COU Admin at some point... - + $configMenuItems = [ __d('controller', 'ApiUsers', [99]) => [ 'icon' => 'vpn_key', @@ -107,7 +112,7 @@ public function configuration() { 'controller' => 'provisioning_targets', 'action' => 'index' ], -// XXX restore when Reports are ready to be exposed. +// XXX restore when Reports are ready to be exposed. // __d('controller', 'Reports', [99]) => [ // 'icon' => 'summarize', // 'controller' => 'reports', @@ -119,27 +124,27 @@ public function configuration() { 'action' => 'index' ] ]; - + ksort($configMenuItems); - + // Insert CO Settings to the front of the list $configMenuItems = array_merge([ - __d('controller', 'CoSettings', [99]) => [ - 'icon' => 'settings', - 'controller' => 'co_settings', - 'action' => 'manage' - ]], - $configMenuItems + __d('controller', 'CoSettings', [99]) => [ + 'icon' => 'settings', + 'controller' => 'co_settings', + 'action' => 'manage' + ]], + $configMenuItems ); - + $this->set('vv_configuration_menu_items', $configMenuItems); $platformMenuItems = []; - + if($this->getCOID() == 1) { // Also pass the platform menu items - + $platformMenuItems = [ __d('controller', 'Cos', [99]) => [ 'icon' => 'home', @@ -153,11 +158,10 @@ public function configuration() { ] ]; } - + ksort($platformMenuItems); - + $this->set('vv_platform_menu_items', $platformMenuItems); - $registryMenuItems = [ __d('controller', 'Groups', [99]) => [ 'icon' => 'people', @@ -175,11 +179,11 @@ public function configuration() { 'action' => 'index' ] ]; - + ksort($registryMenuItems); - + $this->set('vv_registries_menu_items', $registryMenuItems); - + $artifactMenuItems = [ __d('controller', 'ExtIdentitySourceRecords', [99]) => [ 'icon' => 'assignment', @@ -192,19 +196,18 @@ public function configuration() { 'action' => 'index' ] ]; - + ksort($artifactMenuItems); - + $this->set('vv_artifacts_menu_items', $artifactMenuItems); } - + /** * Render a Dashboard. * * @since COmanage Registry v5.0.0 * @param int $id Dashboard ID */ - public function dashboard(?int $id=null) { // XXX placeholder } @@ -214,15 +217,15 @@ public function dashboard(?int $id=null) { * * @since COmanage Registry v5.0.0 */ - + public function search() { /* To add a new backend to search: * (1) Implement $model->search($id, $q, $limit) * (2) Add the model to $models here, and define which roles can query it * (3) Update documentation at https://spaces.at.internet2.edu/pages/viewpage.action?pageId=243078053 */ - - $models = [ + + $models = [ 'Addresses' => [ 'parent' => ['People' => 'person_id', 'PersonRoles' => 'person_role_id'], 'roles' => ['platformAdmin', 'coAdmin'], @@ -293,7 +296,7 @@ public function search() { // A search was passed in from the form on the Global Search bar. $q = trim($this->request->getData('q')); } - + // Only process the request if we have a string of non-space characters if(!empty($q)) { @@ -358,7 +361,7 @@ public function search() { // or if there is a single result overall, redirect to that result. if((count($results['Cos']) == 0 && (count($results['People']) + count($results['Groups'])) == 1) - || + || (count($results['Cos']) == 1 && (count($results['People']) + count($results['Groups'])) == 0)) { // Figure out which model matched, as well as the target model to redirect to @@ -373,18 +376,18 @@ public function search() { $matchClass = array_key_first($results[$m][$targetRecordId]); } } - + $this->Flash->information(__d('result', 'search.exact', [filter_var($this->request->getData('q'), FILTER_SANITIZE_SPECIAL_CHARS), - __d('controller', $matchClass, [1])])); + __d('controller', $matchClass, [1])])); // Redirect to the matchClass controller return $this->redirect([ - 'controller' => Inflector::dasherize($targetClass), - 'action' => 'edit', - $targetRecordId - ]); + 'controller' => Inflector::dasherize($targetClass), + 'action' => 'edit', + $targetRecordId + ]); // XXX handle plugins } elseif(count($results['Cos']) @@ -394,6 +397,12 @@ public function search() { } $this->set('vv_results', $results); - $this->set('vv_title', __d('result', 'search.results')); + // XXX The action is search and the result is not a modelPath. In this use the pattern is reversed. We should + // probably reconsider the po naming for the result domain + [$title, , ] = StringUtilities::entityAndActionToTitle(null, + 'search', + 'results', + 'result'); + $this->set('vv_title', $title); } } \ No newline at end of file diff --git a/app/src/Controller/ExternalIdentitySourcesController.php b/app/src/Controller/ExternalIdentitySourcesController.php index 53ab3bcc9..f32718045 100644 --- a/app/src/Controller/ExternalIdentitySourcesController.php +++ b/app/src/Controller/ExternalIdentitySourcesController.php @@ -30,6 +30,7 @@ namespace App\Controller; // XXX not doing anything with Log yet +use App\Lib\Util\StringUtilities; use Cake\Log\Log; use Cake\ORM\TableRegistry; @@ -109,15 +110,19 @@ public function retrieve(string $id) { $this->set('vv_eis_record', $this->ExternalIdentitySources->retrieve((int)$id, $source_key)); - $this->set('vv_external_identity_record', $this->ExternalIdentitySources - ->ExtIdentitySourceRecords - ->find() - ->where(['ExtIdentitySourceRecords.source_key' => $source_key, - 'ExtIdentitySourceRecords.external_identity_source_id' => $id]) - ->contain(['ExternalIdentities']) - ->first()); - - $this->set('vv_title', __d('operation', 'view.a', [$source_key])); + $externalIdentityRecordObj = $this->ExternalIdentitySources + ->ExtIdentitySourceRecords + ->find() + ->where(['ExtIdentitySourceRecords.source_key' => $source_key, + 'ExtIdentitySourceRecords.external_identity_source_id' => $id]) + ->contain(['ExternalIdentities']) + ->first(); + $this->set('vv_external_identity_record', $externalIdentityRecordObj); + + [$title, , ] = StringUtilities::entityAndActionToTitle($externalIdentityRecordObj, + StringUtilities::entityToClassName($externalIdentityRecordObj), + 'view'); + $this->set('vv_title', $title); } catch(\Exception $e) { $this->Flash->error($e->getMessage()); @@ -152,7 +157,10 @@ public function search(string $id) { $this->set('vv_search_attrs', $this->ExternalIdentitySources->searchableAttributes((int)$id)); - $this->set('vv_title', __d('operation', 'ExternalIdentitySources.search')); + [$title, , ] = StringUtilities::entityAndActionToTitle(null, + $this->getName(), + $this->request->getParam('action')); + $this->set('vv_title', $title); } /** @@ -167,9 +175,8 @@ public function sync(string $id) { $source_key = $this->request->getQuery('source_key'); $this->ExternalIdentitySources->sync((int)$id, $source_key); + // XXX Sync does not have a view. We do not need to set a title. Yet need to fetch the updated Identity $this->set('vv_eis_record', $this->ExternalIdentitySources->retrieve((int)$id, $source_key)); - - $this->set('vv_title', __d('operation', 'view.a', [$source_key])); $this->Flash->success(__d('result', 'ExternalIdentitySources.synced')); } diff --git a/app/src/Controller/ProvisioningTargetsController.php b/app/src/Controller/ProvisioningTargetsController.php index 0f9f8fef2..27633787f 100644 --- a/app/src/Controller/ProvisioningTargetsController.php +++ b/app/src/Controller/ProvisioningTargetsController.php @@ -30,6 +30,8 @@ namespace App\Controller; // XXX not doing anything with Log yet +use App\Lib\Util\StringUtilities; +use Cake\Utility\Inflector; use Cake\Log\Log; use Cake\ORM\TableRegistry; @@ -85,17 +87,21 @@ public function status() { // PrimaryLinkTrait - Look up our primary link to see which object type we're // working with, an also get our CO ID $link = $this->getPrimaryLink(true); - - if($link->attr == 'person_id') { - $statuses = $this->ProvisioningTargets->status(coId: $link->co_id, personId: (int)$link->value); - } elseif($link->attr == 'group_id') { - $statuses = $this->ProvisioningTargets->status(coId: $link->co_id, groupId: (int)$link->value); - } + // Use argument unpacking operator with names parameters in order to make the call more dynamic + $statusCalculateParams = [ + 'coId' => $link->co_id, + // Currently supported function parameters are personId, groupId + Inflector::variable($link->attr) => (int)$link->value + ]; + $statuses = $this->ProvisioningTargets->status(...$statusCalculateParams); $this->set('vv_provisioning_statuses', $statuses); if(!$this->request->is('restful')) { - $this->set('vv_title', __d('operation', 'provisioning.status')); + [$title, , ] = StringUtilities::entityAndActionToTitle(null, + 'provisioning', + $this->request->getParam('action')); + $this->set('vv_title', $title); } } } \ No newline at end of file diff --git a/app/src/Controller/StandardController.php b/app/src/Controller/StandardController.php index c2ec6813a..808f6214b 100644 --- a/app/src/Controller/StandardController.php +++ b/app/src/Controller/StandardController.php @@ -52,6 +52,8 @@ public function add() { $table = $this->$modelsName; // $tableName = models $tableName = $table->getTable(); + // Create an empty entity for FormHelper + $obj = $table->newEmptyEntity(); if($this->request->is('post')) { try { @@ -100,14 +102,10 @@ public function add() { $this->Flash->error($e->getMessage()); } - - // Pass $obj as context so the view can render validation errors - $this->set('vv_obj', $obj); - } else { - // Create an empty entity for FormHelper - - $this->set('vv_obj', $table->newEmptyEntity()); } + + // Pass $obj as context so the view can render validation errors + $this->set('vv_obj', $obj); // PrimaryLinkTrait, via AppController $this->getPrimaryLink(); @@ -116,13 +114,10 @@ public function add() { $this->populateAutoViewVars(); // Default title is add new object - $this->set('vv_title', __d('operation', 'add.a', __d('controller', $modelsName, [1]))); - - // Supertitle is normally the display name of the parent object when subnavigation exists. - // Set this here as the fallback default. This value is overriden in MVEAController to hold the - // name of the parent object, not the model name of the current object. - // TODO: set this to a better value for other kinds of child objects (e.g. Group member) - $this->set('vv_supertitle', __d('controller', $modelsName, [1])); + [$title, $supertitle, $subtitle] = StringUtilities::entityAndActionToTitle($obj, $modelsName, 'add'); + $this->set('vv_title', $title); + $this->set('vv_supertitle', $supertitle); + $this->set('vv_subtitle', $subtitle); // Let the view render $this->render('/Standard/add-edit-view'); @@ -411,24 +406,13 @@ public function edit(string $id) { // AutoViewVarsTrait $this->populateAutoViewVars($obj); - - if(method_exists($table, 'generateDisplayField')) { - // We don't use a trait for this since each table will implement different logic - - $this->set('vv_title', __d('operation', 'edit.ai', $table->generateDisplayField($obj))); - $this->set('vv_supertitle', $table->generateDisplayField($obj)); - // Pass the display field also into subtitle for dealing with External IDs - $this->set('vv_subtitle', $table->generateDisplayField($obj)); - } else { - // Default view title is edit object display field - $field = $table->getDisplayField(); - - if(!empty($obj->$field)) { - $this->set('vv_title', __d('operation', 'edit.ai', $obj->$field)); - } else { - $this->set('vv_title', __d('operation', 'edit.ai', __d('controller', $modelsName, [1]))); - } - } + + // Calculate and set title, supertitle and subtitle + [$title, $supertitle, $subtitle] = StringUtilities::entityAndActionToTitle($obj, $modelsName, 'edit'); + + $this->set('vv_title', $title); + $this->set('vv_supertitle', $supertitle); + $this->set('vv_subtitle', $subtitle); // Let the view render $this->render('/Standard/add-edit-view'); @@ -646,7 +630,8 @@ public function index() { $this->set('vv_permission_set', $this->RegistryAuth->calculatePermissionsForResultSet($resultSet)); // Default index view title is model name - $this->set('vv_title', __d('controller', $modelsName, [99])); + [$title, , ] = StringUtilities::entityAndActionToTitle($resultSet, $modelsName, 'index'); + $this->set('vv_title', $title); // Let the view render $this->render('/Standard/index'); @@ -915,24 +900,13 @@ public function view($id = null) { // AutoViewVarsTrait // We still used this in view() to map select values $this->populateAutoViewVars($obj); - - if(method_exists($table, 'generateDisplayField')) { - // We don't use a trait for this since each table will implement different logic - - $this->set('vv_title', __d('operation', 'view.ai', $table->generateDisplayField($obj))); - $this->set('vv_supertitle', $table->generateDisplayField($obj)); - // Pass the display field also into subtitle for dealing with External IDs - $this->set('vv_subtitle', $table->generateDisplayField($obj)); - } else { - // Default view title is the object display field - $field = $table->getDisplayField(); - - if(!empty($obj->$field)) { - $this->set('vv_title', __d('operation', 'view.ai', $obj->$field)); - } else { - $this->set('vv_title', __d('operation', 'view.ai', __d('controller', $modelsName, [1]))); - } - } + + // Calculate and set title, supertitle and subtitle + [$title, $supertitle, $subtitle] = StringUtilities::entityAndActionToTitle($obj, $modelsName, 'view'); + + $this->set('vv_title', $title); + $this->set('vv_supertitle', $supertitle); + $this->set('vv_subtitle', $subtitle); // Let the view render $this->render('/Standard/add-edit-view'); diff --git a/app/src/Controller/StandardPluginController.php b/app/src/Controller/StandardPluginController.php index 4a5553948..b15b29a9a 100644 --- a/app/src/Controller/StandardPluginController.php +++ b/app/src/Controller/StandardPluginController.php @@ -98,7 +98,8 @@ public function beforeRender(\Cake\Event\EventInterface $event) { // Override the title set in StandardController. Since that was set in edit() // which is called before the rendering hooks, this title will take precedence. - $this->set('vv_title', __d('operation', 'configure.a', $parentObj->$parentDisplayField)); + [$title, , ] = StringUtilities::entityAndActionToTitle($parentObj, $parentClassName, 'configure'); + $this->set('vv_title', $title); } return parent::beforeRender($event); diff --git a/app/src/Lib/Util/StringUtilities.php b/app/src/Lib/Util/StringUtilities.php index b4f959ee4..5b9249846 100644 --- a/app/src/Lib/Util/StringUtilities.php +++ b/app/src/Lib/Util/StringUtilities.php @@ -29,6 +29,7 @@ namespace App\Lib\Util; +use Cake\ORM\TableRegistry; use \Cake\Utility\Inflector; class StringUtilities { @@ -125,6 +126,81 @@ public static function entityToForeignKey($entity): string { return Inflector::underscore(Inflector::singularize(substr($classPath, strrpos($classPath, '\\')+1))) . "_id"; } + /** + * Construct the title, supertitle and subtitle for a given Model and action + * + * - if the Entity is null then we construct the message ID by concatenating the modelPath and the action + * - if the action is null then the message ID is the modelsName + * - if the action is the Index View then the message ID is the modelsName + "others", which in this case is the plural of the name + * - in all other cases the message id is constructed by the displayField. Either it is defined or dynamically + * constructed + * + * @param Entity|null $entity Entity object + * @param string $modelPath The path of the Model, from core Models it is the Model Name. For plugins it is the Plugin.ModelName + * @param string|null $action Request Action + * @param string $domain The po file the message ID is located in + * + * @return array List of title, supertitle, subtitle + */ + public static function entityAndActionToTitle($entity, + string $modelPath, + ?string $action, + string $domain='operation'): array { + $supertitle = ''; + $subtitle = ''; + $title = ''; + + if($entity === null) { + return [__d($domain, "{$modelPath}.{$action}"), '', '']; + } + + $plugin = ''; + $modelsName = $modelPath; + if(str_contains($modelPath, '.')) { + [$plugin, $modelsName] = explode('.', $modelPath, 2); + } + + $linkTable = TableRegistry::getTableLocator()->get($modelPath); + $msgId = "{$action}.a"; + + if(Inflector::singularize(self::entityToClassName($entity)) !== Inflector::singularize($modelsName)) { + $linkTable = TableRegistry::getTableLocator()->get(self::entityToClassName($entity)); + // if the modelPath and the action are equal then we skip the concatenation + $msgId = $modelPath === $action ? $modelPath : "{$modelPath}.{$action}"; + } + + if($action === null) { + return [__d('controller', $modelsName), '', '']; + } + + // Index view + if($action === 'index') { + // 99 is the default for plural + return [__d('controller', $modelsName, [99]), '', '']; + } + + // Add/Edit/View + if(method_exists($linkTable, 'generateDisplayField')) { + // We don't use a trait for this since each table will implement different logic + + $title = __d($domain, $msgId, $linkTable->generateDisplayField($entity)); + $supertitle = $linkTable->generateDisplayField($entity); + // Pass the display field also into subtitle for dealing with External IDs + $subtitle = $linkTable->generateDisplayField($entity); + } else { + // Default view title is edit object display field + $field = $linkTable->getDisplayField(); + + if(!empty($entity->$field)) { + $title = __d($domain, $msgId, $entity->$field); + } else { + $title = __d($domain, $msgId, __d('controller', $modelsName, [1])); + } + } + + return [$title, $supertitle, $subtitle]; + } + /** * Determine the class name from a foreign key (eg: report_id -> Reports). *