diff --git a/app/composer.json b/app/composer.json index d3872f6e9..c1a2744b6 100644 --- a/app/composer.json +++ b/app/composer.json @@ -48,7 +48,8 @@ "SqlConnector\\": "availableplugins/SqlConnector/src/", "SshKeyAuthenticator\\": "plugins/SshKeyAuthenticator/src/", "CoreJob\\": "plugins/CoreJob/src/", - "Transmogrify\\": "plugins/Transmogrify/src/" + "Transmogrify\\": "plugins/Transmogrify/src/", + "HistoricPetitionViewer\\": "plugins/HistoricPetitionViewer/src/" } }, "autoload-dev": { @@ -68,7 +69,8 @@ "SqlConnector\\Test\\": "availableplugins/SqlConnector/tests/", "SshKeyAuthenticator\\Test\\": "plugins/SshKeyAuthenticator/tests/", "CoreJob\\Test\\": "plugins/CoreJob/tests/", - "Transmogrify\\": "plugins/Transmogrify/src/" + "Transmogrify\\": "plugins/Transmogrify/src/", + "HistoricPetitionViewer\\": "plugins/HistoricPetitionViewer/src/" } }, "scripts": { diff --git a/app/plugins/HistoricPetitionViewer/HistoricPetitionViewerPlugin.php b/app/plugins/HistoricPetitionViewer/HistoricPetitionViewerPlugin.php new file mode 100644 index 000000000..714f1692b --- /dev/null +++ b/app/plugins/HistoricPetitionViewer/HistoricPetitionViewerPlugin.php @@ -0,0 +1,93 @@ +plugin( + 'HistoryPetitionViewer', + ['path' => '/historic-petition-viewer'], + function (RouteBuilder $builder) { + // Add custom routes here + + $builder->fallbacks(); + } + ); + parent::routes($routes); + } + + /** + * Add middleware for the plugin. + * + * @param \Cake\Http\MiddlewareQueue $middlewareQueue The middleware queue to update. + * @return \Cake\Http\MiddlewareQueue + */ + public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue + { + // Add your middlewares here + + return $middlewareQueue; + } + + /** + * Add commands for the plugin. + * + * @param \Cake\Console\CommandCollection $commands The command collection to update. + * @return \Cake\Console\CommandCollection + */ + public function console(CommandCollection $commands): CommandCollection + { + // Add your commands here + + $commands = parent::console($commands); + + return $commands; + } + + /** + * Register application container services. + * + * @param \Cake\Core\ContainerInterface $container The Container to update. + * @return void + * @link https://book.cakephp.org/4/en/development/dependency-injection.html#dependency-injection + */ + public function services(ContainerInterface $container): void + { + // Add your services here + } +} diff --git a/app/plugins/HistoricPetitionViewer/config/plugin.json b/app/plugins/HistoricPetitionViewer/config/plugin.json new file mode 100644 index 000000000..b1c36cfc9 --- /dev/null +++ b/app/plugins/HistoricPetitionViewer/config/plugin.json @@ -0,0 +1,77 @@ +{ + "name": "Historic Petition Viewer", + "version": "1.0.0", + "description": "", + "types": {}, + "schema": { + "tables": { + "historic_petition_attributes": { + "comment": "Transmogrified from cm_co_petition_attributes", + "columns": { + "id": { "type": "integer", "autoincrement": true, "primarykey": true }, + "petition_id": { "type": "integer", "foreignkey": { "table": "petitions", "column": "id" } }, + "attribute": { "type": "string", "size": 128 }, + "value": { "type": "text" }, + "created": { "type": "datetime" }, + "modified": { "type": "datetime" }, + "historic_petition_attribute_id": { "type": "integer", "foreignkey": { "table": "historic_petition_attributes", "column": "id" } }, + "revision": { "type": "integer" }, + "deleted": { "type": "boolean" }, + "actor_identifier": { "type": "string", "size": 256 } + }, + "indexes": { + "historic_petition_attributes_i1": { "columns": [ "petition_id" ] }, + "historic_petition_attributes_i2": { "columns": [ "attribute" ] }, + "historic_petition_attributes_i3": { "columns": [ "historic_petition_attribute_id" ] } + } + }, + "historic_petition_metadata": { + "comment": "Transmogrified from cm_co_petitions fields that no longer map to petitions", + "columns": { + "id": { "type": "integer", "autoincrement": true, "primarykey": true }, + "petition_id": { "type": "integer", "foreignkey": { "table": "petitions", "column": "id" } }, + + "enrollee_org_identity_id": { "type": "integer" }, + "archived_org_identity_id": { "type": "integer" }, + "enrollee_person_role_id": { "type": "integer" }, + "sponsor_person_id": { "type": "integer" }, + "approver_person_id": { "type": "integer" }, + "co_invite_id": { "type": "integer" }, + "vetting_request_id": { "type": "integer" }, + + "token": { "type": "string", "size": 256 }, + "enrollee_token": { "type": "string", "size": 48 }, + "petitioner_token": { "type": "string", "size": 48 }, + "return_url": { "type": "string", "size": 512 }, + "approver_comment": { "type": "string", "size": 256 }, + + "created": { "type": "datetime" }, + "modified": { "type": "datetime" }, + "historic_petition_metadata_id": { "type": "integer", "foreignkey": { "table": "historic_petition_metadata", "column": "id" } }, + "revision": { "type": "integer" }, + "deleted": { "type": "boolean" }, + "actor_identifier": { "type": "string", "size": 256 } + }, + "indexes": { + "historic_petition_metadata_i1": { "columns": [ "petition_id" ] }, + "historic_petition_metadata_i2": { "columns": [ "historic_petition_metadata_id" ] } + } + }, + "historic_petition_step_links": { + "comment": "Associates a Petition’s historic data to a specific Enrollment Flow Step for read-only viewing", + "columns": { + "id": { "type": "integer", "autoincrement": true, "primarykey": true }, + "petition_id": { "type": "integer", "foreignkey": { "table": "petitions", "column": "id" } }, + "enrollment_flow_step_id": { "type": "integer", "foreignkey": { "table": "enrollment_flow_steps", "column": "id" } }, + "created": { "type": "datetime" }, + "modified": { "type": "datetime" } + }, + "indexes": { + "historic_petition_step_links_i1": { "columns": [ "petition_id" ] }, + "historic_petition_step_links_i2": { "columns": [ "enrollment_flow_step_id" ] }, + "historic_petition_step_links_u1": { "columns": [ "petition_id", "enrollment_flow_step_id" ] } + } + } + } + } +} \ No newline at end of file diff --git a/app/plugins/HistoricPetitionViewer/config/routes.php b/app/plugins/HistoricPetitionViewer/config/routes.php new file mode 100644 index 000000000..8ddd64321 --- /dev/null +++ b/app/plugins/HistoricPetitionViewer/config/routes.php @@ -0,0 +1,44 @@ + '/historic-petition'], + function (RouteBuilder $routes) { + $routes->connect( + '/', + ['controller' => 'HistoricPetitionViews', + 'action' => 'index'] + ); + $routes->fallbacks(); + } +); \ No newline at end of file diff --git a/app/plugins/HistoricPetitionViewer/src/Controller/AppController.php b/app/plugins/HistoricPetitionViewer/src/Controller/AppController.php new file mode 100644 index 000000000..bcc7e6ad2 --- /dev/null +++ b/app/plugins/HistoricPetitionViewer/src/Controller/AppController.php @@ -0,0 +1,10 @@ +plugin( + 'HistoricPetitionViewer', + ['path' => '/historic-petition-viewer'], + function (RouteBuilder $builder) { + // Add custom routes here + + $builder->fallbacks(); + } + ); + parent::routes($routes); + } + + /** + * Add middleware for the plugin. + * + * @param \Cake\Http\MiddlewareQueue $middlewareQueue The middleware queue to update. + * @return \Cake\Http\MiddlewareQueue + */ + public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue + { + // Add your middlewares here + // remove this method hook if you don't need it + + return $middlewareQueue; + } + + /** + * Add commands for the plugin. + * + * @param \Cake\Console\CommandCollection $commands The command collection to update. + * @return \Cake\Console\CommandCollection + */ + public function console(CommandCollection $commands): CommandCollection + { + // Add your commands here + // remove this method hook if you don't need it + + $commands = parent::console($commands); + + return $commands; + } + + /** + * Register application container services. + * + * @param \Cake\Core\ContainerInterface $container The Container to update. + * @return void + * @link https://book.cakephp.org/5/en/development/dependency-injection.html#dependency-injection + */ + public function services(ContainerInterface $container): void + { + // Add your services here + // remove this method hook if you don't need it + } +} diff --git a/app/plugins/HistoricPetitionViewer/src/Model/Entity/HistoricPetitionAttribute.php b/app/plugins/HistoricPetitionViewer/src/Model/Entity/HistoricPetitionAttribute.php new file mode 100644 index 000000000..5d0aaf7e3 --- /dev/null +++ b/app/plugins/HistoricPetitionViewer/src/Model/Entity/HistoricPetitionAttribute.php @@ -0,0 +1,43 @@ + true, + 'id' => false, + 'slug' => false, + ]; +} \ No newline at end of file diff --git a/app/plugins/HistoricPetitionViewer/src/Model/Entity/HistoricPetitionMetadata.php b/app/plugins/HistoricPetitionViewer/src/Model/Entity/HistoricPetitionMetadata.php new file mode 100644 index 000000000..afe5849c0 --- /dev/null +++ b/app/plugins/HistoricPetitionViewer/src/Model/Entity/HistoricPetitionMetadata.php @@ -0,0 +1,43 @@ + true, + 'id' => false, + 'slug' => false, + ]; +} \ No newline at end of file diff --git a/app/plugins/HistoricPetitionViewer/src/Model/Entity/HistoricPetitionStepLink.php b/app/plugins/HistoricPetitionViewer/src/Model/Entity/HistoricPetitionStepLink.php new file mode 100644 index 000000000..0788334ec --- /dev/null +++ b/app/plugins/HistoricPetitionViewer/src/Model/Entity/HistoricPetitionStepLink.php @@ -0,0 +1,43 @@ + true, + 'id' => false, + 'slug' => false, + ]; +} \ No newline at end of file diff --git a/app/plugins/HistoricPetitionViewer/src/Model/Table/HistoricPetitionAttributesTable.php b/app/plugins/HistoricPetitionViewer/src/Model/Table/HistoricPetitionAttributesTable.php new file mode 100644 index 000000000..6b711196e --- /dev/null +++ b/app/plugins/HistoricPetitionViewer/src/Model/Table/HistoricPetitionAttributesTable.php @@ -0,0 +1,133 @@ +addBehavior('Changelog'); + $this->addBehavior('Log'); + $this->addBehavior('Timestamp'); + + $this->setTableType(TableTypeEnum::Metadata); + + // Define associations + $this->belongsTo('Petitions'); + + $this->setDisplayField('attribute'); + + $this->setPrimaryLink('petition_id'); + $this->setRequiresCO(false); +// $this->setAllowLookupPrimaryLink(['dispatch', 'display']); + + $this->setPermissions([ + // Actions that operate over an entity (ie: require an $id) + 'entity' => [ + 'delete' => false, + 'dispatch' => false, + 'display' => true, + 'edit' => false, + 'view' => ['platformAdmin', 'coAdmin'] + ], + // Actions that operate over a table (ie: do not require an $id) + 'table' => [ + 'add' => false, + 'index' => ['platformAdmin', 'coAdmin'] + ] + ]); + } + + /** + * Set validation rules. + * + * @since COmanage Registry v5.2.0 + * @param Validator $validator Validator + * @return Validator Validator + */ + + public function validationDefault(Validator $validator): Validator { + $schema = $this->getSchema(); + + // petition_id (required, integer) + $validator->add('petition_id', [ + 'content' => ['rule' => 'isInteger'] + ]); + $validator->notEmptyString('petition_id'); + + // attribute (required, string up to 128) + $validator->add('attribute', [ + 'size' => ['rule' => ['maxLength', 128]] + ]); + $validator->notEmptyString('attribute'); + + // value (text, optional) + $validator->allowEmptyString('value'); + + // historic_petition_attribute_id (self-referential changelog FK, optional) + $validator->add('historic_petition_attribute_id', [ + 'content' => ['rule' => 'isInteger'] + ]); + $validator->allowEmptyString('historic_petition_attribute_id'); + + // revision (optional integer) + $validator->add('revision', [ + 'content' => ['rule' => 'isInteger'] + ]); + $validator->allowEmptyString('revision'); + + // deleted (optional boolean) + $validator->add('deleted', [ + 'content' => ['rule' => ['boolean']] + ]); + $validator->allowEmptyString('deleted'); + + // actor_identifier (optional, string up to 256) + $validator->add('actor_identifier', [ + 'size' => ['rule' => ['maxLength', 256]] + ]); + $validator->allowEmptyString('actor_identifier'); + + // created/modified handled by Timestamp behavior; no explicit validation needed + return $validator; + } +} \ No newline at end of file diff --git a/app/plugins/HistoricPetitionViewer/src/Model/Table/HistoricPetitionMetadatasTable.php b/app/plugins/HistoricPetitionViewer/src/Model/Table/HistoricPetitionMetadatasTable.php new file mode 100644 index 000000000..0388315de --- /dev/null +++ b/app/plugins/HistoricPetitionViewer/src/Model/Table/HistoricPetitionMetadatasTable.php @@ -0,0 +1,136 @@ +addBehavior('Changelog'); + $this->addBehavior('Log'); + $this->addBehavior('Timestamp'); + + $this->setTableType(TableTypeEnum::Metadata); + + // Define associations + $this->belongsTo('Petitions'); + + $this->setDisplayField('petition_id'); + + $this->setRequiresCO(false); + $this->setAllowLookupPrimaryLink(['dispatch', 'display']); + + $this->setPermissions([ + // Actions that operate over an entity (ie: require an $id) + 'entity' => [ + 'delete' => false, + 'dispatch' => false, + 'display' => true, + 'edit' => false, + 'view' => ['platformAdmin', 'coAdmin'] + ], + // Actions that operate over a table (ie: do not require an $id) + 'table' => [ + 'add' => false, + 'index' => ['platformAdmin', 'coAdmin'] + ] + ]); + } + + /** + * Set validation rules. + * + * @since COmanage Registry v5.2.0 + * @param Validator $validator Validator + * @return Validator Validator + */ + + public function validationDefault(Validator $validator): Validator { + $schema = $this->getSchema(); + + // petition_id (required, integer) + $validator->add('petition_id', [ + 'content' => ['rule' => 'isInteger'] + ]); + $validator->notEmptyString('petition_id'); + + // Optional integer FKs/refs + foreach ([ + 'enrollee_org_identity_id', + 'archived_org_identity_id', + 'enrollee_person_role_id', + 'sponsor_person_id', + 'approver_person_id', + 'co_invite_id', + 'vetting_request_id', + 'historic_petition_metadata_id', + 'revision' + ] as $intField) { + $validator->add($intField, [ + 'content' => ['rule' => 'isInteger'] + ]); + $validator->allowEmptyString($intField); + } + + // Optional boolean + $validator->add('deleted', [ + 'content' => ['rule' => ['boolean']] + ]); + $validator->allowEmptyString('deleted'); + + // Strings with size limits (optional) + // Use ValidationTrait helpers to enforce schema-driven max length and filtering + foreach ([ + 'token', + 'enrollee_token', + 'petitioner_token', + 'return_url', + 'approver_comment', + 'actor_identifier' + ] as $strField) { + $this->registerStringValidation($validator, $schema, $strField, false); + } + + // created/modified handled by Timestamp behavior + return $validator; + } +} \ No newline at end of file diff --git a/app/plugins/HistoricPetitionViewer/src/Model/Table/HistoricPetitionStepLinksTable.php b/app/plugins/HistoricPetitionViewer/src/Model/Table/HistoricPetitionStepLinksTable.php new file mode 100644 index 000000000..1fda42bc9 --- /dev/null +++ b/app/plugins/HistoricPetitionViewer/src/Model/Table/HistoricPetitionStepLinksTable.php @@ -0,0 +1,123 @@ +addBehavior('Changelog'); + $this->addBehavior('Log'); + $this->addBehavior('Timestamp'); + + $this->setTableType(TableTypeEnum::Metadata); + + // Define associations + $this->belongsTo('Petitions'); + $this->belongsTo('EnrollmnetFlowSteps'); + + $this->setDisplayField('id'); + + $this->setPrimaryLink('enrollment_flow_step_id'); + $this->setRequiresCO(false); + $this->setAllowLookupPrimaryLink(['dispatch', 'display']); + + // All the tabs share the same configuration in the ModelTable file + $this->setTabsConfig( + [ + // Ordered list of Tabs + 'tabs' => ['EnrollmentFlowSteps', 'HistoricPetitionViewer.HistoricPetitionStepLinks'], + // What actions will include the subnavigation header + 'action' => [ + // If a model renders in a subnavigation mode in edit/view mode, it cannot + // render in index mode for the same use case/context + // XXX edit should go first. + 'EnrollmentFlowSteps' => ['view'], + 'HistoricPetitionViewer.HistoricPetitionStepLinks' => ['view'] + ] + ] + ); + + $this->setPermissions([ + // Actions that operate over an entity (ie: require an $id) + 'entity' => [ + 'delete' => false, + 'dispatch' => false, + 'display' => true, + 'edit' => false, + 'view' => ['platformAdmin', 'coAdmin'] + ], + // Actions that operate over a table (ie: do not require an $id) + 'table' => [ + 'add' => false, + 'index' => ['platformAdmin', 'coAdmin'] + ] + ]); + } + + /** + * Set validation rules. + * + * @since COmanage Registry v5.2.0 + * @param Validator $validator Validator + * @return Validator Validator + */ + + public function validationDefault(Validator $validator): Validator { + $schema = $this->getSchema(); + + // petition_id (required, integer) + $validator->add('petition_id', [ + 'content' => ['rule' => 'isInteger'] + ]); + $validator->notEmptyString('petition_id'); + + // enrollment_flow_step_id (required, integer) + $validator->add('enrollment_flow_step_id', [ + 'content' => ['rule' => 'isInteger'] + ]); + $validator->notEmptyString('enrollment_flow_step_id'); + + // created/modified handled by Timestamp behavior; no explicit validation needed + return $validator; + } +} \ No newline at end of file diff --git a/app/plugins/HistoricPetitionViewer/src/View/Cell/HistoricPetitionStepLinksCell.php b/app/plugins/HistoricPetitionViewer/src/View/Cell/HistoricPetitionStepLinksCell.php new file mode 100644 index 000000000..5810af1b4 --- /dev/null +++ b/app/plugins/HistoricPetitionViewer/src/View/Cell/HistoricPetitionStepLinksCell.php @@ -0,0 +1,105 @@ + + */ + protected array $_validCellOptions = [ + 'vv_obj', + 'vv_step', + 'viewVars', + ]; + + /** + * Initialization logic run at the end of object construction. + * + * @return void + */ + public function initialize(): void + { + } + + /** + * Default display method. + * + * @param int $petitionId + * @return void + * @since COmanage Registry v5.2.0 + */ + public function display(int $petitionId): void + { + // Fetch historic metadata rows for this petition (could be zero or more) + $historicMetadata = $this->fetchTable('HistoricPetitionViewer.HistoricPetitionMetadata') + ->find() + ->where(['petition_id' => $petitionId]) + ->all(); + + // Fetch historic attributes rows for this petition (could be zero or more) + $historicAttributes = $this->fetchTable('HistoricPetitionViewer.HistoricPetitionAttributes') + ->find() + ->where(['petition_id' => $petitionId]) + ->orderBy(['attribute' => 'ASC', 'id' => 'ASC']) + ->all(); + + $this->set('vv_historic_petition_metadata', $historicMetadata); + $this->set('vv_historic_petition_attributes', $historicAttributes); + $this->set('vv_obj', $this->vv_obj); + } +} diff --git a/app/plugins/HistoricPetitionViewer/templates/cell/HistoricPetitionStepLinks/display.php b/app/plugins/HistoricPetitionViewer/templates/cell/HistoricPetitionStepLinks/display.php new file mode 100644 index 000000000..851d577a5 --- /dev/null +++ b/app/plugins/HistoricPetitionViewer/templates/cell/HistoricPetitionStepLinks/display.php @@ -0,0 +1,104 @@ +id === null) { + echo __d('error', 'notfound', 'Historic Petition'); + return; +} + +// Normalize collections +$metaRows = $vv_historic_petition_metadata ?? []; +$attrRows = $vv_historic_petition_attributes ?? []; + +// Helper: render a KV list from an entity/array, excluding technical fields +$excludeMetaKeys = [ + 'id', 'petition_id', + 'historic_petition_metadata_id', 'revision', 'deleted', + 'actor_identifier', 'created', 'modified' +]; + +// Order token-like fields first for readability if present +$preferredMetaOrder = [ + 'approver_comment', + 'return_url', + 'token', 'petitioner_token', 'enrollee_token', + 'enrollee_org_identity_id', 'archived_org_identity_id', 'enrollee_person_role_id', + 'sponsor_person_id', 'approver_person_id', + 'co_invite_id', 'vetting_request_id', +]; + +function renderMetaKeyValue(string $label, mixed $value): void { + if ($value === null || $value === '') { + return; + } + ?> +
  • +
    +
    +
  • + +
    +

    #id) ?>

    + +
    +

    + + + + toArray() : (array)$meta; + + // Build ordered list of keys: preferred first, then any remaining + $keys = array_values(array_diff(array_keys($metaArr), $excludeMetaKeys)); + $ordered = array_values(array_unique(array_merge($preferredMetaOrder, $keys))); + ?> + + + +

    + +
    + +
    +

    + + + + +

    + +
    +
    diff --git a/app/plugins/HistoricPetitionViewer/webroot/.gitkeep b/app/plugins/HistoricPetitionViewer/webroot/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/app/plugins/Transmogrify/config/schema/tables.json b/app/plugins/Transmogrify/config/schema/tables.json index 98780b179..92e76dbb8 100644 --- a/app/plugins/Transmogrify/config/schema/tables.json +++ b/app/plugins/Transmogrify/config/schema/tables.json @@ -510,7 +510,7 @@ "vetting_request_id": null, "authenticated_identifier": "petitioner_identifier", "reference_identifier": "enrollee_identifier", - "petitioner_token": "token", + "petitioner_token": null, "enrollee_token": null, "return_url": null, "approver_comment": null, diff --git a/app/src/Application.php b/app/src/Application.php index ae0b8a6dd..2818445f6 100644 --- a/app/src/Application.php +++ b/app/src/Application.php @@ -70,11 +70,14 @@ public function bootstrap(): void $this->addPlugin($p->plugin); } - // The Transmogrify plugin is required for all applications + // The Transmogrification family plugins are required for all applications // It is a special use case. if (!\Cake\Core\Plugin::isLoaded('Transmogrify')) { $this->addPlugin('Transmogrify'); } + if (!\Cake\Core\Plugin::isLoaded('HistoricPetitionViewer')) { + $this->addPlugin('HistoricPetitionViewer'); + } } catch(\Cake\Database\Exception\DatabaseException $e) { // Most likely we are performing the initial database setup and