Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

dev/core/#/233 Expose information about where a contact has been merged to #12489

Merged
merged 2 commits into from
Jul 18, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion CRM/Contact/Page/View.php
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ public static function checkUserPermission($page, $contactID = NULL) {
*/
public static function setTitle($contactId, $isDeleted = FALSE) {
static $contactDetails;
$displayName = $contactImage = NULL;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the declaration of display name deleted?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because it was that funny grey in phpstorm (ie. it's immediately re-defined)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be grey because PHPStorm thinks it's being overwritten:

image

But really it's just confused. I think we should either leave it in, or remove the $contactImage declaration too, by the same logic =]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't the contactImage declaration removed?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, just on the next line down =]

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I agree with @JKingsnorth that we should remove both.

$contactImage = NULL;
if (!isset($contactDetails[$contactId])) {
list($displayName, $contactImage) = self::getContactDetails($contactId);
$contactDetails[$contactId] = array(
Expand All @@ -327,6 +327,15 @@ public static function setTitle($contactId, $isDeleted = FALSE) {
}
if ($isDeleted) {
$title = "<del>{$title}</del>";
$mergedTo = civicrm_api3('Contact', 'getmergedto', ['contact_id' => $contactId, 'api.Contact.get' => ['return' => 'display_name']]);
if ($mergedTo['count']) {
$mergedToContactID = $mergedTo['id'];
$mergedToDisplayName = $mergedTo['values'][$mergedToContactID]['api.Contact.get']['values'][0]['display_name'];
$title .= ' ' . ts('(This contact has been merged to <a href="%1">%2</a>)', [
1 => CRM_Utils_System::url('civicrm/contact/view', ['reset' => 1, 'cid' => $mergedToContactID]),
2 => $mergedToDisplayName,
]);
}
}

// Inline-edit places its own title on the page
Expand Down
143 changes: 143 additions & 0 deletions api/v3/Contact.php
Original file line number Diff line number Diff line change
Expand Up @@ -1162,6 +1162,149 @@ function _civicrm_api3_contact_merge_spec(&$params) {
);
}

/**
* Get the ultimate contact a contact was merged to.
*
* @param array $params
*
* @return array
* API Result Array
* @throws API_Exception
*/
function civicrm_api3_contact_getmergedto($params) {
$contactID = _civicrm_api3_contact_getmergedto($params);
if ($contactID) {
$values = [$contactID => ['id' => $contactID]];
}
else {
$values = [];
}
return civicrm_api3_create_success($values, $params);
}

/**
* Get the contact our contact was finally merged to.
*
* If the contact has been merged multiple times the crucial parent activity will have
* wound up on the ultimate contact so we can figure out the final resting place of the
* contact with only 2 activities even if 50 merges took place.
*
* @param array $params
*
* @return int|false
*/
function _civicrm_api3_contact_getmergedto($params) {
$contactID = FALSE;
$deleteActivity = civicrm_api3('ActivityContact', 'get', [
'contact_id' => $params['contact_id'],
'activity_id.activity_type_id' => 'Contact Deleted By Merge',
'is_deleted' => 0,
'is_test' => $params['is_test'],
'record_type_id' => 'Activity Targets',
'return' => ['activity_id.parent_id'],
'sequential' => 1,
'options' => [
'limit' => 1,
'sort' => 'activity_id.activity_date_time DESC'
],
])['values'];
if (!empty($deleteActivity)) {
$contactID = civicrm_api3('ActivityContact', 'getvalue', [
'activity_id' => $deleteActivity[0]['activity_id.parent_id'],
'record_type_id' => 'Activity Targets',
'return' => 'contact_id',
]);
}
return $contactID;
}

/**
* Adjust metadata for contact_merge api function.
*
* @param array $params
*/
function _civicrm_api3_contact_getmergedto_spec(&$params) {
$params['contact_id'] = [
'title' => ts('ID of contact to find ultimate contact for'),
'type' => CRM_Utils_Type::T_INT,
'api.required' => TRUE,
];
$params['is_test'] = [
'title' => ts('Get test deletions rather than live?'),
'type' => CRM_Utils_Type::T_BOOLEAN,
'api.default' => 0,
];
}

/**
* Get the ultimate contact a contact was merged to.
*
* @param array $params
*
* @return array
* API Result Array
* @throws API_Exception
*/
function civicrm_api3_contact_getmergedfrom($params) {
$contacts = _civicrm_api3_contact_getmergedfrom($params);
return civicrm_api3_create_success($contacts, $params);
}

/**
* Get all the contacts merged into our contact.
*
* @param array $params
*
* @return array
*/
function _civicrm_api3_contact_getmergedfrom($params) {
$activities = [];
$deleteActivities = civicrm_api3('ActivityContact', 'get', [
'contact_id' => $params['contact_id'],
'activity_id.activity_type_id' => 'Contact Merged',
'is_deleted' => 0,
'is_test' => $params['is_test'],
'record_type_id' => 'Activity Targets',
'return' => 'activity_id',
])['values'];

foreach ($deleteActivities as $deleteActivity) {
$activities[] = $deleteActivity['activity_id'];
}
if (empty($activities)) {
return [];
}

$activityContacts = civicrm_api3('ActivityContact', 'get', [
'activity_id.parent_id' => ['IN' => $activities],
'record_type_id' => 'Activity Targets',
'return' => 'contact_id',
])['values'];
$contacts = [];
foreach ($activityContacts as $activityContact) {
$contacts[$activityContact['contact_id']] = ['id' => $activityContact['contact_id']];
}
return $contacts;
}

/**
* Adjust metadata for contact_merge api function.
*
* @param array $params
*/
function _civicrm_api3_contact_getmergedfrom_spec(&$params) {
$params['contact_id'] = [
'title' => ts('ID of contact to find ultimate contact for'),
'type' => CRM_Utils_Type::T_INT,
'api.required' => TRUE,
];
$params['is_test'] = [
'title' => ts('Get test deletions rather than live?'),
'type' => CRM_Utils_Type::T_BOOLEAN,
'api.default' => 0,
];
}

/**
* Adjust metadata for contact_proximity api function.
*
Expand Down
34 changes: 33 additions & 1 deletion tests/phpunit/api/v3/ContactTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ public function testGetMultipleContactSubTypes() {
));

// create a parent
$contact = $this->callAPISuccess('contact', 'create', array(
$this->callAPISuccess('contact', 'create', array(
'email' => 'parent@example.com',
'contact_type' => 'Individual',
));
Expand Down Expand Up @@ -3388,6 +3388,38 @@ public function testMerge() {

}

/**
* Test retrieving merged contacts.
*
* The goal here is to start with a contact deleted by merged and find out the contact that is the current version of them.
*/
public function testMergedGet() {
$this->contactIDs[] = $this->individualCreate();
$this->contactIDs[] = $this->individualCreate();
$this->contactIDs[] = $this->individualCreate();
$this->contactIDs[] = $this->individualCreate();

// First do an 'unnatural merge' - they 'like to merge into the lowest but this will mean that contact 0 merged to contact [3].
// When the batch merge runs.... the new lowest contact is contact[1]. All contacts will merge into that contact,
// including contact[3], resulting in only 3 existing at the end. For each contact the correct answer to 'who did I eventually
// wind up being should be [1]
$this->callAPISuccess('Contact', 'merge', ['to_remove_id' => $this->contactIDs[0], 'to_keep_id' => $this->contactIDs[3]]);

$this->callAPISuccess('Job', 'process_batch_merge', []);
foreach ($this->contactIDs as $contactID) {
if ($contactID === $this->contactIDs[1]) {
continue;
}
$result = $this->callAPIAndDocument('Contact', 'getmergedto', ['sequential' => 1, 'contact_id' => $contactID], __FUNCTION__, __FILE__);
$this->assertEquals(1, $result['count']);
$this->assertEquals($this->contactIDs[1], $result['values'][0]['id']);
}

$result = $this->callAPIAndDocument('Contact', 'getmergedfrom', ['contact_id' => $this->contactIDs[1]], __FUNCTION__, __FILE__)['values'];
$mergedContactIds = array_merge(array_diff($this->contactIDs, [$this->contactIDs[1]]));
$this->assertEquals($mergedContactIds, array_keys($result));
}

/**
* Test merging 2 contacts with delete to trash off.
*
Expand Down