From 815fd9a08c8b19e52846ce829c7c81e184be876b Mon Sep 17 00:00:00 2001 From: Ioannis Igoumenos Date: Tue, 11 Feb 2025 19:57:01 +0200 Subject: [PATCH] AttributesCollector:Add attributes to Person on finalize --- .../Model/Table/AttributeCollectorsTable.php | 151 +++++++++++++++--- .../Model/Table/EnrollmentAttributesTable.php | 5 + .../templates/EnrollmentAttributes/fields.inc | 3 + app/src/Model/Table/AdHocAttributesTable.php | 39 +++++ app/src/Model/Table/AddressesTable.php | 50 +++++- app/src/Model/Table/EmailAddressesTable.php | 36 ++++- app/src/Model/Table/NamesTable.php | 70 +++++++- app/src/Model/Table/PersonRolesTable.php | 27 ++++ app/src/Model/Table/PronounsTable.php | 16 ++ app/src/Model/Table/TelephoneNumbersTable.php | 47 ++++++ app/src/Model/Table/UrlsTable.php | 15 ++ 11 files changed, 434 insertions(+), 25 deletions(-) diff --git a/app/plugins/CoreEnroller/src/Model/Table/AttributeCollectorsTable.php b/app/plugins/CoreEnroller/src/Model/Table/AttributeCollectorsTable.php index 5b55f4b2..4a3cfb79 100644 --- a/app/plugins/CoreEnroller/src/Model/Table/AttributeCollectorsTable.php +++ b/app/plugins/CoreEnroller/src/Model/Table/AttributeCollectorsTable.php @@ -31,12 +31,17 @@ use App\Lib\Enum\PetitionActionEnum; use App\Lib\Enum\StatusEnum; +use App\Model\Entity\Person; +use App\Model\Entity\PersonRole; use App\Model\Entity\Petition; +use Cake\Collection\Collection; +use Cake\Database\Connection; use Cake\Database\Expression\QueryExpression; use Cake\Datasource\EntityInterface; use Cake\ORM\Query; use Cake\ORM\Table; use Cake\ORM\TableRegistry; +use Cake\Utility\Hash; use Cake\Validation\Validator; class AttributeCollectorsTable extends Table { @@ -126,49 +131,153 @@ public function initialize(array $config): void { * @since COmanage Registry v5.1.0 */ - public function finalize(int $id, \App\Model\Entity\Petition $petition) { + public function finalize(int $id, \App\Model\Entity\Petition $petition): bool + { $cfg = $this->get($id); if(empty($petition->enrollee_person_id)) { throw new \InvalidArgumentException(__d('error', 'Petitions.enrollee.notfound', [$petition->id])); } + // I will get the model from the supported attributes. + $supportedAttributes = $this->EnrollmentAttributes->supportedAttributes(); + $People = TableRegistry::getTableLocator()->get('People'); $person = $People->get($petition->enrollee_person_id); + $role = null; $attributes = $this->EnrollmentAttributes ->PetitionAttributes ->find() ->where(['petition_id' => $petition->id]) - ->firstOrFail(); + ->contain( + ['EnrollmentAttributes' => ['AttributeTypes']] + )->toArray(); + + // Get the collection object + $attributesCollection = new Collection($attributes); + + + /*********** PERSON ROLE ***************/ + // Filter the Person Role Attributes and keep the field name + $personRoleAttributes = (new Collection($supportedAttributes))->filter(function($attr, $key) { + return isset($attr['model']) && $attr['model'] == 'PersonRole'; + })->toArray(); + $personRoleAttributes = array_keys($personRoleAttributes); + + // Get all the fields/values required to build the PrersonRole + $fieldsForPersonRole = $attributesCollection->filter(function($attr, $key) use ($personRoleAttributes) { + return in_array($attr['enrollment_attribute']['attribute'], $personRoleAttributes); + })->toArray(); + + // Start a Transaction + $cxn = $this->getConnection(); + $cxn->begin(); + + // Save the Person Role + try { + $personRoleObj = TableRegistry::getTableLocator()->get('PersonRoles'); + $role = $personRoleObj->saveAttributes((int)$person->id, $fieldsForPersonRole); + } catch (\Exception $e) { + $cxn->rollback(); + $this->llog('error', __d('error', 'save', [$e->getMessage()])); + throw new \RuntimeException(__d('error', 'save', ['PersonRole'])); + } - // XXX Should i save the primary name? - // Since we're not modifying $person, it's a bit clearer if we save each entity - // individually than try to save related + /********* MVEAS **************/ + // Filter the MVEAS Attributes and keep the field name + $mveaAttributes = (new Collection($supportedAttributes))->filter(function($attr, $key) { + return isset($attr['mveaModel']); + })->toArray(); + $mveaAttributes = array_keys($mveaAttributes); + + // MVEAs for Person + $this->handleMveaAttributes( + $person, + $role, + 'Person', + $mveaAttributes, + $attributes, + $cxn + ); - $Names = TableRegistry::getTableLocator()->get('Names'); + // MVEAs for Role + $this->handleMveaAttributes( + $person, + $role, + 'PersonRole', + $mveaAttributes, + $attributes, + $cxn + ); - // Get the Primary Name - $primaryName = $Names->primaryName($person->id); + // Save the Date Of Birth. This is the only one that is single valued + // and goes under the Person - // XXX enforce CoSettings required/permitted fields here? - $name = [ - 'person_id' => $person->id, - 'primary_name' => true, - 'type_id' => $cfg->name_type_id - ]; + $cxn->commit(); - foreach(['honorific', 'given', 'middle', 'family', 'suffix'] as $n) { - if(!empty($attributes->$n)) { - $name[$n] = $attributes->$n; - } + return true; + } + + /** + * Handle MVEA (Multi-Valued Enrollment Attributes) for a model. + * + * This method processes and saves Multi-Valued Enrollment Attributes associated + * with a specific model (e.g., Person, PersonRole) for the given person and role. + * + * @param Person|null $person The person entity involved. + * @param PersonRole|null $role The person role entity involved. + * @param string $mveaParent + * @param array $mveaAttributes + * @param array $attributes Collection of petition attributes to filter and process. + * @param Connection $cxn Database connection used for transactions. + * @return void + * @since COmanage Registry v5.1.0 + */ + protected function handleMveaAttributes( + Person|null $person, + PersonRole|null $role, + string $mveaParent, + array $mveaAttributes, + array $attributes, + Connection $cxn + ): void { + $attributesCollection = new Collection($attributes); + // MVEAs for the requested model + $fieldsForMveaModel = $attributesCollection->filter(function($attr, $key) use ($mveaAttributes, $mveaParent) { + return in_array($attr['enrollment_attribute']['attribute'], $mveaAttributes) + && $attr['enrollment_attribute']['attribute_mvea_parent'] == $mveaParent; + })->toArray(); + + if(empty($fieldsForMveaModel)) { + return; } - $Names->saveOrFail($Names->newEntity($name)); - // XXX Should i save the email? + $enrollmentAttributes = Hash::combine( + $fieldsForMveaModel, + '{n}.enrollment_attribute_id', '{n}.enrollment_attribute.attribute', + ); - return true; + + foreach ($enrollmentAttributes as $attribute_id => $attribute) { + // Get all the fields for this enrollment_attribute_id + $fieldsForAttribute = $attributesCollection->filter(function($attr, $key) use ($attribute_id) { + return $attr['enrollment_attribute_id'] == $attribute_id; + })->toArray(); + + $supportedAttributes = $this->EnrollmentAttributes->supportedAttributes(); + $mveaModel = $supportedAttributes[$attribute]['mveaModel']; + + try { + $modelObj = TableRegistry::getTableLocator()->get($mveaModel); + $modelObj->saveAttributes((int)$person->id, $role?->id, $mveaParent, $fieldsForAttribute); + } catch (\Exception $e) { + $cxn->rollback(); + $this->llog('error', __d('error', 'save', [$e->getMessage()])); + throw new \RuntimeException(__d('error', 'save', [$mveaModel])); + } + } } /** diff --git a/app/plugins/CoreEnroller/src/Model/Table/EnrollmentAttributesTable.php b/app/plugins/CoreEnroller/src/Model/Table/EnrollmentAttributesTable.php index ab46c269..9fbb7dd8 100644 --- a/app/plugins/CoreEnroller/src/Model/Table/EnrollmentAttributesTable.php +++ b/app/plugins/CoreEnroller/src/Model/Table/EnrollmentAttributesTable.php @@ -67,6 +67,11 @@ public function initialize(array $config): void { // Define associations $this->belongsTo('CoreEnroller.AttributeCollectors'); + $this->belongsTo('AttributeTypes') + ->setClassName('Types') + ->setForeignKey('attribute_type') + ->setProperty('attribute_type'); + $this->hasMany('CoreEnroller.PetitionAttributes') // XXX do we really want to allow deletion once the definition is in use? diff --git a/app/plugins/CoreEnroller/templates/EnrollmentAttributes/fields.inc b/app/plugins/CoreEnroller/templates/EnrollmentAttributes/fields.inc index 09a3a971..05ddb664 100644 --- a/app/plugins/CoreEnroller/templates/EnrollmentAttributes/fields.inc +++ b/app/plugins/CoreEnroller/templates/EnrollmentAttributes/fields.inc @@ -163,6 +163,9 @@ if (\in_array($attribute_type, ['adHocAttribute'], true)) { print $this->element('form/listItem', [ 'arguments' => [ 'fieldName' => 'attribute_tag', + 'fieldOptions' => [ + 'required' => true + ] ]]); } diff --git a/app/src/Model/Table/AdHocAttributesTable.php b/app/src/Model/Table/AdHocAttributesTable.php index 2ff7490c..212857ce 100644 --- a/app/src/Model/Table/AdHocAttributesTable.php +++ b/app/src/Model/Table/AdHocAttributesTable.php @@ -146,4 +146,43 @@ public function validationDefault(Validator $validator): Validator { return $validator; } + + + /** + * Save ad hoc attributes for a person or person role. + * + * @since COmanage Registry v5.1.0 + * @param int $personId ID of the person the attributes belong to + * @param int|null $roleId ID of the person role the attributes belong to (or null if not applicable) + * @param string $parentModel Name of the parent model ('Person' or 'PersonRole') + * @param array $fields Array of fields containing the attributes to be saved + * @return bool True on success + * @throws \InvalidArgumentException Thrown if required parameters are missing or invalid + * @throws \Cake\ORM\Exception\PersistenceFailedException If saving the entity fails + */ + public function saveAttributes(int $personId, ?int $roleId, string $parentModel, array $fields): bool + { + foreach ($fields as $idx => $field) { + // Check if this has already been saved + $adhoc = [ + 'tag' => $field->enrollment_attribute->attribute_tag, + 'value' => $field->value + ]; + + if($parentModel === 'Person') { + if(empty($personId)) { + throw new \InvalidArgumentException(__d('error', 'personId')); + } + $adhoc['person_id'] = $personId; + } elseif ($parentModel === 'PersonRole') { + if(empty($roleId)) { + throw new \InvalidArgumentException(__d('error', 'person_role_id')); + } + $adhoc['person_role_id'] = $roleId; + } + + $this->saveOrFail($this->newEntity($adhoc)); + } + return true; + } } \ No newline at end of file diff --git a/app/src/Model/Table/AddressesTable.php b/app/src/Model/Table/AddressesTable.php index 46b58b29..741ef659 100644 --- a/app/src/Model/Table/AddressesTable.php +++ b/app/src/Model/Table/AddressesTable.php @@ -29,10 +29,12 @@ namespace App\Model\Table; +use Cake\Collection\Collection; +use Cake\Utility\Hash; +use \App\Lib\Enum\LanguageEnum; use \Cake\ORM\Table; use \Cake\ORM\TableRegistry; use \Cake\Validation\Validator; -use \App\Lib\Enum\LanguageEnum; class AddressesTable extends Table { use \App\Lib\Traits\AutoViewVarsTrait; @@ -277,4 +279,50 @@ public function validationDefault(Validator $validator): Validator { public function getPermittedFields(): array { return $this->permittedFields; } + + + /** + * Save attributes based on the provided data. + * + * This method saves the attributes associated with a person or a person role. + * + * @since COmanage Registry v5.1.0 + * @param int $personId ID of the person + * @param int|null $roleId ID of the person role (nullable) + * @param string $parentModel Model to associate the attributes with ('Person' or 'PersonRole') + * @param array $fields Array of attribute fields to save + * @return bool True on successful save + * @throws \InvalidArgumentException If required parameters are missing + * @throws \Cake\ORM\Exception\PersistenceFailedException If the entity could not be saved + */ + public function saveAttributes(int $personId, ?int $roleId, string $parentModel, array $fields): bool + { + $fieldType = Hash::extract($fields, '{n}.enrollment_attribute.attribute_type.id'); + $fieldTypeId = (new Collection($fieldType))->first(); + + $address = [ + 'type_id' => $fieldTypeId + ]; + + if($parentModel === 'Person') { + if(empty($personId)) { + throw new \InvalidArgumentException(__d('error', 'personId')); + } + $address['person_id'] = $personId; + } elseif ($parentModel === 'PersonRole') { + if(empty($roleId)) { + throw new \InvalidArgumentException(__d('error', 'person_role_id')); + } + $address['person_role_id'] = $roleId; + } + + // We need to get this from CoSettings?? + foreach($fields as $fld) { + $address[$fld->column_name] = $fld->value; + } + + $this->saveOrFail($this->newEntity($address)); + + return true; + } } \ No newline at end of file diff --git a/app/src/Model/Table/EmailAddressesTable.php b/app/src/Model/Table/EmailAddressesTable.php index 889b334c..8dd7c457 100644 --- a/app/src/Model/Table/EmailAddressesTable.php +++ b/app/src/Model/Table/EmailAddressesTable.php @@ -29,10 +29,12 @@ namespace App\Model\Table; +use Cake\Collection\Collection; use Cake\Datasource\EntityInterface; use Cake\Event\EventInterface; use Cake\ORM\Table; use Cake\ORM\TableRegistry; +use Cake\Utility\Hash; use Cake\Validation\Validator; use \App\Lib\Enum\ActionEnum; use \App\Lib\Enum\ProvisioningContextEnum; @@ -370,6 +372,38 @@ public function validationDefault(Validator $validator): Validator { ]); $validator->allowEmptyString('source_email_address_id'); - return $validator; + return $validator; + } + + + /** + * Save attributes for a person, possibly tied to a role and parent model. + * Each field is processed to create a new EmailAddress entity and saved. + * + * @since COmanage Registry v5.1.0 + * @param int $personId Person ID + * @param int|null $roleId Role ID (if applicable) + * @param string $parentModel Parent model name + * @param array $fields Array of fields containing enrollment attributes + * @return bool True on success + * @throws \Cake\Datasource\Exception\RecordNotFoundException + * @throws \Cake\ORM\Exception\PersistenceFailedException + */ + public function saveAttributes(int $personId, ?int $roleId, string $parentModel, array $fields): bool + { + $fieldType = Hash::extract($fields, '{n}.enrollment_attribute.attribute_type.id'); + $fieldTypeId = (new Collection($fieldType))->first(); + + foreach ($fields as $idx => $field) { + // Check if this has already been saved + $email = [ + 'person_id' => $personId, + 'mail' => $field->value, + 'type_id' => $fieldTypeId + ]; + + $this->saveOrFail($this->newEntity($email)); + } + return true; } } \ No newline at end of file diff --git a/app/src/Model/Table/NamesTable.php b/app/src/Model/Table/NamesTable.php index 80505559..11546b87 100644 --- a/app/src/Model/Table/NamesTable.php +++ b/app/src/Model/Table/NamesTable.php @@ -29,14 +29,16 @@ namespace App\Model\Table; +use Cake\Collection\Collection; +use Cake\Utility\Hash; +use \App\Lib\Enum\ActionEnum; +use \App\Lib\Enum\LanguageEnum; use \Cake\Event\EventInterface; 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; @@ -251,6 +253,33 @@ public function primaryName(int $id, string $recordType='person') { } } + /** + * Check if the Person has a primary name + * + * @param int $id Record ID + * @param string $recordType Type of record to find primary name for, 'person' or 'external_identity' + * @return bool Name Entity + * @since COmanage Registry v5.1.0 + */ + + public function hasPrimaryName(int $id, string $recordType='person'): bool + { + if($recordType == 'person') { + // Return the Primary Name + + return $this->find() + ->where(['person_id' => $id, + 'primary_name' => true]) + ->count() > 0; + } else { + // Return the first name, whatever it is + + return $this->find() + ->where(['external_identity_id' => $id]) + ->count() > 0; + } + } + /** * Application Rule to determine if there is at least one Name associated * with the Person. @@ -433,4 +462,41 @@ public function validationDefault(Validator $validator): Validator { return $validator; } + + + /** + * Save attributes for a Person entity. + * + * @param int $personId ID of the person for whom attributes are being saved. + * @param int|null $roleId ID of the associated role (if any). + * @param string $parentModel Parent model Name. + * @param array $fields Array of fields and their values to be saved. + * @return bool Returns true on successful save, throws exception otherwise. + * @throws \Cake\ORM\Exception\PersistenceFailedException If the entity could not be saved. + */ + public function saveAttributes(int $personId, ?int $roleId, string $parentModel, array $fields): bool + { + $fieldType = Hash::extract($fields, '{n}.enrollment_attribute.attribute_type.id'); + $fieldTypeId = (new Collection($fieldType))->first(); + + $name = [ + 'person_id' => $personId, + 'type_id' => $fieldTypeId + ]; + + // Save the primary name + if(!$this->hasPrimaryName($personId)) { + $name['primary_name'] = true; + } + + // We need to get this from CoSettings?? + foreach($fields as $fld) { + $name[$fld->column_name] = $fld->value; + } + + // XXX Check if we already have an this value saved + + $this->saveOrFail($this->newEntity($name)); + return true; + } } \ No newline at end of file diff --git a/app/src/Model/Table/PersonRolesTable.php b/app/src/Model/Table/PersonRolesTable.php index 335ea96b..548b57ac 100644 --- a/app/src/Model/Table/PersonRolesTable.php +++ b/app/src/Model/Table/PersonRolesTable.php @@ -31,6 +31,7 @@ use Cake\Event\EventInterface; use \Cake\I18n\FrozenTime; +use Cake\ORM\Entity; use Cake\ORM\Query; use Cake\ORM\RulesChecker; use Cake\ORM\Table; @@ -709,4 +710,30 @@ public function validationDefault(Validator $validator): Validator { return $validator; } + + + /** + * Save attributes for a person. + * @param int $personId ID of the person for whom attributes are being saved + * @param array $fields Array of fields/attributes to be saved + * + * @return \App\Model\Entity\PersonRole The newly saved Role + * @throws \Cake\Datasource\Exception\RecordNotFoundException If an issue occurs during saving + * @since COmanage Registry v5.1.0 + */ + public function saveAttributes(int $personId, array $fields): \App\Model\Entity\PersonRole + { + $role = [ + 'person_id' => $personId, + ]; + + // We need to get this from CoSettings?? + foreach($fields as $fld) { + $role[$fld->enrollment_attribute->attribute] = $fld->value; + } + + // XXX Check if we already have one + + return $this->saveOrFail($this->newEntity($role)); + } } \ No newline at end of file diff --git a/app/src/Model/Table/PronounsTable.php b/app/src/Model/Table/PronounsTable.php index 875356e4..8f8127b6 100644 --- a/app/src/Model/Table/PronounsTable.php +++ b/app/src/Model/Table/PronounsTable.php @@ -174,4 +174,20 @@ public function validationDefault(Validator $validator): Validator { return $validator; } + + + /** + * Save attributes for a given person and optionally their role. + * + * @since COmanage Registry v5.0.0 + * @param int $personId Identifier for the person + * @param int|null $roleId Identifier for the role (nullable) + * @param string $parentModel Name of the parent model + * @param array $fields Array of attributes to save + * @return bool True on success, false otherwise + */ + public function saveAttributes(int $personId, ?int $roleId, string $parentModel, array $fields): bool + { + return true; + } } \ No newline at end of file diff --git a/app/src/Model/Table/TelephoneNumbersTable.php b/app/src/Model/Table/TelephoneNumbersTable.php index e3d3bc05..72a938f5 100644 --- a/app/src/Model/Table/TelephoneNumbersTable.php +++ b/app/src/Model/Table/TelephoneNumbersTable.php @@ -29,6 +29,8 @@ namespace App\Model\Table; +use Cake\Collection\Collection; +use Cake\Utility\Hash; use \Cake\ORM\Table; use \Cake\ORM\TableRegistry; use \Cake\Validation\Validator; @@ -216,4 +218,49 @@ public function validationDefault(Validator $validator): Validator { return $validator; } + + /** + * Save telephone number attributes. + * + * This method saves telephone number attributes for a person or a person's role. + * + * @param int $personId The person ID. Required if $parentModel is "Person". + * @param int|null $roleId The role ID. Required if $parentModel is "PersonRole". + * @param string $parentModel The parent model, either "Person" or "PersonRole". + * @param array $fields The attributes to be saved, provided as an array of data. + * @return bool True on success. + * @throws \InvalidArgumentException If required parameters are missing or invalid. + * @throws \Cake\ORM\Exception\PersistenceFailedException If saving the entity fails. + * @since COmanage Registry v5.1.0 + */ + public function saveAttributes(int $personId, ?int $roleId, string $parentModel, array $fields): bool + { + $fieldType = Hash::extract($fields, '{n}.enrollment_attribute.attribute_type.id'); + $fieldTypeId = (new Collection($fieldType))->first(); + + $telephone = [ + 'type_id' => $fieldTypeId + ]; + + if($parentModel === 'Person') { + if(empty($personId)) { + throw new \InvalidArgumentException(__d('error', 'personId')); + } + $telephone['person_id'] = $personId; + } elseif ($parentModel === 'PersonRole') { + if(empty($roleId)) { + throw new \InvalidArgumentException(__d('error', 'person_role_id')); + } + $telephone['person_role_id'] = $roleId; + } + + // We need to get this from CoSettings?? + foreach($fields as $fld) { + $telephone[$fld->column_name] = $fld->value; + } + + $this->saveOrFail($this->newEntity($telephone)); + + return true; + } } \ No newline at end of file diff --git a/app/src/Model/Table/UrlsTable.php b/app/src/Model/Table/UrlsTable.php index 0714f083..f861c0be 100644 --- a/app/src/Model/Table/UrlsTable.php +++ b/app/src/Model/Table/UrlsTable.php @@ -191,4 +191,19 @@ public function validationDefault(Validator $validator): Validator { return $validator; } + + /** + * Save attributes for a given person and optionally their role. + * + * @since COmanage Registry v5.0.0 + * @param int $personId Identifier for the person + * @param int|null $roleId Identifier for the role (nullable) + * @param string $parentModel Name of the parent model + * @param array $fields Array of attributes to save + * @return bool True on success, false otherwise + */ + public function saveAttributes(int $personId, ?int $roleId, string $parentModel, array $fields): bool + { + return true; + } } \ No newline at end of file