Skip to content

Commit

Permalink
Add support for multi-value contact reference custom fields
Browse files Browse the repository at this point in the history
  • Loading branch information
colemanw committed Dec 3, 2020
1 parent 6874397 commit a61d2ab
Show file tree
Hide file tree
Showing 12 changed files with 108 additions and 76 deletions.
60 changes: 30 additions & 30 deletions CRM/Core/BAO/CustomField.php
Original file line number Diff line number Diff line change
Expand Up @@ -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}";
Expand Down Expand Up @@ -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');

This comment has been minimized.

Copy link
@webmaster-cses-org-uk

webmaster-cses-org-uk Nov 3, 2021

Contributor

This change removes the check that $value is not empty before processing.

Passing an empty value to to CRM_Core_DAO::getFieldValue() causes a "getFieldValue failed" error, which occurs when submitting a record with custom data containing an empty multi-valued contact field.

Will raise issue and PR to fix.

}
$display = implode(', ', $displayNames);
}
elseif ($field['data_type'] == 'ContactReference') {
$display = $value;
}
elseif (is_array($value)) {
$v = [];
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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
*/
Expand All @@ -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}'";
}
Expand Down
22 changes: 14 additions & 8 deletions CRM/Core/BAO/CustomGroup.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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'][] = '<a href="' . $url . '" title="' . htmlspecialchars(ts('View Contact')) . '">' .
$displayName . '</a>';
}
}
}
}
}
Expand All @@ -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];
}
}
}
Expand Down
7 changes: 1 addition & 6 deletions CRM/Core/BAO/CustomQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]);
Expand Down
12 changes: 11 additions & 1 deletion CRM/Core/BAO/CustomValueTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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';
Expand Down
32 changes: 21 additions & 11 deletions CRM/Core/Form/Renderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -334,29 +334,39 @@ 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'
), '1');

$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));
}
}

/**
Expand Down
4 changes: 2 additions & 2 deletions CRM/Custom/Form/Field.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.');
Expand Down Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions templates/CRM/Contact/Page/View/CustomDataFieldView.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@
</div>
{else}
<div class="crm-label">{$element.field_title}</div>
{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.*}
<div class="crm-content crm-custom-data crm-contact-reference">
<a href="{crmURL p='civicrm/contact/view' q="reset=1&cid=`$element.contact_ref_id`"}" title="view contact">{$element.field_value}</a>
{', '|implode:$element.contact_ref_links}
</div>
{elseif $element.field_data_type EQ 'Money'}
<div class="crm-content crm-custom-data">{$element.field_value|crmMoney}</div>
Expand Down
1 change: 1 addition & 0 deletions templates/CRM/Custom/Form/ContactReference.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion templates/CRM/Custom/Form/Field.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
20 changes: 6 additions & 14 deletions templates/CRM/Custom/Page/CustomDataView.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -69,17 +69,13 @@
{/if}
{else}
<td class="html-adjust">
{if $element.contact_ref_id}
<a href='{crmURL p="civicrm/contact/view" q="reset=1&cid=`$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}
</a>
{/if}
</td>
{/if}
{/if}
Expand Down Expand Up @@ -126,17 +122,13 @@
{/if}
{else}
<div class="content">
{if $element.contact_ref_id}
<a href='{crmURL p="civicrm/contact/view" q="reset=1&cid=`$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}<br/>{/if}
{else}
{if $element.field_value}{$element.field_value} {else}<br/>{/if}
{/if}
{if $element.contact_ref_id}
</a>
{/if}
</div>
{/if}
{/if}
Expand Down
2 changes: 1 addition & 1 deletion tests/phpunit/CRM/Custom/Form/FieldTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ public function serializeDataProvider():array {
'html_type' => 'Autocomplete-Select',
'serialize' => '1',
],
'null',
CRM_Core_DAO::SERIALIZE_SEPARATOR_BOOKEND,
],
];
}
Expand Down
18 changes: 18 additions & 0 deletions tests/phpunit/api/v4/Action/CustomFieldAlterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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();
}

}

}

0 comments on commit a61d2ab

Please sign in to comment.