From 4c7e5001c8e518131fe7b45f0e05259079eb16a4 Mon Sep 17 00:00:00 2001 From: Patrick Figel Date: Wed, 7 Aug 2019 16:38:59 +0200 Subject: [PATCH] dev/core#1109 - Fix merge not updating ContactReference fields This fixes an issue that causes ContactReference custom fields pointing to a duplicated contact that's being merged to not be updated to the main (surviving) contact. --- CRM/Core/DAO.php | 22 ++++++- CRM/Dedupe/Merger.php | 14 ++++- tests/phpunit/CRM/Dedupe/MergerTest.php | 80 ++++++++++++++++++++++++- 3 files changed, 110 insertions(+), 6 deletions(-) diff --git a/CRM/Core/DAO.php b/CRM/Core/DAO.php index 5eb4713b8f..c1e44c0d64 100644 --- a/CRM/Core/DAO.php +++ b/CRM/Core/DAO.php @@ -2375,6 +2375,7 @@ SELECT contact_id } } self::appendCustomTablesExtendingContacts($contactReferences); + self::appendCustomContactReferenceFields($contactReferences); // FixME for time being adding below line statically as no Foreign key constraint defined for table 'civicrm_entity_tag' $contactReferences['civicrm_entity_tag'][] = 'entity_id'; @@ -2398,7 +2399,26 @@ SELECT contact_id $customValueTables = CRM_Core_BAO_CustomGroup::getAllCustomGroupsByBaseEntity('Contact'); $customValueTables->find(); while ($customValueTables->fetch()) { - $cidRefs[$customValueTables->table_name] = ['entity_id']; + $cidRefs[$customValueTables->table_name][] = 'entity_id'; + } + } + + /** + * Add custom ContactReference fields to the list of contact references + * + * This includes active and inactive fields/groups + * + * @param array $cidRefs + * + * @throws \CiviCRM_API3_Exception + */ + public static function appendCustomContactReferenceFields(&$cidRefs) { + $fields = civicrm_api3('CustomField', 'get', [ + 'return' => ['column_name', 'custom_group_id.table_name'], + 'data_type' => 'ContactReference', + ])['values']; + foreach ($fields as $field) { + $cidRefs[$field['custom_group_id.table_name']][] = $field['column_name']; } } diff --git a/CRM/Dedupe/Merger.php b/CRM/Dedupe/Merger.php index 42507b9e7d..9e501f35f6 100644 --- a/CRM/Dedupe/Merger.php +++ b/CRM/Dedupe/Merger.php @@ -504,6 +504,7 @@ INNER JOIN civicrm_membership membership2 ON membership1.membership_type_id = m if ($customTableToCopyFrom !== NULL) { // @todo this duplicates cidRefs? CRM_Core_DAO::appendCustomTablesExtendingContacts($customTables); + CRM_Core_DAO::appendCustomContactReferenceFields($customTables); $customTables = array_keys($customTables); } @@ -529,7 +530,10 @@ INNER JOIN civicrm_membership membership2 ON membership1.membership_type_id = m // skipping non selected single-value custom table's value migration if (!in_array($table, $multi_value_tables)) { if ($customTableToCopyFrom !== NULL && in_array($table, $customTables) && !in_array($table, $customTableToCopyFrom)) { - continue; + if (isset($cidRefs[$table]) && ($delCol = array_search('entity_id', $cidRefs[$table])) !== FALSE) { + // remove entity_id from the field list + unset($cidRefs[$table][$delCol]); + } } } @@ -560,7 +564,13 @@ INNER JOIN civicrm_membership membership2 ON membership1.membership_type_id = m $preOperationSqls = self::operationSql($mainId, $otherId, $table, $tableOperations); $sqls = array_merge($sqls, $preOperationSqls); - if ($customTableToCopyFrom !== NULL && in_array($table, $customTableToCopyFrom) && !self::customRecordExists($mainId, $table, $field)) { + if ($customTableToCopyFrom !== NULL && in_array($table, $customTableToCopyFrom) && !self::customRecordExists($mainId, $table, $field) && $field == 'entity_id') { + // this is the entity_id column of a custom field group where: + // - the custom table should be copied as indicated by $customTableToCopyFrom + // e.g. because a field in the group was selected in a form + // - AND no record exists yet for the $mainId contact + // we only do this for column "entity_id" as we wouldn't want to + // run this INSERT for ContactReference fields $sqls[] = "INSERT INTO $table ($field) VALUES ($mainId)"; } $sqls[] = "UPDATE IGNORE $table SET $field = $mainId WHERE $field = $otherId"; diff --git a/tests/phpunit/CRM/Dedupe/MergerTest.php b/tests/phpunit/CRM/Dedupe/MergerTest.php index be93391b97..a5e3c8c67a 100644 --- a/tests/phpunit/CRM/Dedupe/MergerTest.php +++ b/tests/phpunit/CRM/Dedupe/MergerTest.php @@ -800,6 +800,67 @@ class CRM_Dedupe_MergerTest extends CiviUnitTestCase { $this->callAPISuccess('CustomGroup', 'delete', ['id' => $multiGroup['id']]); } + /** + * Test that ContactReference fields are updated to point to the main contact + * after a merge is performed and the duplicate contact is deleted. + */ + public function testMigrationOfContactReferenceCustomField() { + // Create Custom Fields + $contactGroup = $this->setupCustomGroupForIndividual(); + $activityGroup = $this->customGroupCreate([ + 'name' => 'test_group_activity', + 'extends' => 'Activity', + ]); + $refFieldContact = $this->customFieldCreate([ + 'custom_group_id' => $contactGroup['id'], + 'label' => 'field_1' . $contactGroup['id'], + 'data_type' => 'ContactReference', + 'default_value' => NULL, + ]); + $refFieldActivity = $this->customFieldCreate([ + 'custom_group_id' => $activityGroup['id'], + 'label' => 'field_1' . $activityGroup['id'], + 'data_type' => 'ContactReference', + 'default_value' => NULL, + ]); + + // Contacts setup + $this->setupMatchData(); + $originalContactID = $this->contacts[0]['id']; + $duplicateContactID = $this->contacts[1]['id']; + + // create a contact that won't be merged but has a ContactReference field + // pointing to the duplicate (to be deleted) contact + $unrelatedContact = $this->individualCreate([ + 'first_name' => 'Unrelated', + 'first_name' => 'Contact', + 'email' => 'unrelated@example.com', + "custom_{$refFieldContact['id']}" => $duplicateContactID, + ]); + // also create an activity with a ContactReference custom field + $activity = $this->activityCreate([ + 'target_contact_id' => $unrelatedContact, + "custom_{$refFieldActivity['id']}" => $duplicateContactID, + ]); + + // verify that the fields were set + $this->assertCustomFieldValue($unrelatedContact, $duplicateContactID, "custom_{$refFieldContact['id']}"); + $this->assertEntityCustomFieldValue('Activity', $activity['id'], $duplicateContactID, "custom_{$refFieldActivity['id']}_id"); + + // Perform merge + $this->mergeContacts($originalContactID, $duplicateContactID, []); + + // verify that the ContactReference fields were updated to point to the surviving contact post-merge + $this->assertCustomFieldValue($unrelatedContact, $originalContactID, "custom_{$refFieldContact['id']}"); + $this->assertEntityCustomFieldValue('Activity', $activity['id'], $originalContactID, "custom_{$refFieldActivity['id']}_id"); + + // cleanup created custom set + $this->callAPISuccess('CustomField', 'delete', ['id' => $refFieldContact['id']]); + $this->callAPISuccess('CustomGroup', 'delete', ['id' => $contactGroup['id']]); + $this->callAPISuccess('CustomField', 'delete', ['id' => $refFieldActivity['id']]); + $this->callAPISuccess('CustomGroup', 'delete', ['id' => $activityGroup['id']]); + } + /** * Calls merge method on given contacts, with values given in $params array. * @@ -835,8 +896,21 @@ class CRM_Dedupe_MergerTest extends CiviUnitTestCase { * @param $customFieldName */ private function assertCustomFieldValue($contactID, $expectedValue, $customFieldName) { - $data = $this->callAPISuccess('Contact', 'getsingle', [ - 'id' => $contactID, + $this->assertEntityCustomFieldValue('Contact', $contactID, $expectedValue, $customFieldName); + } + + /** + * Check if the custom field of the given field and entity id matches the + * expected value + * + * @param $entity + * @param $id + * @param $expectedValue + * @param $customFieldName + */ + private function assertEntityCustomFieldValue($entity, $id, $expectedValue, $customFieldName) { + $data = $this->callAPISuccess($entity, 'getsingle', [ + 'id' => $id, 'return' => [$customFieldName], ]); @@ -918,7 +992,7 @@ class CRM_Dedupe_MergerTest extends CiviUnitTestCase { foreach ($fixtures as $fixture) { $contactID = $this->individualCreate($fixture); $this->contacts[] = array_merge($fixture, ['id' => $contactID]); - sleep(5); + sleep(2); } $organizationFixtures = [ [ -- 2.25.1