Skip to content

Commit

Permalink
1. flipping dupe pairs for a row or selections. 2. store meaningful -…
Browse files Browse the repository at this point in the history
… conflict labels & values in conflict. 3. layout for conflict column - new line after every conflict info. 4. 'vs' formatting. 5. Add permission for forced merges. 6. Fix for: 'One of parameters (value: null) is not of the type Money/Timestamp' errors during batch merges. 7. allow hooks to decide if to skip merges even in aggressive mode. 8. for conflicts screen provide option for safe merges as well
  • Loading branch information
deepak-srivastava committed Aug 8, 2015
1 parent 63ef778 commit fd630ef
Show file tree
Hide file tree
Showing 8 changed files with 143 additions and 44 deletions.
35 changes: 28 additions & 7 deletions CRM/Contact/Page/AJAX.php
Expand Up @@ -816,37 +816,38 @@ static function getDedupes() {
$srcTypeImage = CRM_Contact_BAO_Contact_Utils::getImage($srcContactSubType ?
$srcContactSubType : $pairInfo['src_contact_type'],
FALSE,
$pair['srcID']
$pairInfo['entity_id1']
);
$dstTypeImage = CRM_Contact_BAO_Contact_Utils::getImage($dstContactSubType ?
$dstContactSubType : $pairInfo['dst_contact_type'],
FALSE,
$pair['dstID']
$pairInfo['entity_id2']
);

$searchRows[$count]['is_selected'] = $pairInfo['is_selected'];
$searchRows[$count]['is_selected_input'] = "<input type='checkbox' class='crm-dedupe-select' name='pnid_{$pairInfo['prevnext_id']}' value='{$pairInfo['is_selected']}' onclick='toggleDedupeSelect(this)'>";
$searchRows[$count]['src_image'] = $srcTypeImage;
$searchRows[$count]['src'] = CRM_Utils_System::href($pair['srcName'], 'civicrm/contact/view', "reset=1&cid={$pair['srcID']}");
$searchRows[$count]['src'] = CRM_Utils_System::href($pair['srcName'], 'civicrm/contact/view', "reset=1&cid={$pairInfo['entity_id1']}");
$searchRows[$count]['src_email'] = CRM_Utils_Array::value('src_email', $pairInfo);
$searchRows[$count]['src_street'] = CRM_Utils_Array::value('src_street', $pairInfo);
$searchRows[$count]['src_postcode'] = CRM_Utils_Array::value('src_postcode', $pairInfo);
$searchRows[$count]['dst_image'] = $dstTypeImage;
$searchRows[$count]['dst'] = CRM_Utils_System::href($pair['dstName'], 'civicrm/contact/view', "reset=1&cid={$pair['dstID']}");
$searchRows[$count]['dst'] = CRM_Utils_System::href($pair['dstName'], 'civicrm/contact/view', "reset=1&cid={$pairInfo['entity_id2']}");
$searchRows[$count]['dst_email'] = CRM_Utils_Array::value('dst_email', $pairInfo);
$searchRows[$count]['dst_street'] = CRM_Utils_Array::value('dst_street', $pairInfo);
$searchRows[$count]['dst_postcode'] = CRM_Utils_Array::value('dst_postcode', $pairInfo);
$searchRows[$count]['conflicts'] = CRM_Utils_Array::value('conflicts', $pair);
$searchRows[$count]['weight'] = CRM_Utils_Array::value('weight', $pair);

if (!empty($pair['canMerge'])) {
$mergeParams = "reset=1&cid={$pair['srcID']}&oid={$pair['dstID']}&action=update&rgid={$rgid}";
if (!empty($pairInfo['data']['canMerge'])) {
$mergeParams = "reset=1&cid={$pairInfo['entity_id1']}&oid={$pairInfo['entity_id2']}&action=update&rgid={$rgid}";
if ($gid) {
$mergeParams .= "&gid={$gid}";
}

$searchRows[$count]['actions'] = "<a class='crm-dedupe-flip' href='#' data-pnid={$pairInfo['prevnext_id']}>" . ts('flip') . "</a>&nbsp;|&nbsp;";
$searchRows[$count]['actions'] = CRM_Utils_System::href(ts('merge'), 'civicrm/contact/merge', $mergeParams);
$searchRows[$count]['actions'] .= "&nbsp;|&nbsp;<a id='notDuplicate' href='#' onClick=\"processDupes( {$pair['srcID']}, {$pair['dstID']}, 'dupe-nondupe', 'dupe-listing'); return false;\">" . ts('not a duplicate') . "</a>";
$searchRows[$count]['actions'] .= "&nbsp;|&nbsp;<a id='notDuplicate' href='#' onClick=\"processDupes( {$pairInfo['entity_id1']}, {$pairInfo['entity_id2']}, 'dupe-nondupe', 'dupe-listing'); return false;\">" . ts('not a duplicate') . "</a>";
}
else {
$searchRows[$count]['actions'] = '<em>' . ts('Insufficient access rights - cannot merge') . '</em>';
Expand Down Expand Up @@ -882,6 +883,26 @@ public static function paperSize() {
CRM_Utils_JSON::output($paperSize);
}

static function flipDupePairs($prevNextId = NULL) {
if (!$prevNextId) {
$prevNextId = $_REQUEST['pnid'];
}
$query = "
UPDATE civicrm_prevnext_cache cpc
INNER JOIN civicrm_prevnext_cache old on cpc.id = old.id
SET cpc.entity_id1 = cpc.entity_id2, cpc.entity_id2 = old.entity_id1 ";
if (is_array($prevNextId) && !CRM_Utils_Array::crmIsEmptyArray($prevNextId)) {
$prevNextId = implode(', ', $prevNextId);
$prevNextId = CRM_Utils_Type::escape($prevNextId, 'String');
$query .= "WHERE cpc.id IN ({$prevNextId}) AND cpc.is_selected = 1";
} else {
$prevNextId = CRM_Utils_Type::escape($prevNextId, 'Positive');
$query .= "WHERE cpc.id = $prevNextId";
}
CRM_Core_DAO::executeQuery($query);
CRM_Utils_JSON::output();
}

/**
* Used to store selected contacts across multiple pages in advanced search.
*/
Expand Down
10 changes: 7 additions & 3 deletions CRM/Contact/Page/DedupeMerge.php
Expand Up @@ -66,6 +66,13 @@ static function getRunner() {
$cacheKeyString .= $rgid ? "_{$rgid}" : '_0';
$cacheKeyString .= $gid ? "_{$gid}" : '_0';

$urlQry = "reset=1&action=update&rgid={$rgid}";
$urlQry = $gid ? ($urlQry . "&gid={$gid}") : $urlQry;

if ($mode == 'aggressive' && !CRM_Core_Permission::check('force merge duplicate contacts')) {
CRM_Core_Session::setStatus(ts('You do not have permission to force merge duplicate contact records'), ts('Permission Denied'), 'error');
CRM_Utils_System::redirect(CRM_Utils_System::url('civicrm/contact/dedupefind', $urlQry));
}
// Setup the Queue
$queue = CRM_Queue_Service::singleton()->create(array(
'name' => $cacheKeyString,
Expand All @@ -82,9 +89,6 @@ static function getRunner() {
$isSelected = 2;
}

$urlQry = "reset=1&action=update&rgid={$rgid}";
$urlQry = $gid ? ($urlQry . "&gid={$gid}") : $urlQry;

$total = CRM_Core_BAO_PrevNextCache::getCount($cacheKeyString, NULL, $where);
if ($total <= 0) {
// Nothing to do.
Expand Down
12 changes: 9 additions & 3 deletions CRM/Core/BAO/CustomValueTable.php
Expand Up @@ -210,9 +210,15 @@ public static function create(&$customParams) {
default:
break;
}
$set[$field['column_name']] = "%{$count}";
$params[$count] = array($value, $type);
$count++;
if (strtolower($value) === "null") {
// when unsetting a value to null, we don't need to validate the type
// https://projectllr.atlassian.net/browse/VGQBMP-20
$set[$field['column_name']] = $value;
} else {
$set[$field['column_name']] = "%{$count}";
$params[$count] = array($value, $type);
$count++;
}
}

if (!empty($set)) {
Expand Down
2 changes: 1 addition & 1 deletion CRM/Core/BAO/PrevNextCache.php
Expand Up @@ -181,7 +181,7 @@ static function markConflict($id1, $id2, $cacheKey, $conflicts) {
$data = $pncFind->data;
if (!empty($data)) {
$data = unserialize($data);
$data['conflicts'] = implode(",", array_keys($conflicts));
$data['conflicts'] = implode(",", array_values($conflicts));

$pncUp = new CRM_Core_DAO_PrevNextCache();
$pncUp->id = $pncFind->id;
Expand Down
4 changes: 4 additions & 0 deletions CRM/Core/Permission.php
Expand Up @@ -771,6 +771,10 @@ public static function getCorePermissions($descriptions = FALSE) {
$prefix . ts('merge duplicate contacts'),
ts('Delete Contacts must also be granted in order for this to work.'),
),
'force merge duplicate contacts' => array(
$prefix . ts('force merge duplicate contacts'),
ts('Delete Contacts must also be granted in order for this to work.'),
),
'view debug output' => array(
$prefix . ts('view debug output'),
ts('View results of debug and backtrace'),
Expand Down
5 changes: 5 additions & 0 deletions CRM/Core/xml/Menu/Contact.xml
Expand Up @@ -389,6 +389,11 @@
<page_callback>CRM_Contact_Page_AJAX::toggleDedupeSelect</page_callback>
<access_arguments>merge duplicate contacts</access_arguments>
</item>
<item>
<path>civicrm/ajax/flipDupePairs</path>
<page_callback>CRM_Contact_Page_AJAX::flipDupePairs</page_callback>
<access_arguments>merge duplicate contacts</access_arguments>
</item>
<item>
<path>civicrm/activity/sms/add</path>
<path_arguments>action=add</path_arguments>
Expand Down
30 changes: 14 additions & 16 deletions CRM/Dedupe/Merger.php
Expand Up @@ -731,6 +731,9 @@ public static function merge($dupePairs = array(), $cacheParams = array(), $mode

// store any conflicts
if (!empty($conflicts)) {
foreach ($conflicts as $key => $dnc) {
$conflicts[$key] = "{$migrationInfo['rows'][$key]['title']}: '{$migrationInfo['rows'][$key]['main']}' vs. '{$migrationInfo['rows'][$key]['other']}'";
}
CRM_Core_BAO_PrevNextCache::markConflict($mainId, $otherId, $cacheKeyString, $conflicts);
} else {
// delete entry from PrevNextCache table so we don't consider the pair next time
Expand Down Expand Up @@ -798,15 +801,10 @@ public static function skipMerge($mainId, $otherId, &$migrationInfo, $mode = 'sa
// particular field or not
if (!empty($migrationInfo['rows'][$key]['main'])) {
// if main also has a value its a conflict
if ($mode == 'safe') {
// note it down & lets wait for response from the hook.
// For no response skip this merge
$conflicts[$key] = NULL;
}
elseif ($mode == 'aggressive') {
// let the main-field be overwritten
continue;
}

// note it down & lets wait for response from the hook.
// For no response $mode will decide if to skip this merge
$conflicts[$key] = NULL;
}
}
elseif (substr($key, 0, 14) == 'move_location_' and $val != NULL) {
Expand All @@ -831,16 +829,11 @@ public static function skipMerge($mainId, $otherId, &$migrationInfo, $mode = 'sa
// try insert address at new available loc-type
$migrationInfo['location'][$fieldName][$fieldCount]['locTypeId'] = $newTypeId;
}
elseif ($mode == 'safe') {
else {
// note it down & lets wait for response from the hook.
// For no response skip this merge
// For no response $mode will decide if to skip this merge
$conflicts[$key] = NULL;
}
elseif ($mode == 'aggressive') {
// let the loc-type-id be same as that of other-contact & go ahead
// with merge assuming aggressive mode
continue;
}
}
}
elseif ($migrationInfo['rows'][$key]['main'] == $migrationInfo['rows'][$key]['other']) {
Expand All @@ -856,6 +849,7 @@ public static function skipMerge($mainId, $otherId, &$migrationInfo, $mode = 'sa
// merge happens with new values filled in here. For a particular field / row not to be merged
// field should be unset from fields_in_conflict.
$migrationData['fields_in_conflict'] = $conflicts;
$migrationData['merge_mode'] = $mode;
CRM_Utils_Hook::merge('batch', $migrationData, $mainId, $otherId);
$conflicts = $migrationData['fields_in_conflict'];
// allow hook to override / manipulate migrationInfo as well
Expand All @@ -872,6 +866,10 @@ public static function skipMerge($mainId, $otherId, &$migrationInfo, $mode = 'sa
$migrationInfo[$key] = $val;
}
}
// if there are conflicts and mode is aggressive, allow hooks to decide if to skip merges
if (array_key_exists('skip_merge', $migrationData)) {
return (bool) $migrationData['skip_merge'];
}
}
return FALSE;
}
Expand Down
89 changes: 75 additions & 14 deletions templates/CRM/Contact/Page/DedupeFind.tpl
Expand Up @@ -128,19 +128,21 @@
{if $context eq 'search'}
{crmButton href=$backURL icon="close"}{ts}Done{/ts}{/crmButton}
{elseif $context eq 'conflicts'}
{if $gid}
{capture assign=backURL}{crmURL p="civicrm/contact/dedupemerge" q="reset=1&rgid=`$rgid`&gid=`$gid`&action=map&mode=aggressive" a=1}{/capture}
{else}
{capture assign=backURL}{crmURL p="civicrm/contact/dedupemerge" q="reset=1&rgid=`$rgid`&action=map&mode=aggressive" a=1}{/capture}
{/if}
<a href="{$backURL}" title="{ts}Batch Merge Duplicate Contacts{/ts}" onclick="return confirm('{ts escape="js"}This will run the batch merge process on the selected duplicates. The operation will run in force merge mode - all selected duplicates will be merged into main contacts even in case of any conflicts. Click OK to proceed if you are sure you wish to run this operation.{/ts}');" class="button"><span>{ts}Force Merge Selected Duplicates{/ts}</span></a>
{if call_user_func(array('CRM_Core_Permission','check'), 'force merge duplicate contacts')}
{if $gid}
{capture assign=backURL}{crmURL p="civicrm/contact/dedupemerge" q="reset=1&rgid=`$rgid`&gid=`$gid`&action=map&mode=aggressive" a=1}{/capture}
{else}
{capture assign=backURL}{crmURL p="civicrm/contact/dedupemerge" q="reset=1&rgid=`$rgid`&action=map&mode=aggressive" a=1}{/capture}
{/if}
<a href="{$backURL}" title="{ts}Batch Merge Duplicate Contacts{/ts}" onclick="return confirm('{ts escape="js"}This will run the batch merge process on the selected duplicates. The operation will run in force merge mode - all selected duplicates will be merged into main contacts even in case of any conflicts. Click OK to proceed if you are sure you wish to run this operation.{/ts}');" class="button"><span>{ts}Force Merge Selected Duplicates{/ts}</span></a>

{if $gid}
{capture assign=backURL}{crmURL p="civicrm/contact/dedupemerge" q="reset=1&rgid=`$rgid`&gid=`$gid`&mode=aggressive" a=1}{/capture}
{else}
{capture assign=backURL}{crmURL p="civicrm/contact/dedupemerge" q="reset=1&rgid=`$rgid`&mode=aggressive" a=1}{/capture}
{if $gid}
{capture assign=backURL}{crmURL p="civicrm/contact/dedupemerge" q="reset=1&rgid=`$rgid`&gid=`$gid`&action=map" a=1}{/capture}
{else}
{capture assign=backURL}{crmURL p="civicrm/contact/dedupemerge" q="reset=1&rgid=`$rgid`&action=map" a=1}{/capture}
{/if}
<a href="{$backURL}" title="{ts}Batch Merge Duplicate Contacts{/ts}" onclick="return confirm('{ts escape="js"}This will run the batch merge process on the selected duplicates. The operation will run in safe mode - only records with no direct data conflicts will be merged. Click OK to proceed if you are sure you wish to run this operation.{/ts}');" class="button"><span>{ts}Safe Merge Selected Duplicates{/ts}</span></a>
{/if}
<a href="{$backURL}" title="{ts}Batch Merge Duplicate Contacts{/ts}" onclick="return confirm('{ts escape="js"}This will run the batch merge process on the listed duplicates. The operation will run in safe mode - only records with no direct data conflicts will be merged. Click OK to proceed if you are sure you wish to run this operation.{/ts}');" class="button"><span>{ts}Force Merge All Duplicates{/ts}</span></a>

{if $gid}
{capture assign=backURL}{crmURL p="civicrm/contact/dedupefind" q="reset=1&action=update&rgid=`$rgid`&gid=`$gid`" a=1}{/capture}
Expand Down Expand Up @@ -172,6 +174,8 @@
{/if}
<a href="{$backURL}" title="{ts}Batch Merge Duplicate Contacts{/ts}" onclick="return confirm('{ts escape="js"}This will run the batch merge process on the listed duplicates. The operation will run in safe mode - only records with no direct data conflicts will be merged. Click OK to proceed if you are sure you wish to run this operation.{/ts}');" class="button"><span><div class="icon ui-icon-script"></div>{ts}Batch Merge All Duplicates{/ts}</span></a>

<a href='#' title="{ts}Flip Selected Duplicates{/ts}" class="crm-dedupe-flip-selections button"><span>{ts}Flip Selected Duplicates{/ts}</span></a>

{capture assign=backURL}{crmURL p="civicrm/contact/deduperules" q="reset=1" a=1}{/capture}
<a href="{$backURL}" class="button crm-button-type-cancel">
<span><div class="icon ui-icon-close"></div> {ts}Done{/ts}</span>
Expand Down Expand Up @@ -208,7 +212,10 @@ CRM.$(function($) {
{data: "dst_street"},
{data: "src_postcode"},
{data: "dst_postcode"},
{data: "conflicts"},
{
data: "conflicts",
className: "crm-pair-conflict"
},
{data: "weight"},
{data: "actions"},
],
Expand All @@ -231,6 +238,8 @@ CRM.$(function($) {
}
// for action column at the last, set nowrap
$('td:last', row).attr('nowrap','nowrap');
// for conflcts column
$('td.crm-pair-conflict', row).attr('nowrap','nowrap');
}
});

Expand All @@ -246,13 +255,14 @@ CRM.$(function($) {
$('#dupePairs_length_selection').appendTo('#dupePairs_length');

// apply selected class on click of a row
$('#dupePairs tbody').on('click', 'tr', function() {
$('#dupePairs tbody').on('click', 'tr', function(e) {
$(this).toggleClass('crm-row-selected');
$('input.crm-dedupe-select', this).prop('checked', $(this).hasClass('crm-row-selected'));
var ele = $('input.crm-dedupe-select', this);
toggleDedupeSelect(ele, 0);
});


// when select-all checkbox is checked
$('#dupePairs thead tr .crm-dedupe-selection').on('click', function() {
var checked = $('.crm-dedupe-select-all').prop('checked');
if (checked) {
Expand Down Expand Up @@ -291,17 +301,68 @@ CRM.$(function($) {
var column = table.column( $(this).attr('data-column-main') );
column.visible( ! column.visible() );

// nowrap to conflicts column is applied only during initial rendering
// for show / hide clicks we need to set it explicitly
$('#dupePairs tbody td.crm-pair-conflict').attr('nowrap', 'nowrap');

if ($(this).attr('data-column-dupe')) {
column = table.column( $(this).attr('data-column-dupe') );
column.visible( ! column.visible() );
}
});

// keep the conflicts checkbox checked when context is "conflicts"
if(context == 'conflicts') {
$('#conflicts').attr('checked', true);
var column = table.column( $('#conflicts').attr('data-column-main') );
column.visible( ! column.visible() );
}

// on click of flip link of a row
$('#dupePairs tbody').on('click', 'tr .crm-dedupe-flip', function(e) {
e.stopPropagation();
var $el = $(this);
var $elTr = $(this).closest('tr');
var postUrl = {/literal}"{crmURL p='civicrm/ajax/flipDupePairs' h=0 q='snippet=4'}"{literal};
var request = $.post(postUrl, {pnid : $el.data('pnid')});
request.done(function(dt) {
var mapper = {2:4, 5:6, 7:8, 9:10}
var idx = table.row($elTr).index();
$.each(mapper, function(key, val) {
var v1 = table.cell(idx, key).data();
var v2 = table.cell(idx, val).data();
table.cell(idx, key).data(v2);
table.cell(idx, val).data(v1);
});
// keep the checkbox checked if needed
$('input.crm-dedupe-select', $elTr).prop('checked', $elTr.hasClass('crm-row-selected'));
});
});

$(".crm-dedupe-flip-selections").on('click', function(e) {
var ids = [];
$('.crm-row-selected').each(function() {
var ele = CRM.$('input.crm-dedupe-select', this);
ids.push(CRM.$(ele).attr('name').substr(5));
});
if (ids.length > 0) {
var dataUrl = {/literal}"{crmURL p='civicrm/ajax/flipDupePairs' h=0 q='snippet=4'}"{literal};
CRM.$.post(dataUrl, {pnid: ids}, function (response) {
var mapper = {2:4, 5:6, 7:8, 9:10}
$('.crm-row-selected').each(function() {
var idx = table.row(this).index();
$.each(mapper, function(key, val) {
var v1 = table.cell(idx, key).data();
var v2 = table.cell(idx, val).data();
table.cell(idx, key).data(v2);
table.cell(idx, val).data(v1);
});
// keep the checkbox checked if needed
$('input.crm-dedupe-select', this).prop('checked', $(this).hasClass('crm-row-selected'));
});
}, 'json');
}
});
});

function toggleDedupeSelect(element, isMultiple) {
Expand Down

0 comments on commit fd630ef

Please sign in to comment.