From 0be711547aa16b670a6a5ffd88bc43f228bdd22b Mon Sep 17 00:00:00 2001 From: Benn Oshrin Date: Mon, 25 Oct 2021 20:42:09 -0400 Subject: [PATCH] Initial commit of Types (CFM-56) --- app/config/schema/schema.json | 11 +- app/resources/locales/en_US/default.po | 64 ++++ app/resources/locales/en_US/defaultType.po | 40 +++ app/src/Command/TransmogrifyCommand.php | 29 +- app/src/Controller/DashboardsController.php | 5 + app/src/Controller/StandardController.php | 26 +- app/src/Controller/TypesController.php | 78 +++++ app/src/Lib/Enum/EduPersonAffiliationEnum.php | 41 +++ app/src/Lib/Traits/TypeTrait.php | 86 +++++ app/src/Model/Behavior/ChangelogBehavior.php | 6 + app/src/Model/Entity/Type.php | 40 +++ app/src/Model/Table/CosTable.php | 43 +++ app/src/Model/Table/NamesTable.php | 33 +- app/src/Model/Table/TypesTable.php | 324 ++++++++++++++++++ app/src/View/Helper/FieldHelper.php | 9 +- app/templates/Standard/index.php | 29 +- app/templates/Types/columns.inc | 52 +++ app/templates/Types/fields.inc | 61 ++++ 18 files changed, 953 insertions(+), 24 deletions(-) create mode 100644 app/resources/locales/en_US/defaultType.po create mode 100644 app/src/Controller/TypesController.php create mode 100644 app/src/Lib/Enum/EduPersonAffiliationEnum.php create mode 100644 app/src/Lib/Traits/TypeTrait.php create mode 100644 app/src/Model/Entity/Type.php create mode 100644 app/src/Model/Table/TypesTable.php create mode 100644 app/templates/Types/columns.inc create mode 100644 app/templates/Types/fields.inc diff --git a/app/config/schema/schema.json b/app/config/schema/schema.json index bda138bd3..d0223ad1e 100644 --- a/app/config/schema/schema.json +++ b/app/config/schema/schema.json @@ -43,15 +43,16 @@ "columns": { "id": {}, "co_id": {}, - "attribute": { "type": "string", "size": 32 }, - "name": { "type": "string", "size": 32 }, - "display_name": { "type": "string", "size": 64 }, + "attribute": { "type": "string", "size": 32, "notnull": true }, + "display_name": { "type": "string", "size": 64, "notnull": true }, + "value": { "type": "string", "size": 32, "notnull": true }, "edupersonaffiliation": { "type": "string", "size": 32 }, "status": {} }, "indexes": { - "types_i1": { "columns": [ "co_id", "attribute" ] }, - "types_i2": { "columns": [ "co_id", "attribute", "name" ] } + "types_i1": { "columns": [ "co_id" ] }, + "types_i2": { "columns": [ "co_id", "attribute" ] }, + "types_i3": { "columns": [ "co_id", "attribute", "value" ] } } }, diff --git a/app/resources/locales/en_US/default.po b/app/resources/locales/en_US/default.po index c6a235dc2..e883b0f96 100644 --- a/app/resources/locales/en_US/default.po +++ b/app/resources/locales/en_US/default.po @@ -92,6 +92,9 @@ msgstr "{0,plural,=1{External Identity} other{External Identities}}" msgid "registry.ct.People" msgstr "{0,plural,=1{Person} other{People}}" +msgid "registry.ct.Types" +msgstr "{0,plural,=1{Type} other{Types}}" + ### Enumerations msgid "registry.en.BooleanEnum.0" msgstr "False" @@ -99,6 +102,30 @@ msgstr "False" msgid "registry.en.BooleanEnum.1" msgstr "True" +msgid "registry.en.EduPersonAffiliationEnum.affiliate" +msgstr "Affiliate" + +msgid "registry.en.EduPersonAffiliationEnum.alum" +msgstr "Alum" + +msgid "registry.en.EduPersonAffiliationEnum.employee" +msgstr "Employee" + +msgid "registry.en.EduPersonAffiliationEnum.faculty" +msgstr "Faculty" + +msgid "registry.en.EduPersonAffiliationEnum.librarywalkin" +msgstr "Library Walk-In" + +msgid "registry.en.EduPersonAffiliationEnum.member" +msgstr "Member" + +msgid "registry.en.EduPersonAffiliationEnum.staff" +msgstr "Staff" + +msgid "registry.en.EduPersonAffiliationEnum.student" +msgstr "Student" + msgid "registry.en.SetBooleanEnum.0" msgstr "Not Set" @@ -244,6 +271,9 @@ msgstr "When this value is selected, {0} cannot be empty" msgid "registry.er.input.invalid" msgstr "Invalid character found" +msgid "registry.er.invalid" +msgstr "Invalid value \"{0}\"" + msgid "registry.er.notfound" msgstr "{0} not found" @@ -271,6 +301,12 @@ msgstr "No type defined for table {0} column {1}" msgid "registry.er.schema.parse" msgstr "Failed to parse file {0}" +msgid "registry.er.Types.inuse" +msgstr "Type {0} is in use and cannot be deleted" + +msgid "registry.er.unknown" +msgstr "Unknown value \"{0}\"" + ### Fields ### Keys of the form registry.fd.MyModels.field_name[.desc] will apply only to MyModels.field_name ### Keys of the form registry.fd.field_name[.desc] will apply if no model specific key is found @@ -280,12 +316,25 @@ msgstr "Action" msgid "registry.fd.api_key" msgstr "API Key" +msgid "registry.fd.attribute" +msgstr "Attribute" + msgid "registry.fd.date_of_birth" msgstr "Date of Birth" msgid "registry.fd.description" msgstr "Description" +msgid "registry.fd.display_name" +msgstr "Display Name" + +msgid "registry.fd.edupersonaffiliation" +msgstr "eduPersonAffiliation" + +msgid "registry.fd.Types.edupersonaffiliation.desc" +# XXX update link to PE wiki? +msgstr "Map the extended affiliation to this eduPersonAffiliation, see eduPersonAffiliation and Extended Affiliations" + msgid "registry.fd.family" msgstr "Family Name" @@ -316,6 +365,9 @@ msgstr "Required" msgid "registry.fd.status" msgstr "Status" +msgid "registry.fd.Type.status" +msgstr "Suspending a Type will prevent it from being assigned to new attributes, but will not remove it from existing attributes" + msgid "registry.fd.username" msgstr "Username" @@ -340,6 +392,15 @@ msgstr "Leave blank for indefinite validity" msgid "registry.fd.valid_through.tz" msgstr "Valid Through ({0})" +msgid "registry.fd.value" +msgstr "Value" + +msgid "registry.fd.Types.value" +msgstr "Database Value" + +msgid "registry.fd.Types.value.desc" +msgstr "Database value for this type, characters must be alphanumeric, dot, or dash" + msgid "registry.home.collab" msgstr "Available Collaborations" @@ -421,6 +482,9 @@ msgstr "Go To Page" msgid "registry.op.previous" msgstr "Previous" +msgid "registry.op.Types.restore" +msgstr "Add/Restore Default Types" + msgid "registry.op.save" msgstr "Save" diff --git a/app/resources/locales/en_US/defaultType.po b/app/resources/locales/en_US/defaultType.po new file mode 100644 index 000000000..ca0cf9afe --- /dev/null +++ b/app/resources/locales/en_US/defaultType.po @@ -0,0 +1,40 @@ +# COmanage Registry Localizations (defaultType domain) +# +# 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) + +# Labels for default types when a new CO is created + +msgid "Names.alternate" +msgstr "Alternate" + +msgid "Names.author" +msgstr "Author" + +msgid "Names.fka" +msgstr "Formerly Known As" + +msgid "Names.official" +msgstr "Official" + +msgid "Names.preferred" +msgstr "Preferred" diff --git a/app/src/Command/TransmogrifyCommand.php b/app/src/Command/TransmogrifyCommand.php index 919fbc820..859547ac5 100644 --- a/app/src/Command/TransmogrifyCommand.php +++ b/app/src/Command/TransmogrifyCommand.php @@ -52,11 +52,13 @@ class TransmogrifyCommand extends Command { 'source' => 'cm_co_extended_types', 'displayField' => 'display_name', 'fieldMap' => [ + 'attribute' => '&map_extended_type', + 'name' => 'value', // For some reason, cm_co_extended_types never had created/modified metadata 'created' => '&map_now', 'modified' => '&map_now' ], - 'cache' => [ [ 'co_id', 'attribute', 'name' ] ] + 'cache' => [ [ 'co_id', 'attribute', 'value' ] ] ], 'api_users' => [ 'source' => 'cm_api_users', @@ -162,7 +164,7 @@ protected function cacheResults(string $table, array $row) { $key = ""; foreach($field as $subfield) { - // eg: co_id+attribute+name+ + // eg: co_id+attribute+value+ $label .= $subfield . "+"; // eg: 2+Identifier.type+eppn+ @@ -422,6 +424,25 @@ protected function mapFields(string $table, array &$row) { } } + /** + * Map an Extended Type attribute name for model name changes. + * + * @since COmanage Registry v5.0.0 + * @param array $row Row of table data + * @return string Updated attribute name + */ + + protected function map_extended_type(array $row) { + switch($row['attribute']) { + case 'CoDepartment.type': + return 'Department.type'; + case 'CoPersonRole.affiliation': + return 'PersonRole.affiliation'; + } + + return $row['attribute']; + } + /** * Map an identifier type string to a foreign key. * @@ -535,10 +556,10 @@ protected function map_type(array $row, string $type, $coId) { $key = $coId . "+" . $type . "+" . $row['type'] . "+"; - if(empty($this->cache['types']['co_id+attribute+name+'][$key])) { + if(empty($this->cache['types']['co_id+attribute+value+'][$key])) { throw new \InvalidArgumentException("Type not found for " . $key); } - return $this->cache['types']['co_id+attribute+name+'][$key]; + return $this->cache['types']['co_id+attribute+value+'][$key]; } } diff --git a/app/src/Controller/DashboardsController.php b/app/src/Controller/DashboardsController.php index f6abfc6ff..dbcaab3af 100644 --- a/app/src/Controller/DashboardsController.php +++ b/app/src/Controller/DashboardsController.php @@ -80,6 +80,11 @@ public function configuration() { 'icon' => 'people_outline', 'controller' => 'cous', 'action' => 'index' + ], + __('registry.ct.Types', [99]) => [ + 'icon' => 'widgets', + 'controller' => 'types', + 'action' => 'index' ] ]; diff --git a/app/src/Controller/StandardController.php b/app/src/Controller/StandardController.php index 16af3c53e..08b655276 100644 --- a/app/src/Controller/StandardController.php +++ b/app/src/Controller/StandardController.php @@ -59,7 +59,7 @@ public function add() { if($table->save($obj)) { $this->Flash->success(__('registry.rs.saved')); - return $this->generateRedirect($obj->id); + return $this->generateRedirect(null); } $errors = $obj->getErrors(); @@ -256,7 +256,11 @@ public function delete($id) { } // Return to index since there is no delete view - return $this->generateRedirect(); + return $this->generateRedirect(null); + } + catch(\Cake\ORM\Exception\PersistenceFailedException $e) { + // deleteOrFail throws Cake\ORM\Exception\PersistenceFailedException + $this->Flash->error($e->getMessage()); } catch(\Exception $e) { // findById throws Cake\Datasource\Exception\RecordNotFoundException @@ -276,10 +280,10 @@ public function delete($id) { } else { $this->Flash->error($e->getMessage()); } - - // The record is still valid, so redirect back to it - return $this->redirect(['action' => 'edit', $id]); } + + // The record is still valid, so redirect back to it + return $this->redirect(['action' => 'edit', $id]); } /** @@ -340,7 +344,7 @@ public function edit($id) { if($table->save($obj)) { $this->Flash->success(__('registry.rs.saved')); - return $this->generateRedirect(); + return $this->generateRedirect($obj->id); } $errors = $obj->getErrors(); @@ -358,7 +362,7 @@ public function edit($id) { // findById throws Cake\Datasource\Exception\RecordNotFoundException $this->Flash->error($e->getMessage()); - return $this->generateRedirect(); + return $this->generateRedirect(null); } $this->set('vv_obj', $obj); @@ -395,7 +399,7 @@ public function edit($id) { public function generateRedirect(?int $id) { $redirect = []; - if($this->request->getParam('action') == 'add' && $id) { + if(in_array($this->request->getParam('action'), ['add', 'edit']) && $id) { // Redirect to the edit view of the record just added // (if the user has add permission, they probably have edit permission) @@ -515,6 +519,12 @@ protected function populateAutoViewVars(object $obj=null) { && $table->getAutoViewVars()) { foreach($table->getAutoViewVars() as $vvar => $avv) { switch($avv['type']) { + case 'array': + // Use the provided array of values. By default, we use the values + // for the keys as well, to generate HTML along the lines of + // + $this->set($vvar, array_combine($avv['array'], $avv['array'])); + break; case 'enum': // We just want the localized text strings for the defined constants $class = '\\App\\Lib\\Enum\\'.$avv['class']; diff --git a/app/src/Controller/TypesController.php b/app/src/Controller/TypesController.php new file mode 100644 index 000000000..fb4033346 --- /dev/null +++ b/app/src/Controller/TypesController.php @@ -0,0 +1,78 @@ + [ + 'delete' => ['platformAdmin', 'coAdmin'], + 'edit' => ['platformAdmin', 'coAdmin'], + 'view' => ['platformAdmin', 'coAdmin'] + ], + // Actions that operate over a table (ie: do not require an $id) + 'table' => [ + 'add' => ['platformAdmin', 'coAdmin'], + 'index' => ['platformAdmin', 'coAdmin'], + 'restore' => ['platformAdmin', 'coAdmin'] + ] + ]; + + public $pagination = [ + 'order' => [ + 'Types.attribute' => 'asc', + 'Types.display_name' => 'asc' + ] + ]; + + /** + * Restore default types for the requested CO. + * + * @since COmanage Registry v5.0.0 + */ + + public function restore() { + try { + $this->Types->addDefaults($this->getCOID()); + + $this->Flash->success(__('registry.rs.saved')); + } + catch(\Exception $e) { + // findById throws Cake\Datasource\Exception\RecordNotFoundException + + $this->Flash->error($e->getMessage()); + } + + return $this->generateRedirect(null); + } +} \ No newline at end of file diff --git a/app/src/Lib/Enum/EduPersonAffiliationEnum.php b/app/src/Lib/Enum/EduPersonAffiliationEnum.php new file mode 100644 index 000000000..dcb62d314 --- /dev/null +++ b/app/src/Lib/Enum/EduPersonAffiliationEnum.php @@ -0,0 +1,41 @@ +getTableLocator()->get("Types"); + + $query = $Types->find('list', [ + 'keyField' => 'value', + 'valueField' => 'display_name' + ]) + ->where(['co_id' => $coId, + 'attribute' => $attribute, + 'status' => SuspendableStatusEnum::Active]) + ->order(['Types.display_name' => 'ASC']); + + return $query->toArray(); + } + + /** + * Obtain the default (out of the box) types for this model. + * + * @since COmanage Registry v5.0.0 + * @param string $attribute Attribute to obtain default types for + * @return array Array of default types and their default strings + * @throws InvalidArgumentException + */ + + public function defaultTypes(string $attribute) { + $ret = []; + + if(!isset($this->defaultTypes[$attribute])) { + throw new \InvalidArgumentException(__('registry.er.invalid', [$attribute])); + } + + // eg: "Name" + foreach($this->defaultTypes[$attribute] as $t) { + // Map to localized text string + $ret[$t] = __d('defaultType', $this->getAlias().'.'.$t); + } + + return $ret; + } +} diff --git a/app/src/Model/Behavior/ChangelogBehavior.php b/app/src/Model/Behavior/ChangelogBehavior.php index d1acdd2ff..351f60346 100644 --- a/app/src/Model/Behavior/ChangelogBehavior.php +++ b/app/src/Model/Behavior/ChangelogBehavior.php @@ -76,6 +76,12 @@ public function beforeFind(\Cake\Event\Event $event, \Cake\ORM\Query $query, \Ar $alias = $subject->getAlias(); $parentfk = Inflector::singularize($table) . "_id"; + if(isset($options['archived']) && $options['archived']) { + // XXX need to the same check for expunge + + return true; + } + LogBehavior::strace($alias, 'Changelog altering find conditions'); // XXX add support for archived, revision, etc diff --git a/app/src/Model/Entity/Type.php b/app/src/Model/Entity/Type.php new file mode 100644 index 000000000..0c862f615 --- /dev/null +++ b/app/src/Model/Entity/Type.php @@ -0,0 +1,40 @@ + true, + 'id' => false, + 'slug' => false, + ]; +} \ No newline at end of file diff --git a/app/src/Model/Table/CosTable.php b/app/src/Model/Table/CosTable.php index 4ebfaafa5..5fe5e4b2f 100644 --- a/app/src/Model/Table/CosTable.php +++ b/app/src/Model/Table/CosTable.php @@ -32,6 +32,7 @@ use Cake\ORM\Query; use Cake\ORM\RulesChecker; use Cake\ORM\Table; +use Cake\ORM\TableRegistry; use Cake\Validation\Validator; use \App\Lib\Enum\TemplateableStatusEnum; @@ -68,6 +69,8 @@ public function initialize(array $config): void { ->setDependent(true); $this->hasMany('Dashboards') ->setDependent(true); + $this->hasMany('Types') + ->setDependent(true); $this->setDisplayField('name'); @@ -79,6 +82,26 @@ public function initialize(array $config): void { ]); } + /** + * Callback after model save. + * + * @since COmanage Registry v5.0.0 + * @param EventInterface $event Event + * @param EntityInterface $entity Entity (ie: Co) + * @param ArrayObject $options Save options + * @return bool True on success + */ + + public function afterSave(\Cake\Event\EventInterface $event, \Cake\Datasource\EntityInterface $entity, \ArrayObject $options) { + if($entity->isNew() && !empty($entity->id)) { + // Run setup for new CO + + $this->setup($entity->id); + } + + return true; + } + /** * Define business rules. * @@ -169,6 +192,26 @@ public function ruleIsActive($entity, $options) { return true; } + /** + * Perform initial setup for a CO. + * + * @since COmanage Registry v0.9.2 + * @param int $id CO ID + * @return bool True on success + */ + + public function setup(int $id) { + $Type = TableRegistry::getTableLocator()->get('Types'); + + // AR-Type-1 Set up the default values for extended types + $Type->addDefaults($id); + + // Create the default groups +// $this->CoGroup->addDefaults($coId); + + return true; + } + /** * Set validation rules. * diff --git a/app/src/Model/Table/NamesTable.php b/app/src/Model/Table/NamesTable.php index 8e57c9979..b327f26cc 100644 --- a/app/src/Model/Table/NamesTable.php +++ b/app/src/Model/Table/NamesTable.php @@ -40,8 +40,21 @@ class NamesTable extends Table { use \App\Lib\Traits\PrimaryLinkTrait; use \App\Lib\Traits\RulesTrait; use \App\Lib\Traits\TableMetaTrait; + use \App\Lib\Traits\TypeTrait; use \App\Lib\Traits\ValidationTrait; + // Default "out of the box" types for this model. Entries here should be + // given a default localization in app/resources/locales/*/defaultType.po + protected $defaultTypes = [ + 'type' => [ + 'alternate', + 'author', + 'fka', + 'official', + 'preferred' + ] + ]; + /** * Perform Cake Model initialization. * @@ -79,9 +92,9 @@ public function initialize(array $config): void { public function validationDefault(Validator $validator): Validator { // One of CO Person ID or Org Identity ID is required -// XXX Test this via the API? +// XXX Test this via the API? XXX we don't want to allow these to be reassigned $validator->add( - 'co_person_id', + 'person_id', 'content', [ 'rule' => 'isInteger' ] ); @@ -90,7 +103,7 @@ public function validationDefault(Validator $validator): Validator { }); $validator->add( - 'org_identity_id', + 'external_identity_id', 'content', [ 'rule' => 'isInteger' ] ); @@ -163,7 +176,19 @@ public function validationDefault(Validator $validator): Validator { ); $validator->allowEmpty('suffix'); -// XXX need to do something to validate type (test via API) + $validator->add( + 'type_id', + 'content', + [ 'rule' => 'isInteger' ] + ); + $validator->add( + 'type_id', + 'content', +// XXX maybe this should be more generic? validateCO? + [ 'rule' => [ 'validateType' ], + 'provider' => 'table' ] + ); + $validator->notEmpty('type_id'); $validator->add( 'language', diff --git a/app/src/Model/Table/TypesTable.php b/app/src/Model/Table/TypesTable.php new file mode 100644 index 000000000..b44fbd25f --- /dev/null +++ b/app/src/Model/Table/TypesTable.php @@ -0,0 +1,324 @@ +addBehavior('Changelog'); + $this->addBehavior('Log'); + $this->addBehavior('Timestamp'); + + // Types are configuration + $this->setIsConfigurationTable(true); + + // Define associations + $this->belongsTo('Cos'); + $this->hasMany('identifiers'); + $this->hasMany('Names'); +// XXX add other MVEA models + + $this->setDisplayField('display_name'); + + $this->setPrimaryLink('co_id'); + $this->setRequiresCO(true); + $this->setAllowUnkeyedPrimaryLink(['restore']); + + $this->setAutoViewVars([ + 'attributes' => [ + 'type' => 'array', + 'array' => $this->supportedAttributes + ], + 'edupersonaffiliations' => [ + 'type' => 'enum', + 'class' => 'EduPersonAffiliationEnum' + ], + 'statuses' => [ + 'type' => 'enum', + 'class' => 'SuspendableStatusEnum' + ] + ]); + } + + /** + * Add the default types for an attribute. + * + * @since COmanage Registry v0.9.2 + * @param int $coId CO ID + * @param string $attribute Attribute, of the form Model.attribute + * @return bool True on success + * @throws InvalidArgumentException + * @throws PersistenceFailedException + */ + + public function addDefault(int $coId, string $attribute) { + // Make sure $attribute is valid + + if(!in_array($attribute, $this->supportedAttributes)) { + throw new \InvalidArgumentException(__('registry.er.unknown', [$attribute])); + } + + // Split $attribute + $attr = explode('.', $attribute, 2); + + // We need the appropriate model for $attribute to manipulate the default types + // $table = (eg) NamesTable + $table = TableRegistry::getTableLocator()->get($attr[0]); + + // The current set of types for this model, of the form value => display_name + $current = $table->availableTypes($coId, $attribute); + + // The default types for this model, of the same form + $modelDefault = $table->defaultTypes($attr[1]); + + // Construct a set of arrays that we'll convert to entities to save + $records = []; + + foreach($modelDefault as $value => $displayName) { + if(!array_key_exists($value, $current)) { + $records[] = [ + 'co_id' => $coId, + 'attribute' => $attribute, + 'display_name' => $displayName, + 'value' => $value, + 'status' => SuspendableStatusEnum::Active + ]; + } + } + + // Convert the arrays to entities + $entities = $this->newEntities($records); + + // throws PersistenceFailedException on failure + $this->saveManyOrFail($entities); + + return true; + } + + /** + * Add all default values for extended types for the specified CO. + * + * @since COmanage Registry v0.9.2 + * @param int $coId CO ID + * @return bool True on success + * @throws RuntimeException + */ + + public function addDefaults(int $coId) { + foreach(array_values($this->supportedAttributes) as $t) { + try { + $this->addDefault($coId, $t); + } + catch(\Exception $e) { + throw new \RuntimeException($e->getMessage()); + } + } + + return true; + } + + /** + * Define business rules. + * + * @since COmanage Registry v5.0.0 + * @param RulesChecker $rules RulesChecker object + * @return RulesChecker + */ + + public function buildRules(RulesChecker $rules): RulesChecker { + // AR-Type-2 A Type cannot be deleted once used at least one time + $rules->addDelete([$this, 'ruleTypeInUse'], + 'typeInUse', + ['errorField' => 'type_id']); + + return $rules; + } + + /** + * Determine if this type is in use. + * + * @since COmanage Registry v5.0.0 + * @param Entity $entity Entity to be validated + * @return boolean true if the entity is in use, false otherwise + */ + + public function typeInUse($entity) { + $attr = explode('.', $entity->attribute, 2); + + // Pull the table for this attribute, then see if there are any records + // where the column matches the requested type ID + + $table = TableRegistry::getTableLocator()->get($attr[0]); + + // We include changelog-archived records for referential integrity... if a + // record was created that references this type and then was subsequently + // deleted, it is still considered "in use". + + $count = $table->find('all', ['archived' => true]) + ->where([$attr[1]."_id" => $entity->id]) + ->count(); + + return $count != 0; + } + + /** + * Application Rule to determine if the requested type is in use. + * + * @since COmanage Registyr v5.0.0 + * @param Entity $entity Entity to be validated + * @param array $options Application rule options + * @return boolean true if the Rule check passes, false otherwise + */ + + public function ruleTypeInUse($entity, $options) { + if($this->typeInUse($entity)) { + return __('registry.er.Types.inuse', [$entity->value]); + } + + return true; + } + + /** + * Set validation rules. + * + * @since COmanage Registry v5.0.0 + * @param Validator $validator Validator + * @return Validator Validator + */ + + public function validationDefault(Validator $validator): Validator { + $validator->add( + 'co_id', + 'content', + [ 'rule' => 'isInteger' ] + ); + $validator->notEmpty('co_id'); + + $validator->add( + 'attribute', + 'content', + [ 'rule' => [ 'inList', $this->supportedAttributes ] ] + ); + $validator->notEmpty('attribute'); + + $validator->add( + 'value', + 'length', + [ 'rule' => [ 'maxLength', 32 ] ] + ); + $validator->add( + 'value', + 'content', + [ 'rule' => [ 'custom', '/^[a-zA-Z0-9\-\.]+$/' ] ] + ); + $validator->notEmpty('value'); + + $validator->add( + 'display_name', + 'length', + [ 'rule' => [ 'maxLength', 64 ] ] + ); + $validator->add( + 'display_name', + 'content', + [ 'rule' => [ 'validateInput' ], + 'provider' => 'table' ] + ); + $validator->notEmpty('display_name'); + + $validator->add( + 'edupersonaffiliation', + 'content', + [ 'rule' => [ 'inList', [ + EduPersonAffiliationEnum::Affiliate, + EduPersonAffiliationEnum::Alum, + EduPersonAffiliationEnum::Employee, + EduPersonAffiliationEnum::Faculty, + EduPersonAffiliationEnum::LibraryWalkIn, + EduPersonAffiliationEnum::Member, + EduPersonAffiliationEnum::Staff, + EduPersonAffiliationEnum::Student + ] ] ] + ); + $validator->allowEmpty('edupersonaffiliation'); + + $validator->add( + 'status', + 'content', + [ 'rule' => [ 'inList', [ + SuspendableStatusEnum::Active, + SuspendableStatusEnum::Suspended + ] ] ] + ); + $validator->notEmpty('status'); + + return $validator; + } +} \ No newline at end of file diff --git a/app/src/View/Helper/FieldHelper.php b/app/src/View/Helper/FieldHelper.php index 242f0c4b5..8ae5ea251 100644 --- a/app/src/View/Helper/FieldHelper.php +++ b/app/src/View/Helper/FieldHelper.php @@ -78,7 +78,7 @@ public function control(string $fieldName, string $labelText=null) { $coptions = $options; $coptions['label'] = false; - $coptions['readonly'] = !$this->editable; + $coptions['readonly'] = !$this->editable || (isset($options['readonly']) && $options['readonly']); // Selects, Checkboxes, and Radio Buttons use "disabled" $coptions['disabled'] = $coptions['readonly']; @@ -103,6 +103,13 @@ public function control(string $fieldName, $controlCode = $this->Form->text($fieldName, $coptions); $liClass = " modelbox-data"; } else { + if($fieldName != 'status' && !isset($options['empty'])) { + // Cause any select (except status) to render with a blank option, even + // if the field is required. This makes it clear when a value need to be set. + // Note this will be ignore for non-select controls. + $coptions['empty'] = true; + } + $controlCode = $this->Form->control($fieldName, $coptions); } diff --git a/app/templates/Standard/index.php b/app/templates/Standard/index.php index 19d195713..ec622fe3c 100644 --- a/app/templates/Standard/index.php +++ b/app/templates/Standard/index.php @@ -100,10 +100,35 @@ function _column_key($modelsName, $c, $tz=null) { diff --git a/app/templates/Types/columns.inc b/app/templates/Types/columns.inc new file mode 100644 index 000000000..ff3d7726b --- /dev/null +++ b/app/templates/Types/columns.inc @@ -0,0 +1,52 @@ + [ + 'type' => 'link', + 'sortable' => true + ], + 'attribute' => [ + 'type' => 'echo', + 'sortable' => true + ], + 'status' => [ + 'type' => 'enum', + 'class' => 'StatusEnum', + 'sortable' => true + ] +]; + +$topLinks = [ + [ + 'icon' => 'settings_backup_restore', + 'label' => __('registry.op.Types.restore'), + 'link' => [ + 'action' => 'restore' + ] + ] +]; \ No newline at end of file diff --git a/app/templates/Types/fields.inc b/app/templates/Types/fields.inc new file mode 100644 index 000000000..1ea6d9088 --- /dev/null +++ b/app/templates/Types/fields.inc @@ -0,0 +1,61 @@ + + +Field->control('attribute', + ['onChange' => 'fields_update_gadgets();', + 'readonly' => $vv_action == 'edit']); + + print $this->Field->control('display_name'); + + print $this->Field->control('value'); + + print $this->Field->control('status'); + + print $this->Field->control('edupersonaffiliation'); +}