dev/core#1109 - Fix merge not updating ContactReference fields
authorPatrick Figel <pfigel@greenpeace.org>
Wed, 7 Aug 2019 14:38:59 +0000 (16:38 +0200)
committerPatrick Figel <pfigel@greenpeace.org>
Wed, 7 Aug 2019 14:38:59 +0000 (16:38 +0200)
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
CRM/Dedupe/Merger.php
tests/phpunit/CRM/Dedupe/MergerTest.php

index 5eb4713b8fe6cb21cf520acaa6b636478e1ec983..c1e44c0d64413318655e20354b470d8edb65eeeb 100644 (file)
@@ -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'];
     }
   }
 
index 42507b9e7d0b3b76c4e031d063f6d7ac8cfba8a9..9e501f35f60c2a07f11f11f03d049543429ea0f0 100644 (file)
@@ -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";
index be93391b97f0683e1e0a50302db492c0b19a0176..a5e3c8c67a249ecd8fb8053590b988caa5625991 100644 (file)
@@ -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 = [
       [