diff --git a/app/config/schema/schema.json b/app/config/schema/schema.json index c730bf39d..13861a45b 100644 --- a/app/config/schema/schema.json +++ b/app/config/schema/schema.json @@ -218,6 +218,24 @@ }, "mvea": [ "person", "external_identity" ], "sourced": true + }, + + "history_records": { + "comment": "XXX not all foreign keys are defined yet", + + "columns": { + "id": {}, + "action": { "type": "string", "size": 4 }, + "comment": { "type": "string", "size": 256 }, + "person_id": {}, + "external_identity_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" ] } + } } }, diff --git a/app/resources/locales/en_US/controller.po b/app/resources/locales/en_US/controller.po index ee178b23f..095979c1f 100644 --- a/app/resources/locales/en_US/controller.po +++ b/app/resources/locales/en_US/controller.po @@ -45,6 +45,9 @@ msgstr "{0,plural,=1{Email Address} other{Email Addresses}}" msgid "ExternalIdentities" msgstr "{0,plural,=1{External Identity} other{External Identities}}" +msgid "HistoryRecords" +msgstr "{0,plural,=1{History Record} other{History Records}}" + msgid "Identifiers" msgstr "{0,plural,=1{Identifier} other{Identifiers}}" diff --git a/app/resources/locales/en_US/field.po b/app/resources/locales/en_US/field.po index c0d99963d..5b6e295e1 100644 --- a/app/resources/locales/en_US/field.po +++ b/app/resources/locales/en_US/field.po @@ -29,12 +29,18 @@ msgid "action" msgstr "Action" +msgid "actor" +msgstr "Actor" + msgid "api_key" msgstr "API Key" msgid "attribute" msgstr "Attribute" +msgid "comment" +msgstr "Comment" + msgid "CoSettings.address_required_fields" msgstr "Address Required Fields" @@ -53,6 +59,9 @@ msgstr "Name Permitted Fields" msgid "CoSettings.name_required_fields" msgstr "Name Required Fields" +msgid "created" +msgstr "Created" + msgid "date_of_birth" msgstr "Date of Birth" @@ -65,6 +74,9 @@ msgstr "Display Name" msgid "edupersonaffiliation" msgstr "eduPersonAffiliation" +msgid "id" +msgstr "ID" + msgid "Types.edupersonaffiliation.desc" # XXX update link to PE wiki? msgstr "Map the extended affiliation to this eduPersonAffiliation, see eduPersonAffiliation and Extended Affiliations" diff --git a/app/resources/locales/en_US/operation.po b/app/resources/locales/en_US/operation.po index 520385f30..33312c2d9 100644 --- a/app/resources/locales/en_US/operation.po +++ b/app/resources/locales/en_US/operation.po @@ -25,7 +25,7 @@ # Operations (Commands) msgid "add.a" -msgstr "Add New {0}" +msgstr "Add a New {0}" msgid "api.key.generate" msgstr "Generate API Key" diff --git a/app/resources/locales/en_US/result.po b/app/resources/locales/en_US/result.po index 8c1507e53..90ee8c3d8 100644 --- a/app/resources/locales/en_US/result.po +++ b/app/resources/locales/en_US/result.po @@ -24,12 +24,21 @@ # Results +msgid "added.mvea" +msgstr "{0} {1} Added: {2}" + msgid "deleted" msgstr "Deleted" msgid "deleted.a" msgstr "{0} Deleted" +msgid "deleted.mvea" +msgstr "{0} {1} Deleted: {2}" + +msgid "edited.mvea" +msgstr "{0} {1} Edited: {2}" + msgid "saved" msgstr "Saved" diff --git a/app/src/Command/TransmogrifyCommand.php b/app/src/Command/TransmogrifyCommand.php index b03bf4717..2a2e7b0bf 100644 --- a/app/src/Command/TransmogrifyCommand.php +++ b/app/src/Command/TransmogrifyCommand.php @@ -175,6 +175,20 @@ class TransmogrifyCommand extends Command { 'co_department_id' => null, 'organization_id' => null ] + ], + 'history_records' => [ + 'source' => 'cm_history_records', + 'displayField' => 'id', + 'fieldMap' => [ + 'co_person_id' => 'person_id', + 'org_identity_id' => 'external_identity_id', + 'actor_co_person_id' => 'actor_person_id', +// XXX temporary until tables are migrated + 'co_person_role_id' => null, + 'co_group_id' => null, + 'co_email_list_id' => null, + 'co_service_id' => null + ] ] ]; diff --git a/app/src/Controller/ApiV2Controller.php b/app/src/Controller/ApiV2Controller.php index 621b68b76..0e87b768b 100644 --- a/app/src/Controller/ApiV2Controller.php +++ b/app/src/Controller/ApiV2Controller.php @@ -278,7 +278,7 @@ public function index() { // We automatically allow API calls to be filtered on primary link if(!empty($link->attr) && !empty($link->value)) { - $query = $query->where([$link->attr => $link->value]); + $query = $query->where([$table->getAlias().'.'.$link->attr => $link->value]); } // This magically makes REST calls paginated... can use eg direction=, diff --git a/app/src/Controller/HistoryRecordsController.php b/app/src/Controller/HistoryRecordsController.php new file mode 100644 index 000000000..4abd53c6c --- /dev/null +++ b/app/src/Controller/HistoryRecordsController.php @@ -0,0 +1,42 @@ + [ + 'HistoryRecords.id' => 'desc' + ] + ]; +} \ No newline at end of file diff --git a/app/src/Controller/StandardController.php b/app/src/Controller/StandardController.php index 42c934078..e32054eba 100644 --- a/app/src/Controller/StandardController.php +++ b/app/src/Controller/StandardController.php @@ -419,7 +419,7 @@ public function index() { if(!empty($link->attr)) { // If a link attribute is defined but no value is provided, then query // where the link attribute is NULL - $query = $table->find()->where([$link->attr => $link->value]); + $query = $table->find()->where([$table->getAlias().'.'.$link->attr => $link->value]); } else { $query = $table->find(); } @@ -523,7 +523,7 @@ protected function populateAutoViewVars(object $obj=null) { // to PrimaryLinkTrait and call it there? if($v) { - $query = $query->where([$linkFilter => $v]); + $query = $query->where([$table->getAlias().'.'.$linkFilter => $v]); } } } else { @@ -567,11 +567,10 @@ public function view($id = null) { // query modifications via traits $query = $table->findById($id); - // AssociationTrait -/* + // QueryModificationTrait if(method_exists($table, "getViewContains")) { $query = $query->contain($table->getViewContains()); - }*/ + } try { // Pull the current record @@ -581,7 +580,7 @@ public function view($id = null) { // findById throws Cake\Datasource\Exception\RecordNotFoundException $this->Flash->error($e->getMessage()); - return $this->generateRedirect(); + return $this->generateRedirect((int)$id); } $this->set('vv_obj', $obj); @@ -593,13 +592,19 @@ public function view($id = null) { // We still used this in view() to map select values $this->populateAutoViewVars($obj); - // Default view title is view object display field - $field = $table->getDisplayField(); - - if(!empty($obj->$field)) { - $this->set('vv_title', __d('operation', 'view.ai', $obj->$field, $id)); + 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), $id)); } else { - $this->set('vv_title', __d('operation', 'view.ai', __d('controller', $modelsName, [1]), $id)); + // 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, $id)); + } else { + $this->set('vv_title', __d('operation', 'view.ai', __d('controller', $modelsName, [1]), $id)); + } } // Let the view render diff --git a/app/src/Lib/Enum/ActionEnum.php b/app/src/Lib/Enum/ActionEnum.php new file mode 100644 index 000000000..069cde29d --- /dev/null +++ b/app/src/Lib/Enum/ActionEnum.php @@ -0,0 +1,40 @@ +getTableLocator()->get('Types'); + + // We want to exclude the metadata from the change string, except revision, + // which makes it easier to correlate to changelog records + $skipFields = [ + 'id', + 'created', + 'modified', + 'deleted', + 'actor_identifier' + ]; + + // Use the entity's visible field list to start from + $diffFields = array_diff($entity->getVisible(), $skipFields); + + // Remove all _id fields, except type_id + foreach($diffFields as $i => $f) { + if($f != 'type_id' && preg_match('/_id$/', $f)) { + unset($diffFields[$i]); + } + } + + // Create one string per field + $changeSet = []; + + if($entity->isNew() || $entity->deleted) { + // Generate a changeset of non-empty fields + foreach($diffFields as $field) { + $newValue = $entity->get($field); + + if(!empty($newValue)) { + if($field == 'type_id') { + $newValue = $Types->getTypeLabel((int)$newValue); + } + + $changeSet[] = $field . ": " . $newValue; + } + } + } else { + // Ask the entity what changed. This will be a list of field/value pairs, + // but only where field (1) is in $diffFields and (2) changed. + $diff = $entity->extractOriginalChanged($diffFields); + + foreach(array_keys($diff) as $field) { + $oldValue = $diff[$field]; + $newValue = $entity->get($field); + + if($field == 'type_id') { + $oldValue = $Types->getTypeLabel((int)$diff[$field]); + $newValue = $Types->getTypeLabel((int)$newValue); + } + + if(!empty($oldValue) || !empty($newValue)) { + $changeSet[] = $field . ": " . $oldValue . ">" . $newValue; + } + } + } + + // And finally concatenate the field strings together + return implode(';', $changeSet); + } + + /** + * Record history for an entity. + * + * @since COmanage Registry v5.0.0 + * @param Entity $entity Entity to record history for + * @param string $action Action string (if null, auto-calculate from $entity) + * @param string $comment History comment (if null, auto-calculate from $entity) + * @return int HistoryRecord ID + */ + + public function recordHistory($entity, ?string $action=null, ?string $comment=null): int { + $laction = $action; + $lcomment = $comment; + + if(!$laction) { + $laction = ActionEnum::MVEAEdited; + + if($entity->isNew()) { + $laction = ActionEnum::MVEAAdded; + } elseif($entity->deleted) { + // Note this is ChangelogBehavior turning a delete to an update + $laction = ActionEnum::MVEADeleted; + } + } + + if(!$lcomment) { + $langKey = 'edited.mvea'; + + if($entity->isNew()) { + $langKey = 'added.mvea'; + } elseif($entity->deleted) { + // Note this is ChangelogBehavior turning a delete to an update + $langKey = 'deleted.mvea'; + } + + $lcomment = __d('result', + $langKey, + Inflector::singularize($entity->getSource()), + $entity->id, + $this->changesToString($entity)); + } + + $HistoryRecords = $this->getTableLocator()->get('HistoryRecords'); + + return $HistoryRecords->recordForPerson( + $entity->person_id, + $laction, + $lcomment + ); + } +} diff --git a/app/src/Lib/Traits/MVETrait.php b/app/src/Lib/Traits/MVETrait.php index 79aa9edc1..ccd08e034 100644 --- a/app/src/Lib/Traits/MVETrait.php +++ b/app/src/Lib/Traits/MVETrait.php @@ -39,9 +39,9 @@ trait MVETrait { public function whereClause(): array { if(!empty($this->person_id)) { - return ['person_id' => $this->person_id]; + return [$this->getSource().'.person_id' => $this->person_id]; } elseif(!empty($this->external_identity_id)) { - return ['external_identity_id' => $entity->external_identity_id]; + return [$this->getSource().'.external_identity_id' => $entity->external_identity_id]; } else { throw new \InvalidArgumentException(__d('error', 'notfound.person')); } diff --git a/app/src/Model/Entity/HistoryRecord.php b/app/src/Model/Entity/HistoryRecord.php new file mode 100644 index 000000000..d6c2b218d --- /dev/null +++ b/app/src/Model/Entity/HistoryRecord.php @@ -0,0 +1,40 @@ + true, + 'id' => false, + 'slug' => false, + ]; +} \ No newline at end of file diff --git a/app/src/Model/Table/CoSettingsTable.php b/app/src/Model/Table/CoSettingsTable.php index 632c13667..5bf8503bb 100644 --- a/app/src/Model/Table/CoSettingsTable.php +++ b/app/src/Model/Table/CoSettingsTable.php @@ -149,7 +149,7 @@ public function initialize(array $config): void { public function addDefaults(int $coId): int { // Default values for each setting - $defaultSettings = array( + $defaultSettings = [ 'co_id' => $coId, 'address_required_fields' => RequiredAddressFieldsEnum::Street, 'name_default_type_id' => null, @@ -174,7 +174,7 @@ public function addDefaults(int $coId): int { // 'theme_stacking' => SuspendableStatusEnum::Suspended, // 'co_theme_id' => null, // 'global_search_limit' => DEF_GLOBAL_SEARCH_LIMIT - ); + ]; $obj = $this->newEntity($defaultSettings); diff --git a/app/src/Model/Table/EmailAddressesTable.php b/app/src/Model/Table/EmailAddressesTable.php index 34110f34b..fe01241cc 100644 --- a/app/src/Model/Table/EmailAddressesTable.php +++ b/app/src/Model/Table/EmailAddressesTable.php @@ -35,6 +35,7 @@ class EmailAddressesTable 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\TableMetaTrait; @@ -73,7 +74,7 @@ public function initialize(array $config): void { // Define associations $this->belongsTo('People'); - $this->belongsTo('ExternalIdentity'); + $this->belongsTo('ExternalIdentities'); $this->belongsTo('Types'); $this->setDisplayField('mail'); @@ -106,6 +107,22 @@ 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): bool { + $this->recordHistory($entity); + + return true; + } + /** * Set validation rules. * diff --git a/app/src/Model/Table/HistoryRecordsTable.php b/app/src/Model/Table/HistoryRecordsTable.php new file mode 100644 index 000000000..e5712cfa0 --- /dev/null +++ b/app/src/Model/Table/HistoryRecordsTable.php @@ -0,0 +1,195 @@ +addBehavior('Changelog'); + $this->addBehavior('Log'); + $this->addBehavior('Timestamp'); + + // History Records are not configuration + $this->setIsConfigurationTable(false); + + // Define associations + $this->belongsTo('ActorPeople') + ->setClassName('People') + ->setForeignKey('actor_person_id') + // Property is set so ruleValidateCO can find it. We don't use the + // _id suffix to match Cake's default pattern. + ->setProperty('actor_person'); + $this->belongsTo('People'); + $this->belongsTo('ExternalIdentities'); + + $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->setAllowLookupPrimaryLink(['primary']); + $this->setRequiresCO(true); + +// XXX does some of this stuff really belong in the controller? + // Cake appears to incorrectly use the ActorPeople foreign key definition + // even though the relation to PrimaryName is for People. There's probably + // a patch that needs to be made, but for now we'll just force the foreign + // key back. + $this->setEditContains(['ActorPeople' => ['PrimaryName' => ['foreignKey' => 'person_id']]]); + $this->setIndexContains(['ActorPeople' => ['PrimaryName' => ['foreignKey' => 'person_id']]]); + $this->setViewContains([ + 'People' => ['PrimaryName'], + // contain results in a join when the relation is belongsTo (or hasOne), + // and joining the same table twice makes the database unhappy, so we + // force ActorPeople to use multiple queries. + 'ActorPeople' => ['Names' => ['queryBuilder' => function ($q) { + return $q->where(['primary_name' => true]); + }]] + ]); + + $this->setPermissions([ + // Actions that operate over an entity (ie: require an $id) + 'entity' => [ + 'delete' => false, + 'edit' => false, + '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 HistoryRecord $entity Entity to generate display field for + * @return string Display field + */ + + public function generateDisplayField(\App\Model\Entity\HistoryRecord $entity): string { + // Comments may be too long to render, so we just use the model name + // (which will get appended with the record ID) + + return __d('controller', 'HistoryRecords', [1]); + } + + /** + * 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 + * @return int History Record ID + */ + + public function recordForPerson(int $personId, string $action, string $comment): int { + $record = [ + 'person_id' => $personId, + 'action' => $action, + 'comment' => $comment + ]; + + $obj = $this->newEntity($record); + + $this->save($obj); + + return $obj->id; + } + + /** + * Set validation rules. + * + * @since COmanage Registry v5.0.0 + * @param Validator $validator Validator + * @return Validator Validator + * @throws InvalidArgumentException + * @throws RecordNotFoundException + */ + + public function validationDefault(Validator $validator): Validator { + // One of Person ID or External Identity ID is required +// XXX or the other fields as we add them + $validator->add( + 'person_id', + 'content', + [ 'rule' => 'isInteger' ] + ); + $validator->notEmptyString('person_id', null, function($context) { + return empty($context['data']['external_identity_id']); + }); + + $validator->add( + 'external_identity_id', + 'content', + [ 'rule' => 'isInteger' ] + ); + $validator->notEmptyString('external_identity_id', null, function($context) { + return empty($context['data']['person_id']); + }); + + $validator->add( + 'action', + 'length', + [ 'rule' => [ 'maxLength', 4 ] ] + ); + $validator->notEmptyString('action'); + + $validator->add( + 'comment', + 'length', + [ 'rule' => [ 'maxLength', 256 ] ] + ); + $validator->notEmptyString('comment'); + + return $validator; + } +} \ No newline at end of file diff --git a/app/src/Model/Table/IdentifiersTable.php b/app/src/Model/Table/IdentifiersTable.php index b52c86ff4..e7a1ffdc8 100644 --- a/app/src/Model/Table/IdentifiersTable.php +++ b/app/src/Model/Table/IdentifiersTable.php @@ -36,6 +36,7 @@ class IdentifiersTable 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\TableMetaTrait; @@ -85,7 +86,7 @@ public function initialize(array $config): void { // Define associations $this->belongsTo('People'); - $this->belongsTo('ExternalIdentity'); + $this->belongsTo('ExternalIdentities'); $this->belongsTo('Types'); $this->setDisplayField('identifier'); @@ -122,6 +123,22 @@ 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): bool { + $this->recordHistory($entity); + + return true; + } + /** * Set validation rules. * diff --git a/app/src/Model/Table/NamesTable.php b/app/src/Model/Table/NamesTable.php index 72006cdb6..b4c8c865f 100644 --- a/app/src/Model/Table/NamesTable.php +++ b/app/src/Model/Table/NamesTable.php @@ -29,16 +29,18 @@ namespace App\Model\Table; -use Cake\ORM\Query; -use Cake\ORM\RulesChecker; -use Cake\ORM\Table; -use Cake\ORM\TableRegistry; -use Cake\Validation\Validator; +use \Cake\ORM\Query; +use \Cake\ORM\RulesChecker; +use \Cake\ORM\Table; +use \Cake\ORM\TableRegistry; +use \Cake\Validation\Validator; +use \App\Lib\Enum\ActionEnum; use \App\Lib\Enum\LanguageEnum; class NamesTable 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\TableMetaTrait; @@ -65,9 +67,9 @@ class NamesTable extends Table { */ public function initialize(array $config): void { - // Timestamp behavior handles created/modified updates $this->addBehavior('Changelog'); $this->addBehavior('Log'); + // Timestamp behavior handles created/modified updates $this->addBehavior('Timestamp'); // Names are not configuration @@ -75,7 +77,7 @@ public function initialize(array $config): void { // Define associations $this->belongsTo('People'); - $this->belongsTo('ExternalIdentity'); + $this->belongsTo('ExternalIdentities'); $this->belongsTo('Types'); $this->setDisplayField('full_name'); @@ -124,15 +126,20 @@ public function initialize(array $config): void { */ public function afterSave(\Cake\Event\EventInterface $event, \Cake\Datasource\EntityInterface $entity, \ArrayObject $options): bool { + // If we have a parent, we're creating a changelog archive, which we don't want to modify + if($entity->name_id) { + return true; + } + + $this->recordHistory($entity); + // AR-Name-1 A Person must have exactly one Primary Name at all times. // To enforce this, if the current $entity is flagged Primary Name, AND // the current entity is new or was not previously the Primary Name, we look // for any other names on the same Person or External Identity that are // flagged Primary and unset them. - if($entity->primary_name - // If we have a parent, we're creating a changelog archive, which we don't want to modify - && !$entity->name_id) { + if($entity->primary_name) { if($entity->isNew() || !$entity->getOriginal('primary_name')) { // We either have a brand new name flagged as primary, or a previously // existing name that has been updated to be primary. Unset any other primary_name. @@ -156,6 +163,8 @@ public function afterSave(\Cake\Event\EventInterface $event, \Cake\Datasource\En $this->save($obj); } } + + $this->recordHistory($entity, ActionEnum::NamePrimary, __d('result', 'Names.primary_name')); } return true; diff --git a/app/src/Model/Table/PeopleTable.php b/app/src/Model/Table/PeopleTable.php index 9e5d55900..6f65cf58d 100644 --- a/app/src/Model/Table/PeopleTable.php +++ b/app/src/Model/Table/PeopleTable.php @@ -63,9 +63,8 @@ public function initialize(array $config): void { // Define associations $this->belongsTo('Cos'); - $this->hasOne('PrimaryName', [ - 'className' => 'Names' - ]) + $this->hasOne('PrimaryName') + ->setClassName('Names') ->setConditions(['PrimaryName.primary_name' => true]); $this->hasMany('Names') ->setDependent(true); @@ -83,7 +82,12 @@ public function initialize(array $config): void { $this->setRedirectGoal('self'); // XXX does some of this stuff really belong in the controller? - $this->setEditContains(['PrimaryName']); + $this->setEditContains([ + 'PrimaryName', + 'EmailAddresses', + 'Identifiers', + 'Names' + ]); $this->setIndexContains(['PrimaryName']); $this->setAutoViewVars([ diff --git a/app/src/Model/Table/TypesTable.php b/app/src/Model/Table/TypesTable.php index c7c8acf6b..052128a55 100644 --- a/app/src/Model/Table/TypesTable.php +++ b/app/src/Model/Table/TypesTable.php @@ -220,6 +220,20 @@ public function buildRules(RulesChecker $rules): RulesChecker { return $rules; } + /** + * Obtain the type label for a given type entity. + * + * @since COmanage Registry v5.0.0 + * @param int $id Type ID + * @return string Type value (label) + */ + + public function getTypeLabel(int $id): string { + $type = $this->get($id); + + return $type->value; + } + /** * Determine if this type is in use. * diff --git a/app/src/View/Helper/FieldHelper.php b/app/src/View/Helper/FieldHelper.php index 781a27d03..946a32afa 100644 --- a/app/src/View/Helper/FieldHelper.php +++ b/app/src/View/Helper/FieldHelper.php @@ -254,32 +254,45 @@ protected function formNameDiv(string $fieldName, string $labelText=null) { } /** - * Emit a status control (a read only status with an optional link button). + * Generate a status control (a read only status with an optional link button). * * @since Registry Registry v5.0.0 * @param string $fieldName Form field * @param string $status Status text * @param array $link Link information, including 'url', 'label', 'class', 'confirm' + * @param string $labelText Label text (fieldName language key used by default) * @return string */ - public function statusControl(string $fieldName, string $status, array $link=[]) { - $linkHtml = ""; + public function statusControl(string $fieldName, string $status, array $link=[], string $labelText=null): string { + $linkHtml = $status; if($link) { // Construct HTML for the requested link -// XXX use jquery instead? - $linkHtml = " " . $this->Html->link( - $link['label'], - $link['url'], - ['class' => $link['class'], 'confirm' => $link['confirm']] - ); + if(!empty($link['label'])) { + // Create a separate link after $status + + $linkHtml .= " " . $this->Html->link( + $link['label'], + $link['url'], + $link + ); + } else { + // Make $status the link + + $linkHtml = $this->Html->link( + $status, + $link['url'], + // Just pass whatever other args are specified + $link + ); + } } return $this->startLine() - . $this->formNameDiv($fieldName) - . $status . $linkHtml + . $this->formNameDiv($fieldName, $labelText) + . $linkHtml . $this->endLine(); } diff --git a/app/templates/HistoryRecords/columns.inc b/app/templates/HistoryRecords/columns.inc new file mode 100644 index 000000000..e4d5610b4 --- /dev/null +++ b/app/templates/HistoryRecords/columns.inc @@ -0,0 +1,46 @@ + [ + 'type' => 'echo' + ], + 'created' => [ + 'type' => 'datetime' + ], + 'comment' => [ + 'type' => 'echo' + ], + 'actor_person_id' => [ + 'type' => 'relatedLink', + 'action' => 'canvas', + 'label' => __d('field', 'actor'), + 'model' => 'actor_person', + 'submodel' => 'primary_name', + 'field' => 'full_name' + ] +]; diff --git a/app/templates/HistoryRecords/fields.inc b/app/templates/HistoryRecords/fields.inc new file mode 100644 index 000000000..360a8def9 --- /dev/null +++ b/app/templates/HistoryRecords/fields.inc @@ -0,0 +1,79 @@ +Field->control('comment'); + + if($vv_action == 'add') { + // On manual add insert action +// XXX and actor person id + + $hidden = [ + 'action' => \App\Lib\Enum\ActionEnum::CommentAdded + ]; + } + + if($vv_action == 'view') { + print $this->Field->control('action'); + + if(!empty($vv_obj->person->primary_name)) { + $viewLink = [ + 'url' => [ + 'controller' => 'people', + 'action' => 'canvas', + $vv_obj->person->id + ], + ]; + + print $this->Field->statusControl( + 'person_id', + $vv_obj->person->primary_name->full_name, + $viewLink + ); + } + + if(!empty($vv_obj->actor_person->names)) { + $viewLink = [ + 'url' => [ + 'controller' => 'people', + 'action' => 'canvas', + $vv_obj->actor_person->id + ], + ]; + + print $this->Field->statusControl( + 'actor_person_id', + $vv_obj->actor_person->names[0]->full_name, + $viewLink, + __d('field', 'actor') + ); + } + + print $this->Field->control('created'); + } +} diff --git a/app/templates/Identifiers/fields.inc b/app/templates/Identifiers/fields.inc index f60acc2d5..9b95ac46f 100644 --- a/app/templates/Identifiers/fields.inc +++ b/app/templates/Identifiers/fields.inc @@ -25,7 +25,7 @@ * @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) */ -// This view does currently not support read-only +// This view does not currently support read-only if($vv_action == 'add' || $vv_action == 'edit') { print $this->Field->control('identifier'); diff --git a/app/templates/Names/fields.inc b/app/templates/Names/fields.inc index d36a5d2f0..275f26e33 100644 --- a/app/templates/Names/fields.inc +++ b/app/templates/Names/fields.inc @@ -25,7 +25,7 @@ * @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) */ -// This view does currently not support read-only +// This view does not currently support read-only if($vv_action == 'add' || $vv_action == 'edit') { // Dynamic required fields is automatically handled by FormHelper via the // validation rules, but we need to manually check permitted fields. diff --git a/app/templates/People/fields.inc b/app/templates/People/fields.inc index ba55ea479..266629634 100644 --- a/app/templates/People/fields.inc +++ b/app/templates/People/fields.inc @@ -86,5 +86,18 @@ if($vv_action == 'add') { ['class' => 'linkbutton'] ); + print $this->Html->link( + __d('controller', 'HistoryRecords', [99]), + [ 'controller' => 'history_records', + 'action' => 'index', + '?' => [ + 'person_id' => $vv_obj->id + ] + ], + ['class' => 'linkbutton'] + ); + // XXX add other MVEAs here + + debug($vv_obj); } \ No newline at end of file diff --git a/app/templates/Standard/index.php b/app/templates/Standard/index.php index a873d2ea3..86fbf5d75 100644 --- a/app/templates/Standard/index.php +++ b/app/templates/Standard/index.php @@ -33,12 +33,13 @@ declare(strict_types = 1); //use \App\Lib\Enum\StatusEnum; +use \Cake\Utility\Inflector; // $this->name = Models $modelsName = $this->name; // $tablename = models // XXX backport to match? -$tableName = \Cake\Utility\Inflector::tableize(\Cake\Utility\Inflector::singularize($this->name)); +$tableName = Inflector::tableize(Inflector::singularize($this->name)); // Do we have records for this index? This will be set to true during render if we do. // Otherwise, we'll print out a "no records" message. @@ -60,7 +61,7 @@ function _column_key($modelsName, $c, $tz=null) { if(strpos($c, "_id", strlen($c)-3)) { // Key is of the form field_id, use .ct label instead - $k = \Cake\Utility\Inflector::classify(\Cake\Utility\Inflector::pluralize(substr($c, 0, strlen($c)-3))); + $k = Inflector::classify(Inflector::pluralize(substr($c, 0, strlen($c)-3))); return __d('controller' ,$k, [1]); } @@ -198,6 +199,7 @@ function _column_key($modelsName, $c, $tz=null) { } break; case 'datetime': + // XXX dates can be rendered as eg $entity->created->format(DATE_RFC850); print $this->Time->nice($entity->$col, $vv_tz) . $suffix; break; case 'enum': @@ -211,7 +213,7 @@ function _column_key($modelsName, $c, $tz=null) { // AutoViewVar $foos is set, and if so render the lookup value instead $f = null; if(preg_match('/^(.*?)_id$/', $col, $f)) { - $avv = \Cake\Utility\Inflector::variable(\Cake\Utility\Inflector::pluralize($f[1])); + $avv = Inflector::variable(Inflector::pluralize($f[1])); if(!empty(${$avv}[$entity->$col])) { // We found the viewvar (eg: $foos), and it has a corresponding value @@ -259,6 +261,7 @@ function _column_key($modelsName, $c, $tz=null) { } break; case 'link': + case 'relatedLink': case 'echo': default: // By default our label is the column value, but it might be overridden @@ -268,15 +271,29 @@ function _column_key($modelsName, $c, $tz=null) { $m = $cfg['model']; $f = $cfg['field']; - if(!empty($entity->$m->$f)) { - $label = $entity->$m->$f . $suffix; + if(!empty($cfg['submodel'])) { + // We have a related model, eg actor_person.primary_name + $sm = $cfg['submodel']; + + if(!empty($entity->$m->$sm->$f)) { + $label = $entity->$m->$sm->$f . $suffix; + } + } else { + if(!empty($entity->$m->$f)) { + $label = $entity->$m->$f . $suffix; + } } } $linked = false; + // $linkActions can be overridden in columns.inc to apply to all + // generated links, or $cfg['action'] can be set to apply only to + // a specific field (column). + $tryActions = (!empty($cfg['action']) ? [ $cfg['action'] ] : $linkActions); + if($cfg['type'] == 'link') { - foreach($linkActions as $a) { + foreach($tryActions as $a) { // Does this user have permission for this action? if($vv_permission_set[$entity->id][$a]) { print $this->Html->link($label, ['action' => $a, $entity->id]); @@ -284,6 +301,26 @@ function _column_key($modelsName, $c, $tz=null) { break 2; } } + } elseif($cfg['type'] == 'relatedLink') { + $m = $cfg['model']; + + if(!empty($entity->$m->id)) { + // We need the controller for the related entity, however $m + // might be an alias and $entity->getSource() returns the + // aliased class name. So we use PHP's get_class instead. + $c = Inflector::tableize(substr(get_class($entity->$m), strrpos(get_class($entity->$m), '\\')+1)); + + foreach($tryActions as $a) { + // Does this user have permission for this action? +// XXX we actually need to know the permissions on the target (ie: actor person) + if(true || + $vv_permission_set[$entity->id][$a]) { + print $this->Html->link($label, ['controller' => $c, 'action' => $a, $entity->$m->id]); + $linked = true; + break 2; + } + } + } } if(!$linked) { @@ -291,7 +328,6 @@ function _column_key($modelsName, $c, $tz=null) { print $label; } break; - // XXX dates can be rendered as eg $entity->created->format(DATE_RFC850); } ?>