From 5c95f8422d31611ed111933740cedbbc1c89de5a Mon Sep 17 00:00:00 2001 From: Ioannis Igoumenos Date: Mon, 21 Oct 2024 19:53:25 +0300 Subject: [PATCH 1/5] Fix Petition Attributes Form rendering --- .../Model/Table/EnrollmentAttributesTable.php | 63 ++++--- .../AttributeCollectors/dispatch.inc | 19 ++- .../templates/EnrollmentAttributes/fields.inc | 2 +- app/src/Controller/StandardController.php | 147 +--------------- app/src/Lib/Traits/AutoViewVarsTrait.php | 159 ++++++++++++++++++ app/src/View/Helper/FieldHelper.php | 7 +- app/src/View/Helper/PetitionHelper.php | 56 ++++++ 7 files changed, 281 insertions(+), 172 deletions(-) create mode 100644 app/src/View/Helper/PetitionHelper.php diff --git a/app/plugins/CoreEnroller/src/Model/Table/EnrollmentAttributesTable.php b/app/plugins/CoreEnroller/src/Model/Table/EnrollmentAttributesTable.php index d9ffeea29..e71c46025 100644 --- a/app/plugins/CoreEnroller/src/Model/Table/EnrollmentAttributesTable.php +++ b/app/plugins/CoreEnroller/src/Model/Table/EnrollmentAttributesTable.php @@ -211,54 +211,64 @@ public function supportedAttributes(string $format='full'): array { // Single valued Person attributes $attrs['date_of_birth'] = [ 'label' => __d('field', 'date_of_birth'), - 'model' => 'Person' + 'model' => 'Person', + 'fieldType' => 'date' ]; // Single valued Person Role attributes $attrs['affiliation_type_id'] = [ 'label' => __d('field', 'affiliation'), - 'model' => 'PersonRole' + 'model' => 'PersonRole', + 'fieldType' => 'string' ]; $attrs['cou_id'] = [ 'label' => __d('controller', 'Cous', [1]), - 'model' => 'PersonRole' + 'model' => 'PersonRole', + 'fieldType' => 'integer' ]; $attrs['department'] = [ 'label' => __d('field', 'department'), - 'model' => 'PersonRole' + 'model' => 'PersonRole', + 'fieldType' => 'string' ]; $attrs['manager_person_id'] = [ 'label' => __d('field', 'manager'), - 'model' => 'PersonRole' + 'model' => 'PersonRole', + 'fieldType' => 'integer' ]; $attrs['organization'] = [ 'label' => __d('field', 'organization'), - 'model' => 'PersonRole' + 'model' => 'PersonRole', + 'fieldType' => 'string' ]; $attrs['sponsor_person_id'] = [ 'label' => __d('field', 'sponsor'), - 'model' => 'PersonRole' + 'model' => 'PersonRole', + 'fieldType' => 'integer' ]; $attrs['title'] = [ 'label' => __d('field', 'title'), - 'model' => 'PersonRole' + 'model' => 'PersonRole', + 'fieldType' => 'string' ]; $attrs['valid_from'] = [ 'label' => __d('field', 'valid_from'), - 'model' => 'PersonRole' + 'model' => 'PersonRole', + 'fieldType' => 'datetime' ]; $attrs['valid_through'] = [ 'label' => __d('field', 'valid_through'), - 'model' => 'PersonRole' + 'model' => 'PersonRole', + 'fieldType' => 'datetime' ]; // MVEAs, which might attach to the Person or Person Role or both @@ -266,49 +276,57 @@ public function supportedAttributes(string $format='full'): array { $attrs['address'] = [ 'label' => __d('controller', 'Addresses', [1]), 'mveaModel' => 'Addresses', - 'mveaParents' => ['Person', 'PersonRole'] + 'mveaParents' => ['Person', 'PersonRole'], + 'fieldType' => 'string' ]; $attrs['adHocAttribute'] = [ 'label' => __d('controller', 'AdHocAttributes', [1]), 'mveaModel' => 'AdHocAttributes', - 'mveaParents' => ['Person', 'PersonRole'] + 'mveaParents' => ['Person', 'PersonRole'], + 'fieldType' => 'string' ]; $attrs['emailAddress'] = [ 'label' => __d('controller', 'EmailAddresses', [1]), 'mveaModel' => 'EmailAddresses', - 'mveaParents' => ['Person'] + 'mveaParents' => ['Person'], + 'fieldType' => 'string' ]; $attrs['identifier'] = [ 'label' => __d('controller', 'Identifiers', [1]), 'mveaModel' => 'Identifiers', - 'mveaParents' => ['Person'] + 'mveaParents' => ['Person'], + 'fieldType' => 'string' ]; $attrs['name'] = [ 'label' => __d('controller', 'Names', [1]), 'mveaModel' => 'Names', - 'mveaParents' => ['Person'] + 'mveaParents' => ['Person'], + 'fieldType' => 'string' ]; $attrs['pronoun'] = [ 'label' => __d('controller', 'Pronouns', [1]), 'mveaModel' => 'Pronouns', - 'mveaParents' => ['Person'] + 'mveaParents' => ['Person'], + 'fieldType' => 'string' ]; $attrs['telephoneNumber'] = [ 'label' => __d('controller', 'TelephoneNumbers', [1]), 'mveaModel' => 'TelephoneNumbers', - 'mveaParents' => ['Person', 'PersonRole'] + 'mveaParents' => ['Person', 'PersonRole'], + 'fieldType' => 'string' ]; $attrs['url'] = [ 'label' => __d('controller', 'Urls', [1]), 'mveaModel' => 'Urls', - 'mveaParents' => ['Person'] + 'mveaParents' => ['Person'], + 'fieldType' => 'string' ]; // Group memberships, as reflected by the Group ID for the membership to be created in @@ -318,19 +336,22 @@ public function supportedAttributes(string $format='full'): array { // to attach the membership to, but the label says Group Member because that'll be // more obvious 'label' => __d('controller', 'GroupMembers', [1]), - 'model' => 'Group' + 'model' => 'Group', + 'fieldType' => 'integer' ]; // Attributes that are only stored in the Petition $attrs['petition_text'] = [ 'label' => __d('core_enroller', 'field.EnrollmentAttributes.petition_text'), - 'model' => 'Petition' + 'model' => 'Petition', + 'fieldType' => 'string' ]; $attrs['petition_textarea'] = [ 'label' => __d('core_enroller', 'field.EnrollmentAttributes.petition_textarea'), - 'model' => 'Petition' + 'model' => 'Petition', + 'fieldType' => 'text' ]; switch($format) { diff --git a/app/plugins/CoreEnroller/templates/AttributeCollectors/dispatch.inc b/app/plugins/CoreEnroller/templates/AttributeCollectors/dispatch.inc index f9c4ee9ff..1ed292c0f 100644 --- a/app/plugins/CoreEnroller/templates/AttributeCollectors/dispatch.inc +++ b/app/plugins/CoreEnroller/templates/AttributeCollectors/dispatch.inc @@ -46,13 +46,18 @@ foreach($vv_enrollment_attributes as $attr) { } } + $supportedAttributes = $this->Petition->getSupportedEnrollmentAttribute($attr->attribute); + + $formArguments = [ + // We prefix the attribute ID with a string because Cake seems to sometimes have + // problems with field names that are purely integers (even if cast to strings) + 'fieldName' => 'field-' . $attr->id, + 'fieldOptions' => $options, + 'fieldLabel' => $attr->label, + 'fieldType' => $supportedAttributes['fieldType'] + ]; + print $this->element('form/listItem', [ - 'arguments' => [ - // We prefix the attribute ID with a string because Cake seems to sometimes have - // problems with field names that are purely integers (even if cast to strings) - 'fieldName' => 'field-' . $attr->id, - 'fieldOptions' => $options, - 'fieldLabel' => $attr->label - ] + 'arguments' => $formArguments ]); } \ No newline at end of file diff --git a/app/plugins/CoreEnroller/templates/EnrollmentAttributes/fields.inc b/app/plugins/CoreEnroller/templates/EnrollmentAttributes/fields.inc index ae8aac2c9..dc30198bb 100644 --- a/app/plugins/CoreEnroller/templates/EnrollmentAttributes/fields.inc +++ b/app/plugins/CoreEnroller/templates/EnrollmentAttributes/fields.inc @@ -53,7 +53,7 @@ $hidden = [ * Include a "cancel" button next to the "save" button */ -$this->set('vv_include_cancel',true); +$this->set('vv_include_cancel', true); /* * Common Fields for all attributes diff --git a/app/src/Controller/StandardController.php b/app/src/Controller/StandardController.php index e2ad7daec..02340d666 100644 --- a/app/src/Controller/StandardController.php +++ b/app/src/Controller/StandardController.php @@ -616,150 +616,13 @@ protected function populateAutoViewVars(object $obj=null) { $modelsName = $this->name; // $table = the actual table object $table = $this->$modelsName; - - // Populate certain view vars (eg: selects) automatically. - - // AutoViewVarsTrait - if(method_exists($table, "getAutoViewVars") - && $table->getAutoViewVars()) { - foreach($table->getAutoViewVars() as $vvar => $avv) { - switch($avv['type']) { - case 'array': - // Use the provided array of values. By default, we use the values - // for the keys as well, to generate HTML along the lines of - // . (See also 'hash'.) - $this->set($vvar, array_combine($avv['array'], $avv['array'])); - break; - case 'enum': - // We just want the localized text strings for the defined constants. - $class = '\\App\\Lib\\Enum\\'.$avv['class']; - // We support plugin notation for plugin defined enumerations. - if(strstr($avv['class'], ".")) { - $bits = explode('.', $avv['class'], 2); - $class = '\\'.$bits[0].'\\Lib\\Enum\\'.$bits[1]; - } - $this->set($vvar, $class::getLocalizedConsts()); - break; - case 'hash': - // Like 'array' but we assume we are passed key/value pairs - $this->set($vvar, $avv['hash']); - break; - // "auxiliary" and "select" do basically the same thing, but the former - // returns the full object and the latter just returns a hash suitable - // for a select. "type" is a shorthand for "select" for type_id. - case 'type': - // Inject configuration. Since we're only ever looking at the types - // table, inject the current CO along with the requested attribute - $avv['model'] = 'Types'; - if(is_array($avv['attribute'])) { - $avv['where'] = [ - 'attribute IN' => $avv['attribute'], - 'status' => SuspendableStatusEnum::Active - ]; - } else { - $avv['where'] = [ - 'attribute' => $avv['attribute'], - 'status' => SuspendableStatusEnum::Active - ]; - } - // fall through - case 'auxiliary': -// XXX add list as in match? - case 'select': - $avvmodel = $avv['model']; - $this->$avvmodel = TableRegistry::getTableLocator()->get($avvmodel); - // XXX We should probably move to a more generic approach. - // Models can have various types of parent keys (and sometimes multiple concurrently), - // so it’s better to use PrimaryLinkTrait to handle this. - // if(method_exists($this->$avvmodel, "calculateCoForRecord")) { - // $avv['where']['co_id'] = $this->$avvmodel->calculateCoForRecord($obj) - // } - if($this->$avvmodel->getSchema()->hasColumn('co_id')) { - $avv['where']['co_id'] = $this->getCOID(); - } - - $query = $this->$avvmodel->find($avv['type'] == 'auxiliary' ? 'all' : 'list'); - - if(!empty($avv['find'])) { - if($avv['find'] == 'filterPrimaryLink') { - // We're filtering the requested model, not our current model. - // See if the requested key is available, and if so run the find. - - $linkFilter = $table->getPrimaryLink(); - - if($linkFilter) { - // Try to find the $linkFilter value - $v = null; - - // We might have been passed an object with the current value - if($obj && !empty($obj->$linkFilter)) { - $v = $obj->$linkFilter; - } elseif(!empty($this->request->getQuery($linkFilter))) { - $v = $this->request->getQuery($linkFilter); - } -// XXX also need to check getData()? -// XXX shouldn't this use $this->getPrimaryLink() instead? Or maybe move $this->primaryLink -// to PrimaryLinkTrait and call it there? - - if($v) { - $avv['where'][$table->getAlias().'.'.$linkFilter] = $v; - //$query = $query->where([$table->getAlias().'.'.$linkFilter => $v]); - } - } - } else { - // Use the specified finder, if configured - $query = $query->find($avv['find']); - } - } elseif($table->getSchema()->hasColumn('co_id')) { - // XXX is this the best logic? maybe some relation to filterPrimaryLink? - // By default, filter everything on CO ID - $avv['where']['co_id'] = $this->getCOID(); - //$query = $query->where([$table->getAlias().'.co_id' => $this->getCOID()]); - } - // Where Rule. The rule will be transfered as is - if(!empty($avv['where'])) { - // Filter on the specified clause (of the form [column=>value]) - $query = $query->where($avv['where']); - } - - // Where rule that will be evaluated. We use the custom whereEvan key to - // distinguish from the plain where. Also it might contain more than one conditions - if(!empty($avv['whereEval'])) { - foreach ($avv['whereEval'] as $whereClauseColumn => $chainedMethodDescription) { - $calculatedValue = FunctionUtilities::dynamicChainedFunction( - $this, - $chainedMethodDescription - ); - $query = $query->where([$whereClauseColumn => $calculatedValue]); - } - } - - // Sort the list by display field - if(!empty($avv['model']) && method_exists($this->$avvmodel, "getDisplayField")) { - $query->order([$this->$avvmodel->getDisplayField() => 'ASC']); - } elseif(method_exists($table, "getDisplayField")) { - $query->order([$table->getDisplayField() => 'ASC']); - } - - $this->set($vvar, $query->toArray()); - break; - case 'parent': - $modelsName = $this->name; - // $table = the actual table object - $table = $this->$modelsName; - $this->set($vvar, $table->getParents($this->getCOID())); - break; - case 'plugin': - $PluginTable = $this->getTableLocator()->get('Plugins'); - $this->set($vvar, $PluginTable->getActivePluginModels($avv['pluginType'])); - break; - default: -// XXX I18n? and in match? - throw new \LogicException(__d('error', 'auto.viewvar.type.unknown', [$avv['type']])); - } + // AutoViewVarsTrait + if(method_exists($table, 'getAutoViewVars') && $table->getAutoViewVars()) { + foreach ($table->calculateAutoViewVars($this->getCOID(), $obj) as $vvar => $value) { + $this->set($vvar, $value); } - } + } } /** diff --git a/app/src/Lib/Traits/AutoViewVarsTrait.php b/app/src/Lib/Traits/AutoViewVarsTrait.php index afb1862a8..98bf12413 100644 --- a/app/src/Lib/Traits/AutoViewVarsTrait.php +++ b/app/src/Lib/Traits/AutoViewVarsTrait.php @@ -29,6 +29,10 @@ namespace App\Lib\Traits; +use App\Lib\Enum\SuspendableStatusEnum; +use App\Lib\Util\FunctionUtilities; +use \Cake\ORM\TableRegistry; + trait AutoViewVarsTrait { // Array (and configuration) of view variables to automatically populate private $autoViewVars = null; @@ -54,4 +58,159 @@ public function getAutoViewVars() { public function setAutoViewVars($vars) { $this->autoViewVars = $vars; } + + /** + * Calculate the AutoView Vars + * + * @param int $coId + * @param Object|null $obj Current object (eg: from edit), if set + * + * @return \Generator + * @since COmanage Registry v5.0.0 + */ + public function calculateAutoViewVars(int $coId, Object $obj = null): \Generator + { + // $table = the actual table object + $table = $this; + + foreach($table->getAutoViewVars() as $vvar => $avv) { + $generatedValue = null; + + switch($avv['type']) { + case 'array': + // Use the provided array of values. By default, we use the values + // for the keys as well, to generate HTML along the lines of + // . (See also 'hash'.) + $generatedValue = array_combine($avv['array'], $avv['array']); + break; + case 'enum': + // We just want the localized text strings for the defined constants. + $class = '\\App\\Lib\\Enum\\'.$avv['class']; + // We support plugin notation for plugin defined enumerations. + if(strstr($avv['class'], ".")) { + $bits = explode('.', $avv['class'], 2); + $class = '\\'.$bits[0].'\\Lib\\Enum\\'.$bits[1]; + } + + $generatedValue = $class::getLocalizedConsts(); + break; + case 'hash': + // Like 'array' but we assume we are passed key/value pairs + $generatedValue = $avv['hash']; + break; + // "auxiliary" and "select" do basically the same thing, but the former + // returns the full object and the latter just returns a hash suitable + // for a select. "type" is a shorthand for "select" for type_id. + case 'type': + // Inject configuration. Since we're only ever looking at the types + // table, inject the current CO along with the requested attribute + $avv['model'] = 'Types'; + if(\is_array($avv['attribute'])) { + $avv['where'] = [ + 'attribute IN' => $avv['attribute'], + 'status' => SuspendableStatusEnum::Active + ]; + } else { + $avv['where'] = [ + 'attribute' => $avv['attribute'], + 'status' => SuspendableStatusEnum::Active + ]; + } + // fall through + case 'auxiliary': +// XXX add list as in match? + case 'select': + $avvmodel = $avv['model']; + $this->$avvmodel = TableRegistry::getTableLocator()->get($avvmodel); + // XXX We should probably move to a more generic approach. + // Models can have various types of parent keys (and sometimes multiple concurrently), + // so it’s better to use PrimaryLinkTrait to handle this. + // if(method_exists($this->$avvmodel, "calculateCoForRecord")) { + // $avv['where']['co_id'] = $this->$avvmodel->calculateCoForRecord($obj) + // } + if($this->$avvmodel->getSchema()->hasColumn('co_id')) { + $avv['where']['co_id'] = $coId; + } + + $query = $this->$avvmodel->find($avv['type'] == 'auxiliary' ? 'all' : 'list'); + + if(!empty($avv['find'])) { + if($avv['find'] == 'filterPrimaryLink') { + // We're filtering the requested model, not our current model. + // See if the requested key is available, and if so run the find. + + $linkFilter = $table->getPrimaryLink(); + + if($linkFilter) { + // Try to find the $linkFilter value + $v = null; + + // We might have been passed an object with the current value + if($obj && !empty($obj->$linkFilter)) { + $v = $obj->$linkFilter; + } elseif(!empty($this->request->getQuery($linkFilter))) { + $v = $this->request->getQuery($linkFilter); + } +// XXX also need to check getData()? +// XXX shouldn't this use $this->getPrimaryLink() instead? Or maybe move $this->primaryLink +// to PrimaryLinkTrait and call it there? + + if($v) { + $avv['where'][$table->getAlias().'.'.$linkFilter] = $v; + //$query = $query->where([$table->getAlias().'.'.$linkFilter => $v]); + } + } + } else { + // Use the specified finder, if configured + $query = $query->find($avv['find']); + } + } elseif($table->getSchema()->hasColumn('co_id')) { + // XXX is this the best logic? maybe some relation to filterPrimaryLink? + // By default, filter everything on CO ID + $avv['where']['co_id'] = $coId; + //$query = $query->where([$table->getAlias().'.co_id' => $coId]); + } + + // Where Rule. The rule will be transfered as is + if(!empty($avv['where'])) { + // Filter on the specified clause (of the form [column=>value]) + $query = $query->where($avv['where']); + } + + // Where rule that will be evaluated. We use the custom whereEvan key to + // distinguish from the plain where. Also it might contain more than one conditions + if(!empty($avv['whereEval'])) { + foreach ($avv['whereEval'] as $whereClauseColumn => $chainedMethodDescription) { + $calculatedValue = FunctionUtilities::dynamicChainedFunction( + $this, + $chainedMethodDescription + ); + $query = $query->where([$whereClauseColumn => $calculatedValue]); + } + } + + // Sort the list by display field + if(!empty($avv['model']) && method_exists($this->$avvmodel, "getDisplayField")) { + $query->order([$this->$avvmodel->getDisplayField() => 'ASC']); + } elseif(method_exists($table, "getDisplayField")) { + $query->order([$table->getDisplayField() => 'ASC']); + } + + $generatedValue = $query->toArray(); + break; + case 'parent': + $generatedValue = $table->getParents($coId); + break; + case 'plugin': + $PluginTable = TableRegistry::getTableLocator()->get('Plugins'); + $generatedValue = $PluginTable->getActivePluginModels($avv['pluginType']); + break; + default: +// XXX I18n? and in match? + throw new \LogicException(__d('error', 'auto.viewvar.type.unknown', [$avv['type']])); + } + + yield $vvar => $generatedValue; + } + } } diff --git a/app/src/View/Helper/FieldHelper.php b/app/src/View/Helper/FieldHelper.php index 4172aa8ff..0ec64f4d3 100644 --- a/app/src/View/Helper/FieldHelper.php +++ b/app/src/View/Helper/FieldHelper.php @@ -186,8 +186,13 @@ public function calculateLiClasses(): string $fieldName = $this->getView()->get('fieldName'); $vv_field_arguments = $this->getView()->get('vv_field_arguments'); + // Get the fieldtype directly from the configuration or calculate it + // The latter will always work for simple model forms. The first one is used + // for more complex use cases + $fieldType = $vv_field_arguments['fieldType'] ?? $this->getFieldType($fieldName); + // Class calculation by field Type - $classes = match ($this->getFieldType($fieldName)) { + $classes = match ($fieldType) { 'date', 'datetime', 'timestamp' => 'fields-datepicker ', diff --git a/app/src/View/Helper/PetitionHelper.php b/app/src/View/Helper/PetitionHelper.php new file mode 100644 index 000000000..20b236b26 --- /dev/null +++ b/app/src/View/Helper/PetitionHelper.php @@ -0,0 +1,56 @@ +supportedAttributes()[$attribute]; + } +} \ No newline at end of file From ee6ef58a6bd900feb8a5f575bd1c43ba731e5e37 Mon Sep 17 00:00:00 2001 From: Ioannis Igoumenos Date: Mon, 21 Oct 2024 20:19:59 +0300 Subject: [PATCH 2/5] populate autoview vars in the petition attribute collection view --- .../Model/Table/EnrollmentAttributesTable.php | 3 ++- .../AttributeCollectors/dispatch.inc | 3 +++ app/src/View/Helper/PetitionHelper.php | 25 ++++++++++++++++--- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/app/plugins/CoreEnroller/src/Model/Table/EnrollmentAttributesTable.php b/app/plugins/CoreEnroller/src/Model/Table/EnrollmentAttributesTable.php index e71c46025..cee5f79a1 100644 --- a/app/plugins/CoreEnroller/src/Model/Table/EnrollmentAttributesTable.php +++ b/app/plugins/CoreEnroller/src/Model/Table/EnrollmentAttributesTable.php @@ -277,7 +277,8 @@ public function supportedAttributes(string $format='full'): array { 'label' => __d('controller', 'Addresses', [1]), 'mveaModel' => 'Addresses', 'mveaParents' => ['Person', 'PersonRole'], - 'fieldType' => 'string' + 'fieldType' => 'string', + 'autoViewVar' => 'addressTypes', ]; $attrs['adHocAttribute'] = [ diff --git a/app/plugins/CoreEnroller/templates/AttributeCollectors/dispatch.inc b/app/plugins/CoreEnroller/templates/AttributeCollectors/dispatch.inc index 1ed292c0f..4e4af3457 100644 --- a/app/plugins/CoreEnroller/templates/AttributeCollectors/dispatch.inc +++ b/app/plugins/CoreEnroller/templates/AttributeCollectors/dispatch.inc @@ -34,6 +34,9 @@ if($vv_action !== 'dispatch') { // Make the Form fields editable $this->Field->enableFormEditMode(); +$this->Petition->populateAutoViewVars(); +// We just populated the AutoViewVars. Add them to the current context +extract($this->viewVars); foreach($vv_enrollment_attributes as $attr) { $options = []; diff --git a/app/src/View/Helper/PetitionHelper.php b/app/src/View/Helper/PetitionHelper.php index 20b236b26..3b4253b85 100644 --- a/app/src/View/Helper/PetitionHelper.php +++ b/app/src/View/Helper/PetitionHelper.php @@ -39,6 +39,18 @@ class PetitionHelper extends Helper { + protected ?object $enrollmentAttributesTable = null; + + // The current entity, if edit or view + protected ?object $entity = null; + + public function initialize(array $config): void + { + parent::initialize($config); + $this->entity = $this->getView()->get('vv_obj'); + $this->enrollmentAttributesTable = new EnrollmentAttributesTable(); + } + /** * Get the * @@ -47,10 +59,15 @@ class PetitionHelper extends Helper * @return array * @since COmanage Registry v5.0.0 */ - public function getSupportedEnrollmentAttribute(string $attribute): array - { - $enrollment_attr_table = new EnrollmentAttributesTable(); + public function getSupportedEnrollmentAttribute(string $attribute): array + { + return $this->enrollmentAttributesTable->supportedAttributes()[$attribute]; + } - return $enrollment_attr_table->supportedAttributes()[$attribute]; + public function populateAutoViewVars(): void + { + foreach ($this->enrollmentAttributesTable->calculateAutoViewVars(2, $this->entity) as $vvar => $value) { + $this->getView()->set($vvar, $value); } + } } \ No newline at end of file From fe4332e5d59f1164078ada1317f97de8b1f8b01d Mon Sep 17 00:00:00 2001 From: Ioannis Igoumenos Date: Tue, 22 Oct 2024 12:33:13 +0300 Subject: [PATCH 3/5] AttributeCollectors/dispatch.inc fix fields configurations. --- .../AttributeCollectors/dispatch.inc | 63 +++++++++++++++++-- 1 file changed, 58 insertions(+), 5 deletions(-) diff --git a/app/plugins/CoreEnroller/templates/AttributeCollectors/dispatch.inc b/app/plugins/CoreEnroller/templates/AttributeCollectors/dispatch.inc index 4e4af3457..6aa173939 100644 --- a/app/plugins/CoreEnroller/templates/AttributeCollectors/dispatch.inc +++ b/app/plugins/CoreEnroller/templates/AttributeCollectors/dispatch.inc @@ -27,6 +27,9 @@ declare(strict_types = 1); +use App\Lib\Enum\StatusEnum; +use \Cake\Utility\Inflector; + // This view is intended to work with dispatch if($vv_action !== 'dispatch') { return; @@ -34,13 +37,21 @@ if($vv_action !== 'dispatch') { // Make the Form fields editable $this->Field->enableFormEditMode(); +// Populate the AutoViewVars. These are the same we do for the EnrollmentAttributes configuration view $this->Petition->populateAutoViewVars(); // We just populated the AutoViewVars. Add them to the current context extract($this->viewVars); foreach($vv_enrollment_attributes as $attr) { + // Do not render if this is not active + if ($attr->status !== StatusEnum::Active) { + continue; + } + $options = []; + $options['default'] = $attr->default_value ?? ''; + // What does this mean??? if(!empty($vv_petition_attributes)) { $curEntity = $vv_petition_attributes->firstMatch(['enrollment_attribute_id' => $attr->id]); @@ -49,17 +60,59 @@ foreach($vv_enrollment_attributes as $attr) { } } + // Get the static configuration of my attribute $supportedAttributes = $this->Petition->getSupportedEnrollmentAttribute($attr->attribute); - + // Construct the field arguments $formArguments = [ // We prefix the attribute ID with a string because Cake seems to sometimes have // problems with field names that are purely integers (even if cast to strings) - 'fieldName' => 'field-' . $attr->id, - 'fieldOptions' => $options, - 'fieldLabel' => $attr->label, - 'fieldType' => $supportedAttributes['fieldType'] + 'fieldName' => 'field-' . $attr->id, + 'fieldLabel' => $attr->label, + 'fieldType' => $supportedAttributes['fieldType'] ]; + // This field is called attribute_type and not attribute_type_id because we want this + // to behave as a hidden value populated by the appropriate select, and we don't want + // Cake to implement foreign key automagic. + $mveaAutoPopulatedVariable = $attr->attribute . 'Types'; + // Check if this mvea model supports types, if it does then render the attribute type + // dropdown list + if ($this->get($mveaAutoPopulatedVariable) !== null) { + $formArguments['fieldType'] = 'select'; + $formArguments['fieldSelectOptions'] = $this->get($mveaAutoPopulatedVariable); + } + + + /* + * Get the values for the attributes ending with _id + * Supported for attributes: group_id, cou_id, affiliation_type_id + */ + if(str_ends_with($attr->attribute, '_id')) { + $suffix = substr($attr->attribute, 0, -3); + $suffix = Inflector::pluralize(Inflector::camelize($suffix)) ; + $defaultValuesPopulated = 'defaultValue' . $suffix; + if ($this->get($defaultValuesPopulated) !== null) { + $formArguments['fieldType'] = 'select'; + $formArguments['fieldSelectOptions'] = $this->get($defaultValuesPopulated); + } + } + + // READ-ONLY + if (isset($attr->modifiable) && !$attr->modifiable) { + $options['readonly'] = true; + } + + // Set the final fieldOptions + $formArguments['fieldOptions'] = $options; + + // HIDDEN Field + // We print directly, we do not delegate to the element for further processing + if ($attr->hidden) { + print $this->Form->hidden($formArguments['fieldName'], ['value' => $options['default']]); + continue; + } + + // Print the element print $this->element('form/listItem', [ 'arguments' => $formArguments ]); From 0f7a64e5c16d373d8d290fddca97f3e589f099a5 Mon Sep 17 00:00:00 2001 From: Ioannis Igoumenos Date: Tue, 22 Oct 2024 17:56:19 +0300 Subject: [PATCH 4/5] Add field description. Add FieldHelper support to Attribute selection view. --- .../AttributeCollectors/dispatch.inc | 39 +++++++++++++--- app/src/View/Helper/FieldHelper.php | 44 ++++++++++++------- 2 files changed, 61 insertions(+), 22 deletions(-) diff --git a/app/plugins/CoreEnroller/templates/AttributeCollectors/dispatch.inc b/app/plugins/CoreEnroller/templates/AttributeCollectors/dispatch.inc index 6aa173939..370253d28 100644 --- a/app/plugins/CoreEnroller/templates/AttributeCollectors/dispatch.inc +++ b/app/plugins/CoreEnroller/templates/AttributeCollectors/dispatch.inc @@ -48,10 +48,26 @@ foreach($vv_enrollment_attributes as $attr) { continue; } + // Get the static configuration of my attribute + $supportedAttributes = $this->Petition->getSupportedEnrollmentAttribute($attr->attribute); + + // Field Options array $options = []; - $options['default'] = $attr->default_value ?? ''; - // What does this mean??? + + // Do we have a default value configured? + // Either a value or an Environmental Variable, + // Each default value is mutually exclusive to the rest. We do not have to worry about a conflict. + $options['default'] = match(true) { + isset($attr->default_value) => $attr->default_value, + isset($attr->default_value_env_name) + && getenv($attr->default_value_env_name) !== false => getenv($attr->default_value_env_name), + isset($attr->default_value_datetime) => $attr->default_value_datetime, + default => '' + }; + + // If we are rerendering the Petition override the default value with whatever + // was previously saved if(!empty($vv_petition_attributes)) { $curEntity = $vv_petition_attributes->firstMatch(['enrollment_attribute_id' => $attr->id]); @@ -60,15 +76,18 @@ foreach($vv_enrollment_attributes as $attr) { } } - // Get the static configuration of my attribute - $supportedAttributes = $this->Petition->getSupportedEnrollmentAttribute($attr->attribute); + // Set the field Label +// $options['label'] = $attr->label; + // Construct the field arguments $formArguments = [ // We prefix the attribute ID with a string because Cake seems to sometimes have // problems with field names that are purely integers (even if cast to strings) - 'fieldName' => 'field-' . $attr->id, - 'fieldLabel' => $attr->label, - 'fieldType' => $supportedAttributes['fieldType'] + 'fieldName' => 'field-' . $attr->id, + 'fieldLabel' => $attr->label, // fieldLabel is only applicable to checkboxes + 'fieldType' => $supportedAttributes['fieldType'], + 'fieldDescription' => $attr->description, + 'fieldNameAlias' => $attr->attribute // the field name to its enrollment attribute field name ]; // This field is called attribute_type and not attribute_type_id because we want this @@ -102,12 +121,18 @@ foreach($vv_enrollment_attributes as $attr) { $options['readonly'] = true; } + // REQUIRED + if (isset($attr->required) && $attr->required) { + $options['required'] = true; + } + // Set the final fieldOptions $formArguments['fieldOptions'] = $options; // HIDDEN Field // We print directly, we do not delegate to the element for further processing if ($attr->hidden) { + // In case this is a hidden field, we need to get only the value print $this->Form->hidden($formArguments['fieldName'], ['value' => $options['default']]); continue; } diff --git a/app/src/View/Helper/FieldHelper.php b/app/src/View/Helper/FieldHelper.php index 0ec64f4d3..27336f0b2 100644 --- a/app/src/View/Helper/FieldHelper.php +++ b/app/src/View/Helper/FieldHelper.php @@ -221,15 +221,15 @@ public function calculateLiClasses(): string * * @param string $fieldName Form field * @param string $dateType Standard, DateOnly, FromTime, ThroughTime - * @param string|null $label + * @param array|null $fieldArgs * * @return string HTML element * @since COmanage Registry v5.0.0 */ public function dateField(string $fieldName, - string $dateType=DateTypeEnum::Standard, - string $label=null): string + string $dateType = DateTypeEnum::Standard, + array $fieldArgs = null): string { // Initialize $dateFormat = $dateType === DateTypeEnum::DateOnly ? 'yyyy-MM-dd' : 'yyyy-MM-dd HH:mm:ss'; @@ -240,6 +240,9 @@ public function dateField(string $fieldName, ? FrozenTime::parse($queryParams[$fieldName]) : $this->getEntity()?->$fieldName; + if($date_object === null && isset($fieldArgs['default'])) { + $date_object = $fieldArgs['default']; + } // Create the options array for the (text input) form control $coptions = []; @@ -247,8 +250,12 @@ public function dateField(string $fieldName, // that will interact with the field value. Allowing direct access to the input field is for // accessibility purposes. - // ACTION VIEW - if($this->action == 'view') { + // ACTION VIEW or Readonly Field + // The latter applies for the attribute collection view + if($this->action == 'view' + || + (isset($fieldArgs['readonly']) && $fieldArgs['readonly']) + ) { // return the date as plaintext $element = $this->getView()->element('form/notSetDiv', [], [ 'cache' => '_html_elements', @@ -264,7 +271,8 @@ public function dateField(string $fieldName, // Special-case the very common "valid_from" and "valid_through" fields, so we won't need // to specify their types in fields.inc. - $pickerType = match ($fieldName) { + $pickerTypeName = $fieldArgs['fieldNameAlias'] ?? $fieldName; + $pickerType = match ($pickerTypeName) { 'valid_from' => DateTypeEnum::FromTime, 'valid_through' => DateTypeEnum::ThroughTime, default => $dateType @@ -273,9 +281,6 @@ public function dateField(string $fieldName, // Append the timezone to the label $coptions['class'] = 'form-control datepicker'; $coptions['placeholder'] = $dateFormat; - if(!empty($label)) { - $coptions['label'] = $label; - } $coptions['pattern'] = $datePattern; $coptions['title'] = __d('field', $dateTitle); @@ -305,8 +310,14 @@ public function dateField(string $fieldName, 'pickerFloor' => $pickerFloor, ]; + $fieldLabel = ''; + if(!empty($fieldArgs['label'])) { + $fieldLabel = $this->Form->label($fieldName, $fieldArgs['label']); + } // Create a text field to hold our value and call the datePicker - return $this->Form->text($fieldName, $coptions) . $this->getView()->element('datePicker', $date_picker_args); + return $fieldLabel // label + . $this->Form->text($fieldName, $coptions) // hidden input + . $this->getView()->element('datePicker', $date_picker_args); // datepicker field } /** @@ -314,11 +325,13 @@ public function dateField(string $fieldName, * * @param string $fieldName Form field * @param array|null $fieldOptions The second parameter of the Form->control helper. List of element options - * @param string|null $fieldLabel Custom label thext + * @param string|null $fieldLabel Custom label text. Applicable to checkboxes ONLY * @param string $fieldPrefix If the field has a specil prefix provide the value * @param string|null $fieldType Field type to override the one calculated from the schema * @param array|null $fieldSelectOptions Options array to override the one calculated options from the AutoPopulate property * fieldType has to be 'select' + * @param string|null $fieldNameAlias Used for the Petition Attribute Collection form. The form uses generic field name. + * The variable is used to map the generic field name to the actual enrollment attribute name * * @return string HTML element * @since COmanage Registry v5.0.0 @@ -328,7 +341,8 @@ public function formField(string $fieldName, string $fieldLabel = null, string $fieldPrefix = '', string $fieldType = null, - array $fieldSelectOptions = null): string + array $fieldSelectOptions = null, + string $fieldNameAlias = null): string { $fieldArgs = $fieldOptions ?? []; $fieldArgs['label'] = $fieldOptions['label'] ?? false; @@ -370,7 +384,7 @@ public function formField(string $fieldName, $this->getView()->set($optionName, $optionValues); } - // Is this a multiple select + // Is this multiple select? $fieldArgs['multiple'] = !empty($fieldOptions['multiple']); // Manipulate the vv_object for the hasPrefix use case @@ -394,9 +408,9 @@ public function formField(string $fieldName, 'class' => 'form-check-input', ]), 'select' => $this->Form->select($fieldName, $fieldSelectOptions, $fieldArgs), - 'date' => $this->dateField($fieldName, DateTypeEnum::DateOnly), + 'date' => $this->dateField(fieldName: $fieldName, dateType: DateTypeEnum::DateOnly, fieldArgs: $fieldArgs), 'datetime', - 'timestamp' => $this->dateField($fieldName), + 'timestamp' => $this->dateField(fieldName: $fieldName, fieldArgs: $fieldArgs), default => $this->Form->control($fieldName, $fieldArgs) }; } From 8387874a68c667d171108439e361332783cc2f0d Mon Sep 17 00:00:00 2001 From: Ioannis Igoumenos Date: Tue, 22 Oct 2024 19:09:52 +0300 Subject: [PATCH 5/5] Fix field sorting.Fix type dropdowns. --- .../AttributeCollectors/dispatch.inc | 22 ++++++------------- app/src/View/Helper/FieldHelper.php | 3 ++- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/app/plugins/CoreEnroller/templates/AttributeCollectors/dispatch.inc b/app/plugins/CoreEnroller/templates/AttributeCollectors/dispatch.inc index 370253d28..a90b43221 100644 --- a/app/plugins/CoreEnroller/templates/AttributeCollectors/dispatch.inc +++ b/app/plugins/CoreEnroller/templates/AttributeCollectors/dispatch.inc @@ -42,7 +42,13 @@ $this->Petition->populateAutoViewVars(); // We just populated the AutoViewVars. Add them to the current context extract($this->viewVars); -foreach($vv_enrollment_attributes as $attr) { +// Sort according to configuration +$vv_enrollment_attributes_sorted = $vv_enrollment_attributes->sortBy(function ($attribute) { + return $attribute->ordr; +}, SORT_ASC)->toList(); + +// Iterate over the attributes and render +foreach($vv_enrollment_attributes_sorted as $attr) { // Do not render if this is not active if ($attr->status !== StatusEnum::Active) { continue; @@ -76,9 +82,6 @@ foreach($vv_enrollment_attributes as $attr) { } } - // Set the field Label -// $options['label'] = $attr->label; - // Construct the field arguments $formArguments = [ // We prefix the attribute ID with a string because Cake seems to sometimes have @@ -90,17 +93,6 @@ foreach($vv_enrollment_attributes as $attr) { 'fieldNameAlias' => $attr->attribute // the field name to its enrollment attribute field name ]; - // This field is called attribute_type and not attribute_type_id because we want this - // to behave as a hidden value populated by the appropriate select, and we don't want - // Cake to implement foreign key automagic. - $mveaAutoPopulatedVariable = $attr->attribute . 'Types'; - // Check if this mvea model supports types, if it does then render the attribute type - // dropdown list - if ($this->get($mveaAutoPopulatedVariable) !== null) { - $formArguments['fieldType'] = 'select'; - $formArguments['fieldSelectOptions'] = $this->get($mveaAutoPopulatedVariable); - } - /* * Get the values for the attributes ending with _id diff --git a/app/src/View/Helper/FieldHelper.php b/app/src/View/Helper/FieldHelper.php index 27336f0b2..03749912d 100644 --- a/app/src/View/Helper/FieldHelper.php +++ b/app/src/View/Helper/FieldHelper.php @@ -240,7 +240,8 @@ public function dateField(string $fieldName, ? FrozenTime::parse($queryParams[$fieldName]) : $this->getEntity()?->$fieldName; - if($date_object === null && isset($fieldArgs['default'])) { + // Petition Attribute Collection use case + if($date_object === null && !empty($fieldArgs['default'])) { $date_object = $fieldArgs['default']; } // Create the options array for the (text input) form control