diff --git a/app/config/schema/schema.json b/app/config/schema/schema.json index 297b7f8fb..ac3515d8b 100644 --- a/app/config/schema/schema.json +++ b/app/config/schema/schema.json @@ -13,6 +13,7 @@ "cou_id": { "type": "integer", "foreignkey": { "table": "cous", "column": "id" } }, "description": { "type": "string", "size": 128 }, "external_identity_id": { "type": "integer", "foreignkey": { "table": "external_identities", "column": "id" } }, + "external_identity_role_id": { "type": "integer", "foreignkey": { "table": "external_identity_roles", "column": "id" } }, "id": { "type": "integer", "autoincrement": true, "primarykey": true }, "language": { "type": "string", "size": 16 }, "name": { "type": "string", "size": 128, "notnull": true }, @@ -182,18 +183,24 @@ }, "external_identities": { - "comment": [ - "XXX most of these fields are going to move to person_roles instead", - "XXX how is manager_identifier and sponsor_identifier going to work? we can fk from people but not external_identities", - "XXX affiliation should become affiliation_type_id" - ], - "columns": { "id": {}, "person_id": { "notnull": true }, "status": {}, - "affiliation": { "type": "string", "size": 32 }, - "date_of_birth": { "type": "date" }, + "date_of_birth": { "type": "date" } + }, + "indexes": { + "external_identities_i1": { "columns": [ "person_id" ] } + } + }, + + "external_identity_roles": { + "columns": { + "id": {}, + "external_identity_id": { "notnull": true }, + "status": {}, + "ordr": { "type": "integer" }, + "affiliation_type_id": { "type": "integer", "foreignkey": { "table": "types", "column": "id" } }, "title": { "type": "string", "size": 128 }, "organization": { "type": "string", "size": 128 }, "department": { "type": "string", "size": 128 }, @@ -203,7 +210,8 @@ "valid_through": {} }, "indexes": { - "external_identities_i1": { "columns": [ "person_id" ] } + "external_identity_roles_i1": { "columns": [ "external_identity_id" ] }, + "external_identity_roles_i2": { "columns": [ "affiliation_type_id" ] } } }, @@ -238,7 +246,7 @@ "indexes": { "ad_hoc_attributes_i1": { "columns": [ "tag" ] } }, - "mvea": [ "person", "person_role", "external_identity" ], + "mvea": [ "person", "person_role", "external_identity", "external_identity_role" ], "sourced": true }, @@ -258,7 +266,7 @@ "indexes": { "addresses_i1": { "columns": [ "type_id" ] } }, - "mvea": [ "person", "person_role", "external_identity" ], + "mvea": [ "person", "person_role", "external_identity", "external_identity_role" ], "sourced": true }, @@ -309,7 +317,7 @@ "indexes": { "telephone_numbers_i1": { "columns": [ "type_id" ] } }, - "mvea": [ "person", "person_role", "external_identity" ], + "mvea": [ "person", "person_role", "external_identity", "external_identity_role" ], "sourced": true }, @@ -328,8 +336,6 @@ }, "history_records": { - "comment": "XXX not all foreign keys are defined yet", - "columns": { "id": {}, "action": { "type": "string", "size": 4 }, @@ -337,19 +343,22 @@ "person_id": {}, "person_role_id": {}, "external_identity_id": {}, + "external_identity_role_id": {}, "actor_person_id": { "type": "integer", "foreignkey": { "table": "people", "column": "id" } } }, "indexes": { "history_records_i1": { "columns": [ "person_id" ] }, "history_records_i2": { "columns": [ "external_identity_id" ] }, - "history_records_i3": { "columns": [ "actor_person_id" ] } + "history_records_i3": { "columns": [ "actor_person_id" ] }, + "history_records_i4": { "columns": [ "person_role_id" ] }, + "history_records_i5": { "columns": [ "external_identity_role_id" ] } } } }, "drop-tables":[ { - "comment": "A list of tables to manually drop, not yet implemented" + "comment": "A list of tables to manually drop, not yet implemented -- actually are we going to need this? DBAL seems to be able to figure it out..." } ] } \ No newline at end of file diff --git a/app/resources/locales/en_US/controller.po b/app/resources/locales/en_US/controller.po index 8221d2325..80b8d8029 100644 --- a/app/resources/locales/en_US/controller.po +++ b/app/resources/locales/en_US/controller.po @@ -51,6 +51,9 @@ msgstr "{0,plural,=1{Email Address} other{Email Addresses}}" msgid "ExternalIdentities" msgstr "{0,plural,=1{External Identity} other{External Identities}}" +msgid "ExternalIdentityRoles" +msgstr "{0,plural,=1{External Identity Role} other{External Identity Roles}}" + msgid "HistoryRecords" msgstr "{0,plural,=1{History Record} other{History Records}}" diff --git a/app/resources/locales/en_US/error.po b/app/resources/locales/en_US/error.po index 20eb24700..647b31f9e 100644 --- a/app/resources/locales/en_US/error.po +++ b/app/resources/locales/en_US/error.po @@ -120,6 +120,9 @@ msgstr "At least one name is required" msgid "Names.primary_name" msgstr "Primary Name not found" +msgid "Names.primary_name.del" +msgstr "Primary Name cannot be deleted" + msgid "notfound" msgstr "{0} not found" diff --git a/app/src/Command/TransmogrifyCommand.php b/app/src/Command/TransmogrifyCommand.php index 7fe83a509..bd07edc61 100644 --- a/app/src/Command/TransmogrifyCommand.php +++ b/app/src/Command/TransmogrifyCommand.php @@ -69,12 +69,13 @@ class TransmogrifyCommand extends Command { 'displayField' => 'co_id', 'addChangelog' => true, 'booleans' => [], - 'post' => 'insertDefaultSettings', + 'postTable' => 'insertDefaultSettings', 'cache' => [ 'co_id' ], 'fieldMap' => [ 'permitted_fields_name' => 'name_permitted_fields', 'required_fields_addr' => 'address_required_fields', 'required_fields_name' => 'name_required_fields', + 'telephone_number_permitted_fields' => '&populate_co_settings_phone', // XXX CFM-80 these fields are not yet migrated // be sure to add appropriate fields to 'booleans' 'enable_nsf_demo' => null, // CFM-123 @@ -124,6 +125,8 @@ class TransmogrifyCommand extends Command { 'person_roles' => [ 'source' => 'cm_co_person_roles', 'displayField' => 'id', + // We don't currently need status specifically, just that the role exists + 'cache' => [ 'status' ], 'fieldMap' => [ 'co_person_id' => 'person_id', // Rename the changelog key @@ -145,11 +148,19 @@ class TransmogrifyCommand extends Command { 'fieldMap' => [ 'co_id' => null, 'person_id' => '&map_org_identity_co_person_id', - 'o' => 'organization', - 'ou' => 'department', // Rename the changelog key - 'org_identity_id' => 'external_identity_id' + 'org_identity_id' => 'external_identity_id', + // These fields are migrated to external_identity_roles by split_external_identity() + 'title' => null, + 'o' => null, + 'ou' => null, + 'affiliation' => null, + 'manager_identifier' => null, + 'sponsor_identifier' => null, + 'valid_from' => null, + 'valid_through' => null ], + 'postRow' => 'split_external_identity', 'cache' => [ 'person_id' ] ], 'names' => [ @@ -173,7 +184,8 @@ class TransmogrifyCommand extends Command { // XXX temporary until tables are migrated 'co_department_id' => null, 'organization_id' => null - ] + ], + 'postTable' => 'processExtendedAttributes' ], 'addresses' => [ 'source' => 'cm_addresses', @@ -379,6 +391,14 @@ public function execute(Arguments $args, ConsoleIo $io) { $io->out("===" . $t . "==="); + // Run any pre processing functions for the table. + + if(!empty($this->tables[$t]['preTable'])) { + $p = $this->tables[$t]['preTable']; + + $this->$p(); + } + $count = $this->inconn->fetchOne("SELECT COUNT(*) FROM " . $this->tables[$t]['source']); $io->out("= Processing " . $count . " records"); @@ -394,6 +414,9 @@ public function execute(Arguments $args, ConsoleIo $io) { } try { + // Make a copy of the original data for any post processing followups + $origRow = $row; + // Do this before fixBooleans since we'll insert some $this->fixChangelog($t, $row, isset($this->tables[$t]['addChangelog']) && $this->tables[$t]['addChangelog']); @@ -404,6 +427,14 @@ public function execute(Arguments $args, ConsoleIo $io) { $this->outconn->insert($t, $row); $this->cacheResults($t, $row); + + // Run any post processing functions for the row. + + if(!empty($this->tables[$t]['postRow'])) { + $p = $this->tables[$t]['postRow']; + + $this->$p($origRow, $row); + } } catch(ForeignKeyConstraintViolationException $e) { // A foreign key associated with this record did not load, so we can't @@ -419,6 +450,9 @@ public function execute(Arguments $args, ConsoleIo $io) { $io->err("WARNING: Skipping record " . $row['id'] . ": " . $e->getMessage()); } + catch(\Exception $e) { + $io->err("ERROR: Record " . $row['id'] . ": " . $e->getMessage()); + } $tally++; $io->out(floor(($tally * 100)/$count) . "% done"); @@ -437,8 +471,8 @@ public function execute(Arguments $args, ConsoleIo $io) { // Run any post processing functions for the table. - if(!empty($this->tables[$t]['post'])) { - $p = $this->tables[$t]['post']; + if(!empty($this->tables[$t]['postTable'])) { + $p = $this->tables[$t]['postTable']; $this->$p(); } @@ -504,7 +538,7 @@ protected function fixBooleans(string $table, array &$row) { } /** - * Populate empty Changelog data from legacy records. + * Populate empty Changelog data from legacy records, and handle table renames. * * @since COmanage Registry v5.0.0 * @param string $table Table Name @@ -526,6 +560,20 @@ protected function fixChangelog(string $table, array &$row, bool $force=false) { } // The parent FK should remain NULL since this is the original record. + /* + // If the table was renamed, we need to rename the changelog key as well. + // NOTE: We don't actually do this here because it creates issues with the + // order of field processing. Instead, each key must be renamed + // manually in the fieldMap. + // eg: cm_org_identities -> org_identity_id + $oldfk = Inflector::singularize(substr($this->tables[$table]['source'], 3)) . "_id"; + // eg: external_identities -> external_identity_id + $newfk = Inflector::singularize($table) . "_id"; + + if($oldfk != $newfk && array_key_exists($oldfk, $row)) { + $row[$newfk] = $row[$oldfk]; + unset($row[$oldfk]); + }*/ } /** @@ -652,7 +700,7 @@ protected function map_extended_type(array $row) { // For everything else, we need to pluralize the model name $bits = explode('.', $row['attribute'], 2); - return \Cake\Utility\Inflector::pluralize($bits[0]) . "." . $bits[1]; + return Inflector::pluralize($bits[0]) . "." . $bits[1]; } /** @@ -799,4 +847,120 @@ protected function map_type(array $row, string $type, $coId, string $attr="type" protected function map_url_type(array $row) { return $this->map_type($row, 'Urls.type', $this->findCoId($row)); } + + /** + * Set a default value for CO Settings Permitted Telephone Number Fields. + * + * @since COmanage Registry v5.0.0 + * @param array $row Row of table data + * @return string Default value + */ + + protected function populate_co_settings_phone(array $row) { + return \App\Lib\Enum\PermittedTelephoneNumberFieldsEnum::CANE; + } + + /** + * Process Extended Attributes by converting them to Ad Hoc Attributes. + * + * @since COmanage Registry v5.0.0 + */ + + protected function processExtendedAttributes() { + // This is intended to run AFTER AdHocAttributes so that we don't stomp on + // the row identifiers. + + // First, pull the old Extended Attribute configuration. + $extendedAttrs = []; + + $insql = "SELECT * FROM cm_co_extended_attributes ORDER BY id ASC"; + $stmt = $this->inconn->query($insql); + + while($row = $stmt->fetch()) { + $extendedAttrs[ $row['co_id'] ][] = $row['name']; + } + + if(empty($extendedAttrs)) { + // No need to do anything further if no attributes are configured + return; + } + + foreach(array_keys($extendedAttrs) as $coId) { + $insql = "SELECT * FROM cm_co" . $coId . "_person_extended_attributes"; + $stmt = $this->inconn->query($insql); + + while($eaRow = $stmt->fetch()) { + // If we didn't transmogrify the parent row for some reason then trying + // to insert the ad_hoc_attributes will throw an error. + if(!empty($this->cache['person_roles']['id'][ $eaRow['co_person_role_id'] ])) { + foreach($extendedAttrs[$coId] as $ea) { + $adhocRow = [ + 'person_role_id' => $eaRow['co_person_role_id'], + 'tag' => $ea, + 'value' => $eaRow[$ea], + 'created' => $eaRow['created'], + 'modified' => $eaRow['modified'] + ]; + + // Extended Attributes were not changelog enabled + $this->fixChangelog('ad_hoc_attributes', $adhocRow, true); + $this->fixBooleans('ad_hoc_attributes', $adhocRow); + + $this->outconn->insert('ad_hoc_attributes', $adhocRow); + } + } + } + } + } + + /** + * Split an External Identity into an External Identity Role. + * + * @since COmanage Registry v5.0.0 + * @param array $origRow Row of table data (original data) + * @param array $row Row of table data (post fixes) + */ + + protected function split_external_identity(array $origRow, array $row) { + $roleRow = []; + + // We could set the row ID to be the same as the original parent, but then + // we'd have to reset the sequence after the table is finished migrating. + + foreach([ + // Parent Key + 'id' => 'external_identity_id', + 'o' => 'organization', + 'ou' => 'department', + 'manager_identifier' => 'manager_identifier', + 'sponsor_identifier' => 'sponsor_identifier', + 'status' => 'status', + 'title' => 'title', + 'valid_from' => 'valid_from', + 'valid_through' => 'valid_through', + // Fix up changelog + 'org_identity_id' => 'external_identity_role_id', + 'revision' => 'revision', + 'deleted' => 'deleted', + 'actor_identifier' => 'actor_identifier', + 'created' => 'created', + 'modified' => 'modified' + ] as $oldKey => $newKey) { + $roleRow[$newKey] = $origRow[$oldKey]; + } + + // Affiliation requires special handling. We need to use the post-fixed $row + // because map_affiliation_type calls findCoId which uses the foreign key to + // lookup the CO ID in the cache, however by the time we've been called + // affiliation has been null'd out (since we're moving it to the role row). + // So shove it back in before calling map_affiliation_type. + $row['affiliation'] = $origRow['affiliation']; + $roleRow['affiliation_type_id'] = $this->map_affiliation_type($row); + + // Fix up changelog + $roleRow['external_identity_role_id'] = $origRow['org_identity_id']; + unset($roleRow['org_identity_id']); + + $this->outconn->insert('external_identity_roles', $roleRow); + } } diff --git a/app/src/Controller/AppController.php b/app/src/Controller/AppController.php index 7c10bc715..94ef18a9b 100644 --- a/app/src/Controller/AppController.php +++ b/app/src/Controller/AppController.php @@ -188,6 +188,9 @@ public function calculatePermissions(?int $id): array { // Is this record read only? $readOnly = false; + // Can this record be deleted? + $canDelete = true; + // Pull the table permissions $permissions = $table->getPermissions(); @@ -207,12 +210,18 @@ public function calculatePermissions(?int $id): array { } } + if(method_exists($obj, "canDelete")) { + $canDelete = $obj->canDelete(); + } + // Permissions for actions that operate over individual entities foreach($permissions['entity'] as $action => $roles) { $ok = false; - if(!$readOnly || in_array($action, $readOnlyActions)) { + if(($action != 'delete' || $canDelete) + && + !$readOnly || in_array($action, $readOnlyActions)) { if(is_array($roles)) { foreach($roles as $role) { // eg: $role = "platformAdmin", which corresponds to the variables set, above @@ -328,6 +337,7 @@ protected function getPrimaryLink(bool $lookup=false) { $this->cur_pl = $this->$modelsName->findPrimaryLink($param); // Break the loop here since we also have the link attribute, // which might not be $potentialPrimaryLink + $this->set('vv_primary_link', $this->cur_pl->attr); break; } } @@ -370,6 +380,7 @@ protected function getPrimaryLink(bool $lookup=false) { $this->cur_pl = $this->$modelsName->findPrimaryLink($param); // Break the loop here since we also have the link attribute, // which might not be $potentialPrimaryLink + $this->set('vv_primary_link', $this->cur_pl->attr); break; } } @@ -383,6 +394,7 @@ protected function getPrimaryLink(bool $lookup=false) { $this->cur_pl = $this->$modelsName->findPrimaryLink($param); // Break the loop here since we also have the link attribute, // which might not be $potentialPrimaryLink + $this->set('vv_primary_link', $this->cur_pl->attr); break; } } diff --git a/app/src/Controller/ExternalIdentitiesController.php b/app/src/Controller/ExternalIdentitiesController.php new file mode 100644 index 000000000..e5cbce210 --- /dev/null +++ b/app/src/Controller/ExternalIdentitiesController.php @@ -0,0 +1,48 @@ + [ + 'PrimaryName.family' => 'asc' + ], + 'sortableFields' => [ + 'PrimaryName.given', + 'PrimaryName.family' + ] + ]; +} \ No newline at end of file diff --git a/app/src/Controller/ExternalIdentityRolesController.php b/app/src/Controller/ExternalIdentityRolesController.php new file mode 100644 index 000000000..d0db5af81 --- /dev/null +++ b/app/src/Controller/ExternalIdentityRolesController.php @@ -0,0 +1,45 @@ + [ + 'ExternalIdentityRoles.ordr' => 'asc', + 'ExternalIdentityRoles.title' => 'asc' + ] + ]; +} \ No newline at end of file diff --git a/app/src/Controller/MVEAController.php b/app/src/Controller/MVEAController.php index a391fa73f..c12b60721 100644 --- a/app/src/Controller/MVEAController.php +++ b/app/src/Controller/MVEAController.php @@ -56,7 +56,33 @@ public function beforeRender(\Cake\Event\EventInterface $event) { if(!empty($link->value)) { $this->set('vv_primary_link_id', $link->value); + $Names = TableRegistry::get('Names'); + switch($link->attr) { + case 'external_identity_role_id': + $ExternalIdentityRoles = TableRegistry::get('ExternalIdentityRoles'); + $roleEntity = $ExternalIdentityRoles->findById((int)$link->value)->firstOrFail(); + + // Note this is a string, but vv_person_name is an entity + $this->set('vv_ei_role', $ExternalIdentityRoles->generateDisplayField($roleEntity)); + $this->set('vv_ei_role_id', $link->value); + // fall through + case 'external_identity_id': + $ExternalIdentity = TableRegistry::get('ExternalIdentities'); + + // What's the Person ID for the ExternalIdentity? + $eiId = isset($roleEntity) ? $roleEntity->external_identity_id : $link->value; + + $externalIdentity = $ExternalIdentity->findById($eiId)->firstOrFail(); + + // What's the primary name for the Extarnal Identity? + $this->set('vv_ei_name', $Names->primaryName($externalIdentity->id, 'external_identity')); + $this->set('vv_ei_id', $externalIdentity->id); + + // What's the primary name of the Person? + $this->set('vv_person_name', $Names->primaryName($externalIdentity->person_id)); + $this->set('vv_person_id', $externalIdentity->person_id); + break; case 'person_role_id': $PersonRoles = TableRegistry::get('PersonRoles'); $roleEntity = $PersonRoles->findById((int)$link->value)->firstOrFail(); @@ -65,12 +91,10 @@ public function beforeRender(\Cake\Event\EventInterface $event) { $this->set('vv_person_role_id', $link->value); // Also set a name - $Names = TableRegistry::get('Names'); $this->set('vv_person_name', $Names->primaryName($roleEntity->person_id)); $this->set('vv_person_id', $roleEntity->person_id); break; case 'person_id': - $Names = TableRegistry::get('Names'); $this->set('vv_person_name', $Names->primaryName((int)$link->value)); $this->set('vv_person_id', $link->value); break; diff --git a/app/src/Controller/PeopleController.php b/app/src/Controller/PeopleController.php index 540178da5..74232cbe7 100644 --- a/app/src/Controller/PeopleController.php +++ b/app/src/Controller/PeopleController.php @@ -75,33 +75,4 @@ public function beforeRender(\Cake\Event\EventInterface $event) { return parent::beforeRender($event); } - - /** - * Render the Person Canvas. - * - * @since COmanage Registry v5.0.0 - * @param string $id CO Person ID - */ - - public function canvas(string $id) { - // use StandardController::edit to render (and not conflict with edit(), below) - - parent::edit($id); - } - - /** - * Stub function to redirect to canvas. - * - * @since COmanage Registry v5.0.0 - * @param string $id CO Person ID - */ - - public function edit(string $id) { - // Redirect to /canvas - - return $this->redirect([ - 'action' => 'canvas', - $id - ]); - } } \ No newline at end of file diff --git a/app/src/Controller/StandardController.php b/app/src/Controller/StandardController.php index 04babb9c3..da6309a85 100644 --- a/app/src/Controller/StandardController.php +++ b/app/src/Controller/StandardController.php @@ -268,18 +268,23 @@ public function edit(string $id) { $opts['associated'] = $table->getPatchAssociated(); }*/ + // $obj will have whatever editContains also pulled, but we don't want + // to save all that stuff by default, so we'll pull a new copy of the + // object without the associated data. + $saveObj = $table->findById($id)->firstOrFail(); + // Attempt the update the record - $table->patchEntity($obj, $this->request->getData(), $opts); + $table->patchEntity($saveObj, $this->request->getData(), $opts); // This throws \Cake\ORM\Exception\RolledbackTransactionException if aborted // in afterSave - if($table->save($obj)) { + if($table->save($saveObj)) { $this->Flash->success(__d('result', 'saved')); return $this->generateRedirect((int)$id); } - $errors = $obj->getErrors(); + $errors = $saveObj->getErrors(); if(!empty($errors)) { $this->Flash->error(__d('error', 'fields', [ implode(',', diff --git a/app/src/Lib/Events/RuleBuilderEventListener.php b/app/src/Lib/Events/RuleBuilderEventListener.php index 1fb7523ff..edabfba36 100644 --- a/app/src/Lib/Events/RuleBuilderEventListener.php +++ b/app/src/Lib/Events/RuleBuilderEventListener.php @@ -142,7 +142,7 @@ public function ruleFreezeCO(EntityInterface $entity, array $options) { } /** - * Application Rule to require foreign keys to be within the same CO as the. + * Application Rule to require foreign keys to be within the same CO as the * entity being saved. This is more of a Security Rule than an Application * Rule, but for now we don't distinguish between the two types. * @@ -179,7 +179,7 @@ public function ruleValidateCO(EntityInterface $entity, array $options) { if(empty($assn)) { // If you're debugging this, you most likely didn't set up your // associations correctly. - throw new \LogicException("Missing association for $targetProperty in ruleValidateCO"); + throw new \LogicException("Missing association from " . $table->getAlias(). " to $targetProperty in ruleValidateCO"); } // The table holding the foreign key we are validating, eg Type diff --git a/app/src/Lib/Traits/HistoryTrait.php b/app/src/Lib/Traits/HistoryTrait.php index 893dd1c1a..063a68ded 100644 --- a/app/src/Lib/Traits/HistoryTrait.php +++ b/app/src/Lib/Traits/HistoryTrait.php @@ -145,7 +145,8 @@ public function recordHistory($entity, ?string $action=null, ?string $comment=nu $lcomment = __d('result', $langKey, - Inflector::singularize($entity->getSource()), + //Inflector::singularize($entity->getSource()), + __d('controller', $entity->getSource(), [1]), $entity->id, $this->changesToString($entity)); } @@ -154,12 +155,16 @@ public function recordHistory($entity, ?string $action=null, ?string $comment=nu $personId = $this->lookupPersonId($entity); $personRoleId = $this->lookupPersonRoleId($entity); + $externalIdentityId = $this->lookupExternalIdentityId($entity); + $externalIdentityRoleId = $this->lookupExternalIdentityRoleId($entity); return $HistoryRecords->recordForPerson( $personId, $laction, $lcomment, - $personRoleId + $personRoleId, + $externalIdentityId, + $externalIdentityRoleId ); } } diff --git a/app/src/Lib/Traits/PrimaryLinkTrait.php b/app/src/Lib/Traits/PrimaryLinkTrait.php index 06c9aadb1..c6622c3d8 100644 --- a/app/src/Lib/Traits/PrimaryLinkTrait.php +++ b/app/src/Lib/Traits/PrimaryLinkTrait.php @@ -237,6 +237,63 @@ public function getRedirectGoal(): string { return $this->redirectGoal; } + /** + * Determine the External Identity ID associated with an entity. + * + * @since COmanage Registry v5.0.0 + * @param Entity $entity Entity + * @return ?int Person ID + */ + + public function lookupExternalIdentityId($entity): ?int { + // We need to see if 'external_identity_id' exists on the $entity, but it's + // not easy. We can't use property_exists() because Cake is dynamically + // getting. The Cake Entity API documentation says we should be able to call + // $entity->__isset(), but that doesn't actually implement the documented + // behavior. (Github issue: https://github.com/cakephp/cakephp/issues/16408) + // So we have to extract the key and then use array_key_exists() (but NOT isset()). + + $a = $entity->extract(['external_identity_id']); + + if(array_key_exists('external_identity_id', $a)) { + // We want to return here whether or not the key is set since if it's NULL + // we're not directly pointing to an External Identity. We can't use + // property_exists because Cake is dynamically getting. + + return $entity->external_identity_id; + } elseif($entity->getSource() == 'ExternalIdentities') { + return $entity->id; + } else { + $linkEntity = $this->findPrimaryLinkEntity($entity); + + if(!empty($linkEntity->external_identity_id)) { + return $linkEntity->external_identity_id; + } + } + + return null; + } + + /** + * Determine the Person Role ID associated with an entity. + * + * @since COmanage Registry v5.0.0 + * @param Entity $entity Entity + * @return int Person Role ID + */ + + public function lookupExternalIdentityRoleId($entity): ?int { + $a = $entity->extract(['external_identity_role_id']); + + if(array_key_exists('external_identity_role_id', $a)) { + return $entity->external_identity_role_id; + } elseif($entity->getSource() == 'ExternalIdentityRoles') { + return $entity->id; + } + + return null; + } + /** * Determine the Person ID associated with an entity. * @@ -246,7 +303,9 @@ public function getRedirectGoal(): string { */ public function lookupPersonId($entity): ?int { - if(!empty($entity->person_id)) { + $a = $entity->extract(['person_id']); + + if(array_key_exists('person_id', $a)) { return $entity->person_id; } elseif($entity->getSource() == 'People') { return $entity->id; @@ -255,6 +314,13 @@ public function lookupPersonId($entity): ?int { if(!empty($linkEntity->person_id)) { return $linkEntity->person_id; + } else { + // Our parent link does not directly point to Person, so try recursing + // on our parent table + + $LinkTable = TableRegistry::getTableLocator()->get($linkEntity->getSource()); + + return $LinkTable->lookupPersonId($linkEntity); } } @@ -270,7 +336,9 @@ public function lookupPersonId($entity): ?int { */ public function lookupPersonRoleId($entity): ?int { - if(!empty($entity->person_role_id)) { + $a = $entity->extract(['person_role_id']); + + if(array_key_exists('person_role_id', $a)) { return $entity->person_role_id; } elseif($entity->getSource() == 'PersonRoles') { return $entity->id; diff --git a/app/src/Model/Entity/ExternalIdentityRole.php b/app/src/Model/Entity/ExternalIdentityRole.php new file mode 100644 index 000000000..aeafe661e --- /dev/null +++ b/app/src/Model/Entity/ExternalIdentityRole.php @@ -0,0 +1,42 @@ + true, + 'id' => false, + 'slug' => false, + ]; +} \ No newline at end of file diff --git a/app/src/Model/Entity/Name.php b/app/src/Model/Entity/Name.php index 5a96985e9..e60b7f1bc 100644 --- a/app/src/Model/Entity/Name.php +++ b/app/src/Model/Entity/Name.php @@ -99,6 +99,17 @@ protected function _getFullName($showHonorific = false) { return $cn; } + /** + * Determine if this entity record can be deleted. + * + * @since COmanage Registry v5.0.0 + * @return bool True if the record can be deleted, false otherwise + */ + + public function canDelete(): bool { + return $this->notPrimary(); + } + /** * Determine if this is not a Primary Name. * diff --git a/app/src/Model/Entity/TelephoneNumber.php b/app/src/Model/Entity/TelephoneNumber.php index be254563a..3ee0ad622 100644 --- a/app/src/Model/Entity/TelephoneNumber.php +++ b/app/src/Model/Entity/TelephoneNumber.php @@ -52,17 +52,20 @@ protected function _getFormattedNumber() { // Start with number since it's always required, then prepend and/or append $n = $this->number; + // Prepend the area code, if set + if(!empty($this->area_code)) { + $n = $this->area_code . " " . $n; + } + + // Prepend the country code if set if(!empty($this->country_code)) { // We'll only output + style if a country code was provided $n = "+" . $this->country_code . " " . $n; } - if(!empty($this->area_code)) { - $n = $this->area_code . " " . $n; - } - + // Append the extension, if set if(!empty($this->extension)) { - $n .= " " . __d('field', 'number.ext') . $this->extension; + $n .= " " . __d('field', 'TelephoneNumbers.number.ext') . $this->extension; } return $n; diff --git a/app/src/Model/Table/AdHocAttributesTable.php b/app/src/Model/Table/AdHocAttributesTable.php index d9542a696..0a3ef2dc6 100644 --- a/app/src/Model/Table/AdHocAttributesTable.php +++ b/app/src/Model/Table/AdHocAttributesTable.php @@ -60,11 +60,11 @@ public function initialize(array $config): void { $this->belongsTo('People'); $this->belongsTo('PersonRoles'); $this->belongsTo('ExternalIdentities'); + $this->belongsTo('ExternalIdentityRoles'); $this->setDisplayField('tag'); -// XXX note primary link is external_identity_id when set... - $this->setPrimaryLink(['person_id', 'person_role_id']); + $this->setPrimaryLink(['external_identity_id', 'external_identity_role_id', 'person_id', 'person_role_id']); $this->setRequiresCO(true); $this->setPermissions([ diff --git a/app/src/Model/Table/AddressesTable.php b/app/src/Model/Table/AddressesTable.php index f8b67c503..d39bff85b 100644 --- a/app/src/Model/Table/AddressesTable.php +++ b/app/src/Model/Table/AddressesTable.php @@ -75,12 +75,12 @@ public function initialize(array $config): void { $this->belongsTo('People'); $this->belongsTo('PersonRoles'); $this->belongsTo('ExternalIdentities'); + $this->belongsTo('ExternalIdentityRoles'); $this->belongsTo('Types'); $this->setDisplayField('street'); -// XXX note primary link is external_identity_id when set... - $this->setPrimaryLink(['person_id', 'person_role_id']); + $this->setPrimaryLink(['external_identity_id', 'external_identity_role_id', 'person_id', 'person_role_id']); $this->setRequiresCO(true); $this->setAcceptsCoId(true); diff --git a/app/src/Model/Table/EmailAddressesTable.php b/app/src/Model/Table/EmailAddressesTable.php index 9da2c1563..f1cccae3a 100644 --- a/app/src/Model/Table/EmailAddressesTable.php +++ b/app/src/Model/Table/EmailAddressesTable.php @@ -79,8 +79,7 @@ public function initialize(array $config): void { $this->setDisplayField('mail'); -// XXX note primary link is external_identity_id when set... - $this->setPrimaryLink('person_id'); + $this->setPrimaryLink(['external_identity_id', 'person_id']); $this->setAllowLookupPrimaryLink(['primary']); $this->setRequiresCO(true); diff --git a/app/src/Model/Table/ExternalIdentitiesTable.php b/app/src/Model/Table/ExternalIdentitiesTable.php new file mode 100644 index 000000000..4111abe6b --- /dev/null +++ b/app/src/Model/Table/ExternalIdentitiesTable.php @@ -0,0 +1,177 @@ +addBehavior('Changelog'); + $this->addBehavior('Log'); + $this->addBehavior('Timestamp'); + + // External Identities are not configuration + $this->setIsConfigurationTable(false); + + // Define associations + $this->belongsTo('People'); + + $this->hasOne('PrimaryName') + ->setClassName('Names') + ->setConditions(['PrimaryName.primary_name' => true]); + $this->hasMany('Names') + ->setDependent(true); + $this->hasMany('Addresses') + ->setDependent(true); + $this->hasMany('AdHocAttributes') + ->setDependent(true); + $this->hasMany('EmailAddresses') + ->setDependent(true); + $this->hasMany('ExternalIdentityRoles') + ->setDependent(true); + $this->hasMany('HistoryRecords') + ->setDependent(true); + $this->hasMany('Identifiers') + ->setDependent(true); + $this->hasMany('TelephoneNumbers') + ->setDependent(true); + $this->hasMany('Urls') + ->setDependent(true); + + $this->setDisplayField('id'); + + $this->setPrimaryLink('person_id'); + $this->setRequiresCO(true); + $this->setRedirectGoal('self'); + +// XXX does some of this stuff really belong in the controller? + $this->setEditContains([ + 'PrimaryName', +/* 'Addresses', + 'AdHocAttributes', + 'EmailAddresses', + 'Identifiers', + 'Names', + 'PersonRoles', + 'TelephoneNumbers', + 'Urls'*/ + ]); + $this->setIndexContains(['PrimaryName']); + + $this->setAutoViewVars([ + 'statuses' => [ + 'type' => 'enum', +// XXX maybe this (and EIRoles) should be SuspendableStatusEnum? + 'class' => 'StatusEnum' + ] + ]); + + $this->setPermissions([ + // Actions that operate over an entity (ie: require an $id) +// See also CFM-126 +// XXX need to add couAdmin, eventually + 'entity' => [ + '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'] + ] + ]); + } + + + /** + * Table specific logic to generate a display field. + * + * @since COmanage Registry v5.0.0 + * @param ExternalIdentity $entity Entity to generate display field for + * @return string Display field + */ + + public function generateDisplayField(\App\Model\Entity\ExternalIdentity $entity): string { + if(empty($entity->primary_name)) { + throw new \InvalidArgumentException(__d('error', 'Names.primary_name')); + } + + return $entity->primary_name->full_name; + } + + /** + * Set validation rules. + * + * @since COmanage Registry v5.0.0 + * @param Validator $validator Validator + * @return Validator Validator + */ + + public function validationDefault(Validator $validator): Validator { + $schema = $this->getSchema(); + + $this->registerPrimaryKeyValidation($validator, $this->getPrimaryLinks()); + + $validator->add('status', [ +// XXX what to do about the sync status? + 'content' => ['rule' => ['inList', StatusEnum::getConstValues()]] + ]); + $validator->notEmptyString('status'); + + $validator->add('date_of_birth', [ + 'content' => ['rule' => 'date'] + ]); + $validator->allowEmptyString('date_of_birth'); + + return $validator; + } +} \ No newline at end of file diff --git a/app/src/Model/Table/ExternalIdentityRolesTable.php b/app/src/Model/Table/ExternalIdentityRolesTable.php new file mode 100644 index 000000000..16d200bcf --- /dev/null +++ b/app/src/Model/Table/ExternalIdentityRolesTable.php @@ -0,0 +1,192 @@ +addBehavior('Changelog'); + $this->addBehavior('Log'); + $this->addBehavior('Timestamp'); + + // External Identity Roles are not configuration + $this->setIsConfigurationTable(false); + + // Define associations + $this->belongsTo('ExternalIdentities'); + $this->belongsTo('Types') + ->setForeignKey('affiliation_type_id') + // Property is set so ruleValidateCO can find it. We don't use the + // _id suffix to match Cake's default pattern. + ->setProperty('affiliation_type'); + + $this->hasMany('Addresses') + ->setDependent(true); + $this->hasMany('AdHocAttributes') + ->setDependent(true); + $this->hasMany('TelephoneNumbers') + ->setDependent(true); + $this->hasMany('HistoryRecords') + ->setDependent(true); + + $this->setDisplayField('id'); + + $this->setPrimaryLink('external_identity_id'); + $this->setRequiresCO(true); + $this->setRedirectGoal('self'); + + $this->setEditContains([ + /* + 'Addresses', + 'AdHocAttributes', + 'TelephoneNumbers'*/ + ]); + + $this->setAutoViewVars([ + 'statuses' => [ + 'type' => 'enum', + 'class' => 'StatusEnum' + ], + 'affiliationTypes' => [ + 'type' => 'type', + 'attribute' => 'PersonRoles.affiliation' + ] + ]); + + $this->setPermissions([ + // Actions that operate over an entity (ie: require an $id) +// See also CFM-126 +// XXX need to add couAdmin, eventually + 'entity' => [ + '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'] + ] + ]); + } + + /** + * Table specific logic to generate a display field. + * + * @since COmanage Registry v5.0.0 + * @param Person $entity Entity to generate display field for + * @return string Display field + */ + + public function generateDisplayField(\App\Model\Entity\ExternalIdentityRole $entity): string { + // Try to find something renderable + + if(!empty($entity->title)) { + return $entity->title; + } + +// XXX else affiliation type if set, else organization, else department + + return (string)$entity->id; + } + + /** + * Set validation rules. + * + * @since COmanage Registry v5.0.0 + * @param Validator $validator Validator + * @return Validator Validator + */ + + public function validationDefault(Validator $validator): Validator { + $schema = $this->getSchema(); + + $this->registerPrimaryKeyValidation($validator, $this->getPrimaryLinks()); + + $validator->add('affiliation_type_id', [ + 'content' => ['rule' => 'isInteger'] + ]); + $validator->notEmptyString('affiliation_type_id'); + + $this->registerStringValidation($validator, $schema, 'title', false); + + $this->registerStringValidation($validator, $schema, 'organization', false); + + $this->registerStringValidation($validator, $schema, 'department', false); + + $this->registerStringValidation($validator, $schema, 'manager_identifier', false); + + $this->registerStringValidation($validator, $schema, 'sponsor_identifier', false); + + $validator->add('valid_from', [ + 'content' => ['rule' => 'dateTime'] + ]); + $validator->allowEmptyString('valid_from'); + + $validator->add('valid_through', [ + 'content' => ['rule' => 'dateTime'] + ]); + $validator->allowEmptyString('valid_through'); + + $validator->add('status', [ + 'content' => ['rule' => ['inList', StatusEnum::getConstValues()]] + ]); + $validator->notEmptyString('status'); + + $validator->add('ordr', [ + 'content' => ['rule' => 'isInteger'] + ]); + $validator->allowEmptyString('ordr'); + + return $validator; + } +} \ No newline at end of file diff --git a/app/src/Model/Table/HistoryRecordsTable.php b/app/src/Model/Table/HistoryRecordsTable.php index 75e32d632..fb08bef6c 100644 --- a/app/src/Model/Table/HistoryRecordsTable.php +++ b/app/src/Model/Table/HistoryRecordsTable.php @@ -66,12 +66,13 @@ public function initialize(array $config): void { $this->belongsTo('People'); $this->belongsTo('PersonRoles'); $this->belongsTo('ExternalIdentities'); + $this->belongsTo('ExternalIdentityRoles'); $this->setDisplayField('comment'); // XXX note primary link is external_identity_id when set... // or the other fields as we add them - $this->setPrimaryLink('person_id'); + $this->setPrimaryLink(['external_identity_id', 'person_id']); $this->setAllowLookupPrimaryLink(['primary']); $this->setRequiresCO(true); @@ -89,7 +90,8 @@ public function initialize(array $config): void { // force ActorPeople to use multiple queries. 'ActorPeople' => ['Names' => ['queryBuilder' => function ($q) { return $q->where(['primary_name' => true]); - }]] + }]], + 'ExternalIdentities' => ['PrimaryName'] ]); $this->setPermissions([ @@ -126,14 +128,21 @@ public function generateDisplayField(\App\Model\Entity\HistoryRecord $entity): s * Record a History Record entry for a Person. * * @since COmanage Registry v5.0.0 - * @param int $personId Person ID - * @param string $action Action - * @param string $comment Comment - * @param int $personRoleId Person Role ID - * @return int History Record ID + * @param int $personId Person ID + * @param string $action Action + * @param string $comment Comment + * @param int $personRoleId Person Role ID + * @param int $externalIdentityId External Identity ID + * @param int $externalIdentityRoleId External Identity Role ID + * @return int History Record ID */ - public function recordForPerson(int $personId, string $action, string $comment, ?int $personRoleId=null): int { + public function recordForPerson(int $personId, + string $action, + string $comment, + ?int $personRoleId=null, + ?int $externalIdentityId=null, + ?int $externalIdentityRoleId=null): int { $record = [ 'person_id' => $personId, 'action' => $action, @@ -144,9 +153,17 @@ public function recordForPerson(int $personId, string $action, string $comment, $record['person_role_id'] = $personRoleId; } + if($externalIdentityId) { + $record['external_identity_id'] = $externalIdentityId; + } + + if($externalIdentityRoleId) { + $record['external_identity_role_id'] = $externalIdentityRoleId; + } + $obj = $this->newEntity($record); - $this->save($obj); + $this->saveOrFail($obj); return $obj->id; } diff --git a/app/src/Model/Table/IdentifiersTable.php b/app/src/Model/Table/IdentifiersTable.php index ca5f91b72..867202eb7 100644 --- a/app/src/Model/Table/IdentifiersTable.php +++ b/app/src/Model/Table/IdentifiersTable.php @@ -91,8 +91,7 @@ public function initialize(array $config): void { $this->setDisplayField('identifier'); -// XXX note primary link is external_identity_id when set... - $this->setPrimaryLink('person_id'); + $this->setPrimaryLink(['external_identity_id', 'person_id']); $this->setAllowLookupPrimaryLink(['primary']); $this->setRequiresCO(true); diff --git a/app/src/Model/Table/NamesTable.php b/app/src/Model/Table/NamesTable.php index 8ad142291..2f36199fb 100644 --- a/app/src/Model/Table/NamesTable.php +++ b/app/src/Model/Table/NamesTable.php @@ -82,8 +82,7 @@ public function initialize(array $config): void { $this->setDisplayField('full_name'); -// XXX note primary link is external_identity_id when set... - $this->setPrimaryLink('person_id'); + $this->setPrimaryLink(['external_identity_id', 'person_id']); $this->setAllowLookupPrimaryLink(['primary']); $this->setRequiresCO(true); $this->setAcceptsCoId(true); @@ -187,6 +186,11 @@ public function buildRules(RulesChecker $rules): RulesChecker { // but cake won't pass the error without a specific field ['errorField' => 'id']); + // AR-Name-1 The Primary Name cannot be deleted. + $rules->addDelete([$this, 'rulePrimaryNameDelete'], + 'primaryNameDelete', + ['errorField' => 'primary_name']); + return $rules; } @@ -194,13 +198,14 @@ public function buildRules(RulesChecker $rules): RulesChecker { * Obtain the primary name entity for a person. * * @since COmanage Registry v5.0.0 - * @param int $personId Person ID - * @return Name Name Entity + * @param int $id Record ID + * @param string $recordType Type of record to find primary name for, 'person' or 'external_identity' + * @return Name Name Entity */ - public function primaryName(int $personId) { + public function primaryName(int $id, string $recordType='person') { return $this->find() - ->where(['person_id' => $personId, + ->where([$recordType.'_id' => $id, 'primary_name' => true]) ->firstOrFail(); } @@ -224,6 +229,23 @@ public function ruleMinimumOneName($entity, $options) { return true; } + + /** + * Application Rule to determine if the Primary Name is trying to be deleted. + * + * @since COmanage Registry 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 rulePrimaryNameDelete($entity, $options) { + if($entity->primary_name) { + return __d('error', 'Names.primary_name.del'); + } + + return true; + } /** * Set validation rules. diff --git a/app/src/Model/Table/PeopleTable.php b/app/src/Model/Table/PeopleTable.php index e1b649050..572384117 100644 --- a/app/src/Model/Table/PeopleTable.php +++ b/app/src/Model/Table/PeopleTable.php @@ -38,6 +38,7 @@ class PeopleTable extends Table { use \App\Lib\Traits\AutoViewVarsTrait; use \App\Lib\Traits\CoLinkTrait; + use \App\Lib\Traits\HistoryTrait; use \App\Lib\Traits\PermissionsTrait; use \App\Lib\Traits\PrimaryLinkTrait; use \App\Lib\Traits\QueryModificationTrait; @@ -74,6 +75,8 @@ public function initialize(array $config): void { ->setDependent(true); $this->hasMany('EmailAddresses') ->setDependent(true); + $this->hasMany('HistoryRecords') + ->setDependent(true); $this->hasMany('Identifiers') ->setDependent(true); $this->hasMany('PersonRoles') @@ -88,7 +91,6 @@ public function initialize(array $config): void { $this->setPrimaryLink('co_id'); $this->setRequiresCO(true); - $this->setAllowLookupPrimaryLink(['canvas']); $this->setRedirectGoal('self'); // XXX does some of this stuff really belong in the controller? @@ -120,7 +122,6 @@ public function initialize(array $config): void { // Actions that operate over an entity (ie: require an $id) // See also CFM-126 'entity' => [ - 'canvas' => ['platformAdmin', 'coAdmin'], 'delete' => ['platformAdmin', 'coAdmin'], 'edit' => ['platformAdmin', 'coAdmin'], 'view' => ['platformAdmin', 'coAdmin'] diff --git a/app/src/Model/Table/PersonRolesTable.php b/app/src/Model/Table/PersonRolesTable.php index bc86418e6..7519ec709 100644 --- a/app/src/Model/Table/PersonRolesTable.php +++ b/app/src/Model/Table/PersonRolesTable.php @@ -38,6 +38,7 @@ class PersonRolesTable extends Table { use \App\Lib\Traits\AutoViewVarsTrait; use \App\Lib\Traits\CoLinkTrait; + use \App\Lib\Traits\HistoryTrait; use \App\Lib\Traits\PermissionsTrait; use \App\Lib\Traits\PrimaryLinkTrait; use \App\Lib\Traits\QueryModificationTrait; @@ -74,7 +75,8 @@ public function initialize(array $config): void { ->setForeignKey('sponsor_person_id') ->setProperty('sponsor_person'); $this->belongsTo('Types') - ->setForeignKey('affiliation_type_id'); + ->setForeignKey('affiliation_type_id') + ->setProperty('affiliation_type'); $this->hasMany('Addresses') ->setDependent(true); @@ -126,7 +128,6 @@ public function initialize(array $config): void { // See also CFM-126 // XXX need to add couAdmin, eventually 'entity' => [ - 'canvas' => ['platformAdmin', 'coAdmin'], 'delete' => ['platformAdmin', 'coAdmin'], 'edit' => ['platformAdmin', 'coAdmin'], 'view' => ['platformAdmin', 'coAdmin'] @@ -173,7 +174,7 @@ public function validationDefault(Validator $validator): Validator { $validator->add('person_id', [ 'content' => ['rule' => 'isInteger'] ]); - $validator->notEmptyString('co_id'); + $validator->notEmptyString('person_id'); $validator->add('cou_id', [ 'content' => ['rule' => 'isInteger'] @@ -217,12 +218,6 @@ public function validationDefault(Validator $validator): Validator { ]); $validator->notEmptyString('status'); - $validator->add('timezone', [ - 'content' => ['rule' => ['validateTimeZone'], - 'provider' => 'table'] - ]); - $validator->allowEmptyString('timezone'); - $validator->add('ordr', [ 'content' => ['rule' => 'isInteger'] ]); diff --git a/app/src/Model/Table/TelephoneNumbersTable.php b/app/src/Model/Table/TelephoneNumbersTable.php index ea26713e6..400e46e14 100644 --- a/app/src/Model/Table/TelephoneNumbersTable.php +++ b/app/src/Model/Table/TelephoneNumbersTable.php @@ -75,12 +75,12 @@ public function initialize(array $config): void { $this->belongsTo('People'); $this->belongsTo('PersonRoles'); $this->belongsTo('ExternalIdentities'); + $this->belongsTo('ExternalIdentityRoles'); $this->belongsTo('Types'); $this->setDisplayField('number'); -// XXX note primary link is external_identity_id when set... - $this->setPrimaryLink(['person_id', 'person_role_id']); + $this->setPrimaryLink(['external_identity_id', 'external_identity_role_id', 'person_id', 'person_role_id']); $this->setRequiresCO(true); $this->setAcceptsCoId(true); diff --git a/app/src/Model/Table/UrlsTable.php b/app/src/Model/Table/UrlsTable.php index 009310f9a..94100fd5d 100644 --- a/app/src/Model/Table/UrlsTable.php +++ b/app/src/Model/Table/UrlsTable.php @@ -74,8 +74,7 @@ public function initialize(array $config): void { $this->setDisplayField('url'); -// XXX note primary link is external_identity_id when set... - $this->setPrimaryLink(['person_id']); + $this->setPrimaryLink(['external_identity_id', 'person_id']); $this->setRequiresCO(true); $this->setAutoViewVars([ diff --git a/app/templates/ExternalIdentities/columns.inc b/app/templates/ExternalIdentities/columns.inc new file mode 100644 index 000000000..2ad6a7341 --- /dev/null +++ b/app/templates/ExternalIdentities/columns.inc @@ -0,0 +1,38 @@ + [ + 'type' => 'link', + 'model' => 'primary_name', + 'field' => 'full_name', +// XXX see comments in the controller about sorting on given vs family + 'sortable' => 'PrimaryName.family' + ] +]; diff --git a/app/templates/ExternalIdentities/fields.inc b/app/templates/ExternalIdentities/fields.inc new file mode 100644 index 000000000..961774364 --- /dev/null +++ b/app/templates/ExternalIdentities/fields.inc @@ -0,0 +1,146 @@ +Field->control('status', ['empty' => false]); + + print $this->Field->control('date_of_birth'); +} + +// XXX This is a placeholder for canvas... maybe it should become a separate page +// rather than overload fields.inc? + +print $this->Html->link( + __d('controller', 'Names', [99]), + [ 'controller' => 'names', + 'action' => 'index', + '?' => [ + 'external_identity_id' => $vv_obj->id + ] + ], + ['class' => 'linkbutton'] +); + +print $this->Html->link( + __d('controller', 'EmailAddresses', [99]), + [ 'controller' => 'email_addresses', + 'action' => 'index', + '?' => [ + 'external_identity_id' => $vv_obj->id + ] + ], + ['class' => 'linkbutton'] +); + +print $this->Html->link( + __d('controller', 'Identifiers', [99]), + [ 'controller' => 'identifiers', + 'action' => 'index', + '?' => [ + 'external_identity_id' => $vv_obj->id + ] + ], + ['class' => 'linkbutton'] +); + +print $this->Html->link( + __d('controller', 'ExternalIdentityRoles', [99]), + [ 'controller' => 'external_identity_roles', + 'action' => 'index', + '?' => [ + 'external_identity_id' => $vv_obj->id + ] + ], + ['class' => 'linkbutton'] +); + +print $this->Html->link( + __d('controller', 'HistoryRecords', [99]), + [ 'controller' => 'history_records', + 'action' => 'index', + '?' => [ + 'external_identity_id' => $vv_obj->id + ] + ], + ['class' => 'linkbutton'] +); + +print $this->Html->link( + __d('controller', 'AdHocAttributes', [99]), + [ 'controller' => 'ad_hoc_attributes', + 'action' => 'index', + '?' => [ + 'external_identity_id' => $vv_obj->id + ] + ], + ['class' => 'linkbutton'] +); +print $this->Html->link( + __d('controller', 'Addresses', [99]), + [ 'controller' => 'addresses', + 'action' => 'index', + '?' => [ + 'external_identity_id' => $vv_obj->id + ] + ], + ['class' => 'linkbutton'] +); + +print $this->Html->link( + __d('controller', 'TelephoneNumbers', [99]), + [ 'controller' => 'telephone_numbers', + 'action' => 'index', + '?' => [ + 'external_identity_id' => $vv_obj->id + ] + ], + ['class' => 'linkbutton'] +); + +print $this->Html->link( + __d('controller', 'Urls', [99]), + [ 'controller' => 'urls', + 'action' => 'index', + '?' => [ + 'person_id' => $vv_obj->id + ] + ], + ['class' => 'linkbutton'] +); + +print $this->Html->link( + __d('controller', 'ExternalIdentities', [99]), + [ 'controller' => 'external-identities', + 'action' => 'index', + '?' => [ + 'external_identity_id' => $vv_obj->id + ] + ], + ['class' => 'linkbutton'] +); \ No newline at end of file diff --git a/app/templates/ExternalIdentityRoles/columns.inc b/app/templates/ExternalIdentityRoles/columns.inc new file mode 100644 index 000000000..79ebb1dba --- /dev/null +++ b/app/templates/ExternalIdentityRoles/columns.inc @@ -0,0 +1,45 @@ + [ + 'type' => 'link', + 'sortable' => true + ], + 'affiliation_type_id' => [ + 'type' => 'fk', + 'label' => __d('field', 'affiliation'), + 'sortable' => true + ], + 'status' => [ + 'type' => 'enum', + 'class' => 'StatusEnum', + 'sortable' => true + ] +]; diff --git a/app/templates/ExternalIdentityRoles/fields.inc b/app/templates/ExternalIdentityRoles/fields.inc new file mode 100644 index 000000000..1bd58b465 --- /dev/null +++ b/app/templates/ExternalIdentityRoles/fields.inc @@ -0,0 +1,90 @@ +Field->control('affiliation_type_id', [], __d('field', 'affiliation')); + + print $this->Field->control('status', ['empty' => false]); + + print $this->Field->control('ordr'); + + print $this->Field->control('title'); + + print $this->Field->control('organization'); + + print $this->Field->control('department'); + +// XXX need to clarify this is an _identifier_ not an actual Person FK + print $this->Field->control('sponsor_identifier', [], __d('field', 'sponsor')); + + print $this->Field->control('manager_identifier', [], __d('field', 'manager')); + +// XXX these need to render date pickers +// - we specifically have code in FieldHelper that checks for these two, but not date_of_birth +// - can FieldHelper introspect the date type rather than require a hard coded list of fields? +// though note valid_from/through uses special logic for 00:00:00 and 23:59:59 + print $this->Field->control('valid_from'); + + print $this->Field->control('valid_through'); +} + +// XXX This is a placeholder for canvas... maybe it should become a separate page +// rather than overload fields.inc? + +print $this->Html->link( + __d('controller', 'AdHocAttributes', [99]), + [ 'controller' => 'ad_hoc_attributes', + 'action' => 'index', + '?' => [ + 'external_identity_role_id' => $vv_obj->id + ] + ], + ['class' => 'linkbutton'] +); + +print $this->Html->link( + __d('controller', 'Addresses', [99]), + [ 'controller' => 'addresses', + 'action' => 'index', + '?' => [ + 'external_identity_role_id' => $vv_obj->id + ] + ], + ['class' => 'linkbutton'] +); + +print $this->Html->link( + __d('controller', 'TelephoneNumbers', [99]), + [ 'controller' => 'telephone_numbers', + 'action' => 'index', + '?' => [ + 'external_identity_role_id' => $vv_obj->id + ] + ], + ['class' => 'linkbutton'] +); diff --git a/app/templates/HistoryRecords/columns.inc b/app/templates/HistoryRecords/columns.inc index e4d5610b4..b25e57c4d 100644 --- a/app/templates/HistoryRecords/columns.inc +++ b/app/templates/HistoryRecords/columns.inc @@ -37,7 +37,7 @@ $indexColumns = [ ], 'actor_person_id' => [ 'type' => 'relatedLink', - 'action' => 'canvas', + 'action' => 'edit', 'label' => __d('field', 'actor'), 'model' => 'actor_person', 'submodel' => 'primary_name', diff --git a/app/templates/HistoryRecords/fields.inc b/app/templates/HistoryRecords/fields.inc index 95ff683ef..639d2ab98 100644 --- a/app/templates/HistoryRecords/fields.inc +++ b/app/templates/HistoryRecords/fields.inc @@ -45,7 +45,7 @@ if($vv_action == 'add' || $vv_action == 'view') { $viewLink = [ 'url' => [ 'controller' => 'people', - 'action' => 'canvas', + 'action' => 'edit', $vv_obj->person->id ], ]; @@ -73,11 +73,43 @@ if($vv_action == 'add' || $vv_action == 'view') { ); } + if(!empty($vv_obj->external_identity->primary_name)) { + $viewLink = [ + 'url' => [ + 'controller' => 'external_identities', + 'action' => 'edit', + $vv_obj->external_identity->id + ], + ]; + + print $this->Field->statusControl( + 'external_identity_id', + $vv_obj->external_identity->primary_name->full_name, + $viewLink + ); + } + + if(!empty($vv_obj->external_identity_role_id)) { + $viewLink = [ + 'url' => [ + 'controller' => 'external_identity_roles', + 'action' => 'edit', + $vv_obj->external_identity_role_id + ], + ]; + + print $this->Field->statusControl( + 'external_identity_role_id', + $vv_obj->external_identity_role_id, + $viewLink + ); + } + if(!empty($vv_obj->actor_person->names)) { $viewLink = [ 'url' => [ 'controller' => 'people', - 'action' => 'canvas', + 'action' => 'edit', $vv_obj->actor_person->id ], ]; diff --git a/app/templates/People/columns.inc b/app/templates/People/columns.inc index e521be396..4b6c5bbaf 100644 --- a/app/templates/People/columns.inc +++ b/app/templates/People/columns.inc @@ -25,9 +25,6 @@ * @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) */ -// Which action to render as the main link for the row -$linkActions = ['canvas']; - $indexColumns = [ 'name' => [ 'type' => 'link', diff --git a/app/templates/People/fields.inc b/app/templates/People/fields.inc index 6bcaf1606..ccb3e0a11 100644 --- a/app/templates/People/fields.inc +++ b/app/templates/People/fields.inc @@ -45,7 +45,7 @@ if($vv_action == 'add') { // The initial name must be primary 'names.0.primary_name' => true ]; -} elseif($vv_action == 'canvas') { +} elseif($vv_action == 'edit') { // XXX This is a placeholder for canvas... maybe it should become a separate page // rather than overload fields.inc? @@ -147,11 +147,22 @@ if($vv_action == 'add') { ], ['class' => 'linkbutton'] ); + + print $this->Html->link( + __d('controller', 'ExternalIdentities', [99]), + [ 'controller' => 'external-identities', + 'action' => 'index', + '?' => [ + 'person_id' => $vv_obj->id + ] + ], + ['class' => 'linkbutton'] + ); - debug($vv_obj); + $a = $vv_obj->extract(['person_role_id', 'person_id']); } -if($vv_action == 'add' || $vv_action == 'canvas') { +if($vv_action == 'add' || $vv_action == 'edit') { print $this->Field->control('status', ['empty' => false]); print $this->Field->control('date_of_birth'); diff --git a/app/templates/PersonRoles/fields.inc b/app/templates/PersonRoles/fields.inc index f73e014c0..b163efe3c 100644 --- a/app/templates/PersonRoles/fields.inc +++ b/app/templates/PersonRoles/fields.inc @@ -51,7 +51,7 @@ if($vv_action == 'add' || $vv_action == 'edit') { if(!empty($vv_obj->$fp->names[0])) { $fname = $vv_obj->$fp->names[0]->full_name; - $flink = ['url' => ['controller' => 'people', 'action' => 'canvas', $vv_obj->$fp->id]]; + $flink = ['url' => ['controller' => 'people', 'action' => 'edit', $vv_obj->$fp->id]]; } print $this->Field->statusControl($f.'_person_id', $fname, $flink, __d('field', $f)); diff --git a/app/templates/Standard/add-edit-view.php b/app/templates/Standard/add-edit-view.php index 008d0a495..b2c4f4186 100644 --- a/app/templates/Standard/add-edit-view.php +++ b/app/templates/Standard/add-edit-view.php @@ -109,8 +109,7 @@ print $this->Field->startControlSet($this->name, $vv_action, // XXX We need a model specific mechanism to disable read-only - // (eg: canvas should be declared by People) - ($vv_action == 'add' || $vv_action == 'canvas' || $vv_action == 'edit'), + ($vv_action == 'add' || $vv_action == 'edit'), $vv_required_fields); // We allow the fields.inc file to be specified for Controllers that have more diff --git a/app/templates/Standard/index.php b/app/templates/Standard/index.php index 01fa7eec3..b51b3b200 100644 --- a/app/templates/Standard/index.php +++ b/app/templates/Standard/index.php @@ -407,7 +407,6 @@ function _column_key($modelsName, $c, $tz=null) { 'dg_bd_txt_repl_str' => '' // dialog body text replacement strings ), ); - } elseif(!empty($a['controller'])) { // We're linking into a related controller /* XXX Modify the following for links to related controllers set in $indexActions. diff --git a/app/templates/element/breadcrumbs.php b/app/templates/element/breadcrumbs.php index edee2b1c9..b937e24f8 100644 --- a/app/templates/element/breadcrumbs.php +++ b/app/templates/element/breadcrumbs.php @@ -80,7 +80,7 @@ $this->Breadcrumbs->add( $vv_person_name->full_name, ['controller' => 'people', - 'action' => 'canvas', + 'action' => 'edit', $vv_person_id] ); } @@ -99,6 +99,36 @@ $vv_person_role_id] ); } + + if(!empty($vv_ei_name)) { + $this->Breadcrumbs->add( + __d('controller', 'ExternalIdentities', [99]), + ['controller' => 'external_identities', + '?' => ['co_id' => !empty($vv_cur_co) ? $vv_cur_co->id : 1]] + ); + + $this->Breadcrumbs->add( + $vv_ei_name->full_name, + ['controller' => 'external_identities', + 'action' => 'edit', + $vv_ei_id] + ); + } + + if(!empty($vv_ei_role)) { + $this->Breadcrumbs->add( + __d('controller', 'ExternalIdentityRoles', [99]), + ['controller' => 'external_identity_roles', + '?' => ['external_identity_id' => $vv_ei_id]] + ); + + $this->Breadcrumbs->add( + $vv_ei_role, + ['controller' => 'external_identity_roles', + 'action' => 'edit', + $vv_ei_role_id] + ); + } } if($vv_action != 'index' @@ -125,7 +155,7 @@ // XXX This is initially for api_users:generate, not clear how much this does // or does not generalize. If we start adding more exceptions here, we should // flip the logic and let api_users:generate declare that it wants a link back. - if(!in_array($vv_action, ['add', 'canvas', 'edit', 'index', 'view']) + if(!in_array($vv_action, ['add', 'edit', 'index', 'view']) && !empty($vv_obj->id) && !empty($vv_obj->$vv_display_field)) { $oaction = ($vv_permissions['edit']