From a61d2ab7ce6d0fc069f0af7a0c1edd3d6c3054c2 Mon Sep 17 00:00:00 2001 From: Coleman Watts Date: Fri, 6 Nov 2020 16:58:08 -0500 Subject: [PATCH] Add support for multi-value contact reference custom fields --- CRM/Core/BAO/CustomField.php | 60 +++++++++---------- CRM/Core/BAO/CustomGroup.php | 22 ++++--- CRM/Core/BAO/CustomQuery.php | 7 +-- CRM/Core/BAO/CustomValueTable.php | 12 +++- CRM/Core/Form/Renderer.php | 32 ++++++---- CRM/Custom/Form/Field.php | 4 +- .../Contact/Page/View/CustomDataFieldView.tpl | 4 +- .../CRM/Custom/Form/ContactReference.tpl | 1 + templates/CRM/Custom/Form/Field.tpl | 2 +- templates/CRM/Custom/Page/CustomDataView.tpl | 20 ++----- tests/phpunit/CRM/Custom/Form/FieldTest.php | 2 +- .../api/v4/Action/CustomFieldAlterTest.php | 18 ++++++ 12 files changed, 108 insertions(+), 76 deletions(-) diff --git a/CRM/Core/BAO/CustomField.php b/CRM/Core/BAO/CustomField.php index f951f3fce329..fd0ee4ffbaf1 100644 --- a/CRM/Core/BAO/CustomField.php +++ b/CRM/Core/BAO/CustomField.php @@ -889,6 +889,9 @@ public static function addQuickFormElement( } $fieldAttributes['class'] = ltrim(($fieldAttributes['class'] ?? '') . ' crm-form-contact-reference huge'); $fieldAttributes['data-api-entity'] = 'Contact'; + if (!empty($field->serialize) || $search) { + $fieldAttributes['multiple'] = TRUE; + } $element = $qf->add('text', $elementName, $label, $fieldAttributes, $useRequired && !$search); $urlParams = "context=customfield&id={$field->id}"; @@ -1071,13 +1074,15 @@ private static function formatDisplayValue($value, $field, $entityId = NULL) { case 'Autocomplete-Select': case 'Radio': case 'CheckBox': - if ($field['data_type'] == 'ContactReference' && $value) { - if (is_numeric($value)) { - $display = CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_Contact', $value, 'display_name'); - } - else { - $display = $value; + if ($field['data_type'] == 'ContactReference' && (is_array($value) || is_numeric($value))) { + $displayNames = []; + foreach ((array) $value as $contactId) { + $displayNames[] = CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_Contact', $contactId, 'display_name'); } + $display = implode(', ', $displayNames); + } + elseif ($field['data_type'] == 'ContactReference') { + $display = $value; } elseif (is_array($value)) { $v = []; @@ -1707,7 +1712,7 @@ public static function createField($field, $operation) { /** * @param CRM_Core_DAO_CustomField $field - * @param string $operation + * @param string $operation add|modify|delete * * @return bool */ @@ -2653,7 +2658,7 @@ private static function getOptionsForField(&$field, $optionGroupName) { /** * @param CRM_Core_DAO_CustomField $field - * @param 'add|modify' $operation + * @param 'add|modify|delete' $operation * * @return array */ @@ -2679,29 +2684,24 @@ protected static function prepareCreateParams($field, $operation) { // For adding/dropping FK constraints $params['fkName'] = CRM_Core_BAO_SchemaHandler::getIndexName($tableName, $field->column_name); - if ($field->data_type == 'Country' && !self::isSerialized($field)) { - $params['fk_table_name'] = 'civicrm_country'; - $params['fk_field_name'] = 'id'; - $params['fk_attributes'] = 'ON DELETE SET NULL'; - } - elseif ($field->data_type == 'StateProvince' && !self::isSerialized($field)) { - $params['fk_table_name'] = 'civicrm_state_province'; - $params['fk_field_name'] = 'id'; - $params['fk_attributes'] = 'ON DELETE SET NULL'; - } - elseif ($field->data_type == 'StateProvince' || $field->data_type == 'Country') { - $params['type'] = 'varchar(255)'; - } - elseif ($field->data_type == 'File') { - $params['fk_table_name'] = 'civicrm_file'; - $params['fk_field_name'] = 'id'; - $params['fk_attributes'] = 'ON DELETE SET NULL'; - } - elseif ($field->data_type == 'ContactReference') { - $params['fk_table_name'] = 'civicrm_contact'; - $params['fk_field_name'] = 'id'; - $params['fk_attributes'] = 'ON DELETE SET NULL'; + $fkFields = [ + 'Country' => 'civicrm_country', + 'StateProvince' => 'civicrm_state_province', + 'ContactReference' => 'civicrm_contact', + 'File' => 'civicrm_file', + ]; + if (isset($fkFields[$field->data_type])) { + // Serialized fields store value-separated strings which are incompatible with FK constraints + if ($field->serialize) { + $params['type'] = 'varchar(255)'; + } + else { + $params['fk_table_name'] = $fkFields[$field->data_type]; + $params['fk_field_name'] = 'id'; + $params['fk_attributes'] = 'ON DELETE SET NULL'; + } } + if (isset($field->default_value)) { $params['default'] = "'{$field->default_value}'"; } diff --git a/CRM/Core/BAO/CustomGroup.php b/CRM/Core/BAO/CustomGroup.php index 52a96839fb92..61da4970e4f8 100644 --- a/CRM/Core/BAO/CustomGroup.php +++ b/CRM/Core/BAO/CustomGroup.php @@ -1288,7 +1288,7 @@ public static function setDefaults(&$groupTree, &$defaults, $viewMode = FALSE, $ $serialize = CRM_Core_BAO_CustomField::isSerialized($field); if ($serialize) { - if ($field['data_type'] != 'Country' && $field['data_type'] != 'StateProvince') { + if ($field['data_type'] != 'Country' && $field['data_type'] != 'StateProvince' && $field['data_type'] != 'ContactReference') { $defaults[$elementName] = []; $customOption = CRM_Core_BAO_CustomOption::getCustomOption($field['id'], $inactiveNeeded); if ($viewMode) { @@ -1885,13 +1885,19 @@ public static function buildCustomDataView(&$form, &$groupTree, $returnCount = F $details[$groupID][$values['id']]['editable'] = TRUE; } // also return contact reference contact id if user has view all or edit all contacts perm - if ((CRM_Core_Permission::check('view all contacts') || - CRM_Core_Permission::check('edit all contacts')) - && - $details[$groupID][$values['id']]['fields'][$k]['field_data_type'] == - 'ContactReference' + if ($details[$groupID][$values['id']]['fields'][$k]['field_data_type'] === 'ContactReference' + && CRM_Core_Permission::check([['view all contacts', 'edit all contacts']]) ) { - $details[$groupID][$values['id']]['fields'][$k]['contact_ref_id'] = $values['data'] ?? NULL; + $details[$groupID][$values['id']]['fields'][$k]['contact_ref_links'] = []; + $path = CRM_Contact_DAO_Contact::getEntityPaths()['view']; + foreach (CRM_Utils_Array::explodePadded($values['data'] ?? []) as $contactId) { + $displayName = CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_Contact', $contactId, 'display_name'); + if ($displayName) { + $url = CRM_Utils_System::url(str_replace('[id]', $contactId, $path)); + $details[$groupID][$values['id']]['fields'][$k]['contact_ref_links'][] = '' . + $displayName . ''; + } + } } } } @@ -1903,7 +1909,7 @@ public static function buildCustomDataView(&$form, &$groupTree, $returnCount = F $details[$groupID][0]['collapse_display'] = $group['collapse_display'] ?? NULL; $details[$groupID][0]['collapse_adv_display'] = $group['collapse_adv_display'] ?? NULL; $details[$groupID][0]['style'] = $group['style'] ?? NULL; - $details[$groupID][0]['fields'][$k] = ['field_title' => CRM_Utils_Array::value('label', $properties)]; + $details[$groupID][0]['fields'][$k] = ['field_title' => $properties['label'] ?? NULL]; } } } diff --git a/CRM/Core/BAO/CustomQuery.php b/CRM/Core/BAO/CustomQuery.php index 6babf1fd73b6..92093305daf0 100644 --- a/CRM/Core/BAO/CustomQuery.php +++ b/CRM/Core/BAO/CustomQuery.php @@ -215,6 +215,7 @@ public function where() { case 'String': case 'StateProvince': case 'Country': + case 'ContactReference': if ($field['is_search_range'] && is_array($value)) { //didn't found any field under any of these three data-types as searchable by range @@ -278,12 +279,6 @@ public function where() { } break; - case 'ContactReference': - $label = $value ? CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_Contact', $value, 'sort_name') : ''; - $this->_where[$grouping][] = CRM_Contact_BAO_Query::buildClause($fieldName, $op, $value, 'String'); - $this->_qill[$grouping][] = $field['label'] . " $qillOp $label"; - break; - case 'Int': $this->_where[$grouping][] = CRM_Contact_BAO_Query::buildClause($fieldName, $op, $value, 'Integer'); $this->_qill[$grouping][] = ts("%1 %2 %3", [1 => $field['label'], 2 => $qillOp, 3 => $qillValue]); diff --git a/CRM/Core/BAO/CustomValueTable.php b/CRM/Core/BAO/CustomValueTable.php index fa3396a3b3aa..8a3b8c228ab7 100644 --- a/CRM/Core/BAO/CustomValueTable.php +++ b/CRM/Core/BAO/CustomValueTable.php @@ -34,6 +34,7 @@ public static function create($customParams, $parentOperation = NULL) { } $paramFieldsExtendContactForEntities = []; + $VS = CRM_Core_DAO::VALUE_SEPARATOR; foreach ($customParams as $tableName => $tables) { foreach ($tables as $index => $fields) { @@ -187,8 +188,17 @@ public static function create($customParams, $parentOperation = NULL) { break; case 'ContactReference': - if ($value == NULL) { + if ($value == NULL || $value === '' || $value === $VS . $VS) { $type = 'Timestamp'; + $value = NULL; + } + elseif (strpos($value, $VS) !== FALSE) { + $type = 'String'; + // Validate the string contains only integers and value-separators + $validChars = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, $VS]; + if (str_replace($validChars, '', $value)) { + throw new CRM_Core_Exception('Contact ID must be of type Integer'); + } } else { $type = 'Integer'; diff --git a/CRM/Core/Form/Renderer.php b/CRM/Core/Form/Renderer.php index 8b224a98c20e..c959a235cd4d 100644 --- a/CRM/Core/Form/Renderer.php +++ b/CRM/Core/Form/Renderer.php @@ -334,7 +334,9 @@ public function renderFrozenEntityRef(&$el, $field) { */ public static function preprocessContactReference($field) { $val = $field->getValue(); - if ($val && is_numeric($val)) { + $multiple = $field->getAttribute('multiple'); + $data = []; + if ($val) { $list = array_keys(CRM_Core_BAO_Setting::valueOptions(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME, 'contact_reference_options' @@ -342,21 +344,29 @@ public static function preprocessContactReference($field) { $return = array_unique(array_merge(['sort_name'], $list)); - $contact = civicrm_api('contact', 'getsingle', ['id' => $val, 'return' => $return, 'version' => 3]); + $cids = is_array($val) ? $val : explode(',', $val); - if (!empty($contact['id'])) { - $view = []; - foreach ($return as $fld) { - if (!empty($contact[$fld])) { - $view[] = $contact[$fld]; + foreach ($cids as $cid) { + $contact = civicrm_api('contact', 'getsingle', ['id' => $cid, 'return' => $return, 'version' => 3]); + if (!empty($contact['id'])) { + $view = []; + foreach ($return as $fld) { + if (!empty($contact[$fld])) { + $view[] = $contact[$fld]; + } } + $data[] = [ + 'id' => $contact['id'], + 'text' => implode(' :: ', $view), + ]; } - $field->setAttribute('data-entity-value', json_encode([ - 'id' => $contact['id'], - 'text' => implode(' :: ', $view), - ])); } } + + if ($data) { + $field->setAttribute('data-entity-value', json_encode($multiple ? $data : $data[0])); + $field->setValue(implode(',', $cids)); + } } /** diff --git a/CRM/Custom/Form/Field.php b/CRM/Custom/Form/Field.php index 8200f446f85b..f2ed93cbbf17 100644 --- a/CRM/Custom/Form/Field.php +++ b/CRM/Custom/Form/Field.php @@ -740,7 +740,7 @@ public static function formRule($fields, $files, $self) { } } elseif (in_array($htmlType, self::$htmlTypesWithOptions) && - !in_array($dataType, ['Boolean', 'Country', 'StateProvince']) + !in_array($dataType, ['Boolean', 'Country', 'StateProvince', 'ContactReference']) ) { if (!$fields['option_group_id']) { $errors['option_group_id'] = ts('You must select a Multiple Choice Option set if you chose Reuse an existing set.'); @@ -956,7 +956,7 @@ public function getDefaultEntity() { * The serialize type - CRM_Core_DAO::SERIALIZE_XXX or the string 'null' */ public function determineSerializeType($params) { - if ($params['data_type'] !== 'ContactReference' && ($params['html_type'] === 'Select' || $params['html_type'] === 'Autocomplete-Select')) { + if ($params['html_type'] === 'Select' || $params['html_type'] === 'Autocomplete-Select') { return !empty($params['serialize']) ? CRM_Core_DAO::SERIALIZE_SEPARATOR_BOOKEND : 'null'; } else { diff --git a/templates/CRM/Contact/Page/View/CustomDataFieldView.tpl b/templates/CRM/Contact/Page/View/CustomDataFieldView.tpl index e9042f21574d..0ddd19d6aa12 100644 --- a/templates/CRM/Contact/Page/View/CustomDataFieldView.tpl +++ b/templates/CRM/Contact/Page/View/CustomDataFieldView.tpl @@ -27,10 +27,10 @@ {else}
{$element.field_title}
- {if $element.field_data_type EQ 'ContactReference' && $element.contact_ref_id} + {if $element.field_data_type EQ 'ContactReference' && $element.contact_ref_links} {*Contact ref id passed if user has sufficient permissions - so make a link.*}
- {$element.field_value} + {', '|implode:$element.contact_ref_links}
{elseif $element.field_data_type EQ 'Money'}
{$element.field_value|crmMoney}
diff --git a/templates/CRM/Custom/Form/ContactReference.tpl b/templates/CRM/Custom/Form/ContactReference.tpl index d318a1014913..d754c10d8250 100644 --- a/templates/CRM/Custom/Form/ContactReference.tpl +++ b/templates/CRM/Custom/Form/ContactReference.tpl @@ -18,6 +18,7 @@ $field.crmSelect2({ placeholder: {/literal}'{ts escape="js"}- select contact -{/ts}'{literal}, minimumInputLength: 1, + multiple: !!$field.attr('multiple'), ajax: { url: {/literal}"{$customUrls.$element_name}"{literal}, quietMillis: 300, diff --git a/templates/CRM/Custom/Form/Field.tpl b/templates/CRM/Custom/Form/Field.tpl index 75c9fed24713..02f4ce7b0236 100644 --- a/templates/CRM/Custom/Form/Field.tpl +++ b/templates/CRM/Custom/Form/Field.tpl @@ -277,7 +277,7 @@ $("#noteColumns, #noteRows, #noteLength", $form).toggle(dataType === 'Memo'); - $(".crm-custom-field-form-block-serialize", $form).toggle((htmlType === 'Select' || htmlType === 'Autocomplete-Select') && dataType !== 'ContactReference'); + $(".crm-custom-field-form-block-serialize", $form).toggle(htmlType === 'Select' || htmlType === 'Autocomplete-Select'); } function makeDefaultValueField(dataType) { diff --git a/templates/CRM/Custom/Page/CustomDataView.tpl b/templates/CRM/Custom/Page/CustomDataView.tpl index fafb2fd82d1a..f7f3f870031e 100644 --- a/templates/CRM/Custom/Page/CustomDataView.tpl +++ b/templates/CRM/Custom/Page/CustomDataView.tpl @@ -69,17 +69,13 @@ {/if} {else} - {if $element.contact_ref_id} - - {/if} - {if $element.field_data_type == 'Memo'} + {if $element.field_data_type EQ 'ContactReference' && $element.contact_ref_links} + {', '|implode:$element.contact_ref_links} + {elseif $element.field_data_type == 'Memo'} {$element.field_value|nl2br} {else} {$element.field_value} {/if} - {if $element.contact_ref_id} - - {/if} {/if} {/if} @@ -126,17 +122,13 @@ {/if} {else}
- {if $element.contact_ref_id} - - {/if} - {if $element.field_data_type == 'Memo'} + {if $element.field_data_type EQ 'ContactReference' && $element.contact_ref_links} + {', '|implode:$element.contact_ref_links} + {elseif $element.field_data_type == 'Memo'} {if $element.field_value}{$element.field_value|nl2br}{else}
{/if} {else} {if $element.field_value}{$element.field_value} {else}
{/if} {/if} - {if $element.contact_ref_id} -
- {/if}
{/if} {/if} diff --git a/tests/phpunit/CRM/Custom/Form/FieldTest.php b/tests/phpunit/CRM/Custom/Form/FieldTest.php index 0af0ec280452..c694ea6d57ae 100644 --- a/tests/phpunit/CRM/Custom/Form/FieldTest.php +++ b/tests/phpunit/CRM/Custom/Form/FieldTest.php @@ -372,7 +372,7 @@ public function serializeDataProvider():array { 'html_type' => 'Autocomplete-Select', 'serialize' => '1', ], - 'null', + CRM_Core_DAO::SERIALIZE_SEPARATOR_BOOKEND, ], ]; } diff --git a/tests/phpunit/api/v4/Action/CustomFieldAlterTest.php b/tests/phpunit/api/v4/Action/CustomFieldAlterTest.php index 689ef37ba556..1e58fc0b1755 100644 --- a/tests/phpunit/api/v4/Action/CustomFieldAlterTest.php +++ b/tests/phpunit/api/v4/Action/CustomFieldAlterTest.php @@ -57,6 +57,14 @@ public function testChangeSerialize() { ->addValue('data_type', 'Country') ->addValue('html_type', 'Select'), 0 ) + ->addChain('field4', CustomField::create() + ->addValue('custom_group_id', '$id') + ->addValue('serialize', TRUE) + ->addValue('is_required', TRUE) + ->addValue('label', 'TestContact') + ->addValue('data_type', 'ContactReference') + ->addValue('html_type', 'Autocomplete-Select'), 0 + ) ->execute() ->first(); @@ -144,6 +152,16 @@ public function testChangeSerialize() { $this->assertCount(1, $result); // The two values originally entered will now be one value $this->assertEquals([1228], $result['A4']['MyFieldsToAlter.TestCountry']); + + // Repeatedly change contact ref field to ensure FK index is correctly added/dropped with no SQL error + for ($i = 1; $i < 6; ++$i) { + CustomField::update(FALSE) + ->addWhere('id', '=', $customGroup['field4']['id']) + ->addValue('serialize', $i % 2 == 0) + ->addValue('is_required', $i % 2 == 0) + ->execute(); + } + } }