From 3d2f86c56ea4e053ed133053e9f3c8f902aee27f Mon Sep 17 00:00:00 2001 From: Coleman Watts Date: Sun, 1 May 2022 17:54:54 -0400 Subject: [PATCH] CustomGroup - Add metadata about how a custom group relates to entity types Fixes dev/core#2905 Before: Hard coded, plus a very strange use of the 'description' field to store a callback function. After: The 'grouping' field in the OptionValue for custom extends and custom type is used. APIv4 getfields can then retrieve the necessary options. --- CRM/Core/BAO/CustomGroup.php | 110 ++++++++++++------ CRM/Core/DAO/CustomGroup.php | 5 +- CRM/Core/PseudoConstant.php | 2 +- CRM/Core/SelectValues.php | 15 +-- CRM/Custom/Page/Group.php | 3 +- .../Incremental/sql/5.50.alpha1.mysql.tpl | 21 ++++ Civi/Api4/Utils/CoreUtil.php | 6 - Civi/Api4/Utils/FormattingUtil.php | 8 +- ...tionValue_cg_extends_objects_grant.mgd.php | 24 ++++ .../phpunit/CRM/Core/BAO/CustomGroupTest.php | 14 +++ .../api/v4/Action/BasicCustomFieldTest.php | 9 ++ tests/phpunit/api/v4/Entity/CaseTest.php | 11 ++ xml/schema/Core/CustomGroup.xml | 3 + xml/templates/civicrm_data.tpl | 8 +- 14 files changed, 181 insertions(+), 58 deletions(-) create mode 100644 ext/civigrant/managed/OptionValue_cg_extends_objects_grant.mgd.php diff --git a/CRM/Core/BAO/CustomGroup.php b/CRM/Core/BAO/CustomGroup.php index 1dfa5697e0..1bcb41f893 100644 --- a/CRM/Core/BAO/CustomGroup.php +++ b/CRM/Core/BAO/CustomGroup.php @@ -2070,46 +2070,88 @@ SELECT civicrm_custom_group.id as groupID, civicrm_custom_group.title as groupT } /** - * Get the list of types for objects that a custom group extends to. + * Deprecated function, use APIv4 getFields instead. * + * @deprecated * @param array $types * Var which should have the list appended. - * - * @return array - * Array of types. - * @throws \Exception */ public static function getExtendedObjectTypes(&$types = []) { - static $flag = FALSE, $objTypes = []; - - if (!$flag) { - $extendObjs = []; - CRM_Core_OptionValue::getValues(['name' => 'cg_extend_objects'], $extendObjs, 'weight', TRUE); - - foreach ($extendObjs as $ovId => $ovValues) { - if ($ovValues['description']) { - // description is expected to be a callback func to subtypes - list($callback, $args) = explode(';', trim($ovValues['description'])); - - if (empty($args)) { - $args = []; - } - - if (!is_array($args)) { - throw new CRM_Core_Exception('Arg is not of type array'); - } - - list($className) = explode('::', $callback); - require_once str_replace('_', DIRECTORY_SEPARATOR, $className) . '.php'; - - $objTypes[$ovValues['value']] = call_user_func_array($callback, $args); + $cache = Civi::cache('metadata'); + if (!$cache->has(__FUNCTION__)) { + $objTypes = []; + + $extendObjs = \Civi\Api4\OptionValue::get(FALSE) + ->addSelect('value', 'grouping') + ->addWhere('option_group_id:name', '=', 'cg_extend_objects') + ->addWhere('grouping', 'IS NOT EMPTY') + ->addWhere('is_active', '=', TRUE) + ->execute()->indexBy('value')->column('grouping'); + + foreach ($extendObjs as $entityName => $grouping) { + try { + $objTypes[$entityName] = civicrm_api4($entityName, 'getFields', [ + 'loadOptions' => TRUE, + 'where' => [['name', '=', $grouping]], + 'checkPermissions' => FALSE, + 'select' => ['options'], + ], 0)['options'] ?? NULL; + } + catch (\Civi\API\Exception\NotImplementedException $e) { + // Skip if component is disabled } } - $flag = TRUE; + $cache->set(__FUNCTION__, $objTypes); + } + else { + $objTypes = $cache->get(__FUNCTION__); } $types = array_merge($types, $objTypes); - return $objTypes; + } + + /** + * Loads pseudoconstant option values for the `extends_entity_column_value` field. + * + * @param $context + * @param array $params + * @param array $props + * @return array + */ + public static function getExtendsEntityColumnValueOptions($context, $params, $props = []) { + // Requesting this option list only makes sense if the value of 'extends' is known or can be looked up + if (!empty($props['id']) || !empty($props['name']) || !empty($props['extends'])) { + $id = $props['id'] ?? NULL; + $name = $props['name'] ?? NULL; + $extends = $props['extends'] ?? NULL; + $entityColumnId = $props['extends_entity_column_id'] ?? NULL; + if (!$extends) { + $extends = CRM_Core_DAO::getFieldValue(parent::class, $id ?: $name, 'extends', $id ? 'id' : 'name'); + } + if (!array_key_exists('extends_entity_column_id', $props) && ($id || $name)) { + $entityColumnId = CRM_Core_DAO::getFieldValue(parent::class, $id ?: $name, 'extends_entity_column_id', $id ? 'id' : 'name'); + } + // If there is an entityColumnId (currently only used by Participants) filter by that type. + if ($entityColumnId) { + $pseudoSelectors = CRM_Core_OptionGroup::values('custom_data_type', FALSE, FALSE, FALSE, NULL, 'name'); + $extends = $pseudoSelectors[$entityColumnId]; + } + $allOptions = self::getSubTypes(); + return $allOptions[$extends] ?? []; + } + return []; + } + + /** + * @inheritDoc + */ + public static function buildOptions($fieldName, $context = NULL, $props = []) { + // This is necessary in order to pass $props to the callback function + if ($fieldName === 'extends_entity_column_value') { + $options = self::getExtendsEntityColumnValueOptions($context, [], $props); + return CRM_Core_PseudoConstant::formatArrayOptions($context, $options); + } + return parent::buildOptions($fieldName, $context, $props); } /** @@ -2237,24 +2279,26 @@ SELECT civicrm_custom_group.id as groupID, civicrm_custom_group.title as groupT return [$multipleFieldGroups, $groupTree]; } + /** + * Use APIv4 getFields (or self::getExtendsEntityColumnValueOptions) instead of this beast. + * @deprecated + * @return array + */ public static function getSubTypes(): array { $sel2 = []; $activityType = CRM_Core_PseudoConstant::activityType(FALSE, TRUE, FALSE, 'label', TRUE); $eventType = CRM_Core_OptionGroup::values('event_type'); - $grantType = CRM_Core_OptionGroup::values('grant_type'); $campaignTypes = CRM_Campaign_PseudoConstant::campaignType(); $membershipType = CRM_Member_BAO_MembershipType::getMembershipTypes(FALSE); $participantRole = CRM_Core_OptionGroup::values('participant_role'); asort($activityType); asort($eventType); - asort($grantType); asort($membershipType); asort($participantRole); $sel2['Event'] = $eventType; - $sel2['Grant'] = $grantType; $sel2['Activity'] = $activityType; $sel2['Campaign'] = $campaignTypes; $sel2['Membership'] = $membershipType; diff --git a/CRM/Core/DAO/CustomGroup.php b/CRM/Core/DAO/CustomGroup.php index 55448549ca..ecf8b4d503 100644 --- a/CRM/Core/DAO/CustomGroup.php +++ b/CRM/Core/DAO/CustomGroup.php @@ -6,7 +6,7 @@ * * Generated from xml/schema/CRM/Core/CustomGroup.xml * DO NOT EDIT. Generated by CRM_Core_CodeGen - * (GenCodeChecksum:65d78bdab80228a63775214faab08ee0) + * (GenCodeChecksum:c007e857cfaec7c07a5923125af29474) */ /** @@ -385,6 +385,9 @@ class CRM_Core_DAO_CustomGroup extends CRM_Core_DAO { 'bao' => 'CRM_Core_BAO_CustomGroup', 'localizable' => 0, 'serialize' => self::SERIALIZE_SEPARATOR_BOOKEND, + 'pseudoconstant' => [ + 'callback' => 'CRM_Core_BAO_CustomGroup::getExtendsEntityColumnValueOptions', + ], 'add' => '1.6', ], 'style' => [ diff --git a/CRM/Core/PseudoConstant.php b/CRM/Core/PseudoConstant.php index c24d734fca..ca7189dc31 100644 --- a/CRM/Core/PseudoConstant.php +++ b/CRM/Core/PseudoConstant.php @@ -1598,7 +1598,7 @@ WHERE id = %1 * List of options, each as a record of id+name+label. * Ex: [['id' => 123, 'name' => 'foo_bar', 'label' => 'Foo Bar']] */ - private static function formatArrayOptions($context, array &$options) { + public static function formatArrayOptions($context, array &$options) { // Already flat; return keys/values according to context if (!isset($options[0]) || !is_array($options[0])) { // For validate context, machine names are expected in place of labels. diff --git a/CRM/Core/SelectValues.php b/CRM/Core/SelectValues.php index 6f309fedd5..495786cfcf 100644 --- a/CRM/Core/SelectValues.php +++ b/CRM/Core/SelectValues.php @@ -223,10 +223,11 @@ class CRM_Core_SelectValues { } /** - * Various pre defined extensions for dynamic properties and groups. + * List of all entities that can be extended by custom fields. * - * @return array + * Includes pseudo-entities for Contact and Participant, in order to present sub-types on the form. * + * @return array */ public static function customGroupExtends() { $customGroupExtends = [ @@ -238,17 +239,17 @@ class CRM_Core_SelectValues { 'Membership' => ts('Memberships'), 'Event' => ts('Events'), 'Participant' => ts('Participants'), - 'ParticipantRole' => ts('Participants (Role)'), - 'ParticipantEventName' => ts('Participants (Event Name)'), - 'ParticipantEventType' => ts('Participants (Event Type)'), 'Pledge' => ts('Pledges'), - 'Grant' => ts('Grants'), 'Address' => ts('Addresses'), 'Campaign' => ts('Campaigns'), ]; + // Contact, Individual, $contactTypes = ['Contact' => ts('Contacts')] + self::contactType(); + // ParticipantRole, ParticipantEventName, etc. + $pseudoSelectors = CRM_Core_OptionGroup::values('custom_data_type', FALSE, FALSE, FALSE, NULL, 'label', TRUE, FALSE, 'name'); + // OptionValues provided by extensions $extendObjs = CRM_Core_OptionGroup::values('cg_extend_objects'); - $customGroupExtends = array_merge($contactTypes, $customGroupExtends, $extendObjs); + $customGroupExtends = array_merge($contactTypes, $customGroupExtends, $extendObjs, $pseudoSelectors); return $customGroupExtends; } diff --git a/CRM/Custom/Page/Group.php b/CRM/Custom/Page/Group.php index 919e5260e2..60185a2710 100644 --- a/CRM/Custom/Page/Group.php +++ b/CRM/Custom/Page/Group.php @@ -132,14 +132,13 @@ class CRM_Custom_Page_Group extends CRM_Core_Page { $customGroup[$id]['extends_display'] = $customGroupExtends[$customGroup[$id]['extends']]; } - //fix for Displaying subTypes + // FIXME: This hardcoded array is mostly redundant with CRM_Core_BAO_CustomGroup::getSubTypes $subTypes = []; $subTypes['Activity'] = CRM_Core_PseudoConstant::activityType(FALSE, TRUE, FALSE, 'label', TRUE); $subTypes['Contribution'] = CRM_Contribute_PseudoConstant::financialType(); $subTypes['Membership'] = CRM_Member_BAO_MembershipType::getMembershipTypes(FALSE); $subTypes['Event'] = CRM_Core_OptionGroup::values('event_type'); - $subTypes['Grant'] = CRM_Core_OptionGroup::values('grant_type'); $subTypes['Campaign'] = CRM_Campaign_PseudoConstant::campaignType(); $subTypes['Participant'] = []; $subTypes['ParticipantRole'] = CRM_Core_OptionGroup::values('participant_role'); diff --git a/CRM/Upgrade/Incremental/sql/5.50.alpha1.mysql.tpl b/CRM/Upgrade/Incremental/sql/5.50.alpha1.mysql.tpl index 108aea14b8..096cd60e43 100644 --- a/CRM/Upgrade/Incremental/sql/5.50.alpha1.mysql.tpl +++ b/CRM/Upgrade/Incremental/sql/5.50.alpha1.mysql.tpl @@ -32,3 +32,24 @@ INSERT IGNORE INTO `civicrm_state_province` (`country_id`, `abbreviation`, `name INSERT IGNORE INTO `civicrm_state_province` (`country_id`, `abbreviation`, `name`) VALUES (@country_id, 'SMI', 'Smiths'); INSERT IGNORE INTO `civicrm_state_province` (`country_id`, `abbreviation`, `name`) VALUES (@country_id, 'SOU', 'Southampton'); INSERT IGNORE INTO `civicrm_state_province` (`country_id`, `abbreviation`, `name`) VALUES (@country_id, 'WAR', 'Warwick'); + +SELECT @option_group_id_cgeo := max(id) FROM civicrm_option_group WHERE name = 'cg_extend_objects'; + +UPDATE civicrm_option_value + SET grouping = 'case_type_id', {localize field='description'}description = NULL{/localize} + WHERE option_group_id = @option_group_id_cgeo AND value = 'Case'; + +SELECT @option_group_id_cdt := max(id) FROM civicrm_option_group WHERE name = 'custom_data_type'; + +UPDATE civicrm_option_value + SET {localize field='label'}label = '{ts escape="sql"}Participants (Role){/ts}'{/localize}, grouping = 'role_id' + WHERE option_group_id = @option_group_id_cdt AND name = 'ParticipantRole'; + +UPDATE civicrm_option_value + SET {localize field='label'}label = '{ts escape="sql"}Participants (Event Name){/ts}'{/localize}, grouping = 'event_id' + WHERE option_group_id = @option_group_id_cdt AND name = 'ParticipantEventName'; + +UPDATE civicrm_option_value + SET {localize field='label'}label = '{ts escape="sql"}Participants (Event Type){/ts}'{/localize}, grouping = 'event_id.event_type_id' + WHERE option_group_id = @option_group_id_cdt AND name = 'ParticipantEventType'; + diff --git a/Civi/Api4/Utils/CoreUtil.php b/Civi/Api4/Utils/CoreUtil.php index 8a6d3a25e0..7c54903268 100644 --- a/Civi/Api4/Utils/CoreUtil.php +++ b/Civi/Api4/Utils/CoreUtil.php @@ -123,12 +123,6 @@ class CoreUtil { 'column' => 'id', ]; - case 'Participant': - return [ - 'extends' => ['Participant', 'ParticipantRole', 'ParticipantEventName', 'ParticipantEventType'], - 'column' => 'id', - ]; - case 'RelationshipCache': return [ 'extends' => ['Relationship'], diff --git a/Civi/Api4/Utils/FormattingUtil.php b/Civi/Api4/Utils/FormattingUtil.php index 87ee2d7ae3..e8bdbcb6b7 100644 --- a/Civi/Api4/Utils/FormattingUtil.php +++ b/Civi/Api4/Utils/FormattingUtil.php @@ -192,7 +192,6 @@ class FormattingUtil { * @throws \CRM_Core_Exception */ public static function formatOutputValues(&$results, $fields, $action = 'get', $selectAliases = []) { - $fieldOptions = []; foreach ($results as &$result) { $contactTypePaths = []; foreach ($result as $key => $value) { @@ -211,16 +210,17 @@ class FormattingUtil { } // Evaluate pseudoconstant suffixes $suffix = strrpos($fieldName, ':'); + $fieldOptions = NULL; if ($suffix) { - $fieldOptions[$fieldName] = $fieldOptions[$fieldName] ?? self::getPseudoconstantList($field, $fieldName, $result, $action); + $fieldOptions = self::getPseudoconstantList($field, $fieldName, $result, $action); $dataType = NULL; } if ($fieldExpr->supportsExpansion) { if (!empty($field['serialize']) && is_string($value)) { $value = \CRM_Core_DAO::unSerializeField($value, $field['serialize']); } - if (isset($fieldOptions[$fieldName])) { - $value = self::replacePseudoconstant($fieldOptions[$fieldName], $value); + if (isset($fieldOptions)) { + $value = self::replacePseudoconstant($fieldOptions, $value); } } // Keep track of contact types for self::contactFieldsToRemove diff --git a/ext/civigrant/managed/OptionValue_cg_extends_objects_grant.mgd.php b/ext/civigrant/managed/OptionValue_cg_extends_objects_grant.mgd.php new file mode 100644 index 0000000000..3ce34ab566 --- /dev/null +++ b/ext/civigrant/managed/OptionValue_cg_extends_objects_grant.mgd.php @@ -0,0 +1,24 @@ + 'cg_extend_objects:Grant', + 'entity' => 'OptionValue', + 'cleanup' => 'always', + 'update' => 'always', + 'params' => [ + 'version' => 4, + 'values' => [ + 'option_group_id.name' => 'cg_extend_objects', + 'label' => E::ts('Grants'), + 'value' => 'Grant', + 'name' => 'civicrm_grant', + 'is_reserved' => TRUE, + 'is_active' => TRUE, + 'grouping' => 'grant_type_id', + ], + ], + ], +]; diff --git a/tests/phpunit/CRM/Core/BAO/CustomGroupTest.php b/tests/phpunit/CRM/Core/BAO/CustomGroupTest.php index 7a8fc3c2c2..1f001a412d 100644 --- a/tests/phpunit/CRM/Core/BAO/CustomGroupTest.php +++ b/tests/phpunit/CRM/Core/BAO/CustomGroupTest.php @@ -687,4 +687,18 @@ class CRM_Core_BAO_CustomGroupTest extends CiviUnitTestCase { $this->assertEquals($expectedName, $group->name); } + public function testCustomGroupExtends() { + $extends = \CRM_Core_SelectValues::customGroupExtends(); + $this->assertArrayHasKey('Contribution', $extends); + $this->assertArrayHasKey('Case', $extends); + $this->assertArrayHasKey('Contact', $extends); + $this->assertArrayHasKey('Individual', $extends); + $this->assertArrayHasKey('Household', $extends); + $this->assertArrayHasKey('Organization', $extends); + $this->assertArrayHasKey('Participant', $extends); + $this->assertArrayHasKey('ParticipantRole', $extends); + $this->assertArrayHasKey('ParticipantEventName', $extends); + $this->assertArrayHasKey('ParticipantEventType', $extends); + } + } diff --git a/tests/phpunit/api/v4/Action/BasicCustomFieldTest.php b/tests/phpunit/api/v4/Action/BasicCustomFieldTest.php index 9e4ab6ef40..d773074061 100644 --- a/tests/phpunit/api/v4/Action/BasicCustomFieldTest.php +++ b/tests/phpunit/api/v4/Action/BasicCustomFieldTest.php @@ -519,4 +519,13 @@ class BasicCustomFieldTest extends BaseCustomValueTest { $this->assertEquals('2025-06-11 12:15:30', $contact["$cgName.DateTime"]); } + public function testExtendsMetadata() { + $field = \Civi\Api4\CustomGroup::getFields(FALSE) + ->setLoadOptions(['id', 'name']) + ->addWhere('name', '=', 'extends') + ->execute()->first(); + $options = array_column($field['options'], 'name', 'id'); + $this->assertArrayHasKey('Participant', $options); + } + } diff --git a/tests/phpunit/api/v4/Entity/CaseTest.php b/tests/phpunit/api/v4/Entity/CaseTest.php index b493607ae6..b38f519c67 100644 --- a/tests/phpunit/api/v4/Entity/CaseTest.php +++ b/tests/phpunit/api/v4/Entity/CaseTest.php @@ -69,4 +69,15 @@ class CaseTest extends UnitTestCase { $this->assertEquals($contactID, $relationships[0]['contact_id_a']); } + public function testCgExtendsObjects() { + $field = \Civi\Api4\CustomGroup::getFields(FALSE) + ->setLoadOptions(TRUE) + ->addValue('extends', 'Case') + ->addWhere('name', '=', 'extends_entity_column_value') + ->execute() + ->first(); + + $this->assertContains('Test Case Type', $field['options']); + } + } diff --git a/xml/schema/Core/CustomGroup.xml b/xml/schema/Core/CustomGroup.xml index 7f644b54ab..a2ed26e80f 100644 --- a/xml/schema/Core/CustomGroup.xml +++ b/xml/schema/Core/CustomGroup.xml @@ -82,6 +82,9 @@ 255 linking custom group for dynamic object SEPARATOR_BOOKEND + + CRM_Core_BAO_CustomGroup::getExtendsEntityColumnValueOptions + 1.6 diff --git a/xml/templates/civicrm_data.tpl b/xml/templates/civicrm_data.tpl index 462a10d5c1..88954b1727 100644 --- a/xml/templates/civicrm_data.tpl +++ b/xml/templates/civicrm_data.tpl @@ -685,9 +685,9 @@ VALUES (@option_group_id_pht, '{ts escape="sql"}Voicemail{/ts}' , 5, 'Voicemail' , NULL, 0, 0, 5, NULL, 0, 0, 1, NULL, NULL, NULL), -- custom data types. - (@option_group_id_cdt, 'Participant Role', '1', 'ParticipantRole', NULL, 0, 0, 1, NULL, 0, 0, 1, NULL, NULL , NULL), - (@option_group_id_cdt, 'Participant Event Name', '2', 'ParticipantEventName', NULL, 0, 0, 2, NULL, 0, 0, 1, NULL, NULL , NULL), - (@option_group_id_cdt, 'Participant Event Type', '3', 'ParticipantEventType', NULL, 0, 0, 3, NULL, 0, 0, 1, NULL, NULL , NULL), + (@option_group_id_cdt, '{ts escape="sql"}Participants (Role){/ts}', '1', 'ParticipantRole', 'role_id', 0, 0, 1, NULL, 0, 0, 1, NULL, NULL , NULL), + (@option_group_id_cdt, '{ts escape="sql"}Participants (Event Name){/ts}', '2', 'ParticipantEventName', 'event_id', 0, 0, 2, NULL, 0, 0, 1, NULL, NULL , NULL), + (@option_group_id_cdt, '{ts escape="sql"}Participants (Event Type){/ts}', '3', 'ParticipantEventType', 'event_id.event_type_id', 0, 0, 3, NULL, 0, 0, 1, NULL, NULL , NULL), -- visibility. (@option_group_id_vis, 'Public', 1, 'public', NULL, 0, 0, 1, NULL, 0, 0, 1, NULL, NULL , NULL), @@ -1097,7 +1097,7 @@ VALUES -- custom group objects (@option_group_id_cgeo, '{ts escape="sql"}Survey{/ts}', 'Survey', 'civicrm_survey', NULL, 0, 0, 1, NULL, 0, 0, 1, NULL, NULL, NULL), - (@option_group_id_cgeo, '{ts escape="sql"}Cases{/ts}', 'Case', 'civicrm_case', NULL, 0, 0, 2, 'CRM_Case_PseudoConstant::caseType;', 0, 0, 1, NULL, NULL, NULL); + (@option_group_id_cgeo, '{ts escape="sql"}Cases{/ts}', 'Case', 'civicrm_case', 'case_type_id', 0, 0, 2, NULL, 0, 0, 1, NULL, NULL, NULL); -- CRM-6138 {include file='languages.tpl'} -- 2.25.1