From fd630ef95ece7c8f8005ddaf0e0e8cc391c1be43 Mon Sep 17 00:00:00 2001 From: deepak-srivastava Date: Tue, 21 Jul 2015 15:58:46 +0100 Subject: [PATCH] 1. flipping dupe pairs for a row or selections. 2. store meaningful - 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 --- CRM/Contact/Page/AJAX.php | 35 +++++++-- CRM/Contact/Page/DedupeMerge.php | 10 ++- CRM/Core/BAO/CustomValueTable.php | 12 ++- CRM/Core/BAO/PrevNextCache.php | 2 +- CRM/Core/Permission.php | 4 + CRM/Core/xml/Menu/Contact.xml | 5 ++ CRM/Dedupe/Merger.php | 30 ++++---- templates/CRM/Contact/Page/DedupeFind.tpl | 89 +++++++++++++++++++---- 8 files changed, 143 insertions(+), 44 deletions(-) diff --git a/CRM/Contact/Page/AJAX.php b/CRM/Contact/Page/AJAX.php index e597802c87..dc7ea91851 100644 --- a/CRM/Contact/Page/AJAX.php +++ b/CRM/Contact/Page/AJAX.php @@ -816,37 +816,38 @@ LIMIT {$offset}, {$rowCount} $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'] = ""; $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'] = "" . ts('flip') . " | "; $searchRows[$count]['actions'] = CRM_Utils_System::href(ts('merge'), 'civicrm/contact/merge', $mergeParams); - $searchRows[$count]['actions'] .= " | " . ts('not a duplicate') . ""; + $searchRows[$count]['actions'] .= " | " . ts('not a duplicate') . ""; } else { $searchRows[$count]['actions'] = '' . ts('Insufficient access rights - cannot merge') . ''; @@ -882,6 +883,26 @@ LIMIT {$offset}, {$rowCount} 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. */ diff --git a/CRM/Contact/Page/DedupeMerge.php b/CRM/Contact/Page/DedupeMerge.php index a3e393f66a..97970ea44a 100644 --- a/CRM/Contact/Page/DedupeMerge.php +++ b/CRM/Contact/Page/DedupeMerge.php @@ -66,6 +66,13 @@ class CRM_Contact_Page_DedupeMerge extends CRM_Core_Page{ $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, @@ -82,9 +89,6 @@ class CRM_Contact_Page_DedupeMerge extends CRM_Core_Page{ $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. diff --git a/CRM/Core/BAO/CustomValueTable.php b/CRM/Core/BAO/CustomValueTable.php index 10c3778727..d2da71444e 100644 --- a/CRM/Core/BAO/CustomValueTable.php +++ b/CRM/Core/BAO/CustomValueTable.php @@ -210,9 +210,15 @@ class CRM_Core_BAO_CustomValueTable { 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)) { diff --git a/CRM/Core/BAO/PrevNextCache.php b/CRM/Core/BAO/PrevNextCache.php index 90bc832f07..74d9995a33 100644 --- a/CRM/Core/BAO/PrevNextCache.php +++ b/CRM/Core/BAO/PrevNextCache.php @@ -181,7 +181,7 @@ WHERE cacheKey = %3 AND $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; diff --git a/CRM/Core/Permission.php b/CRM/Core/Permission.php index 9d983b921f..0e99a8d1b6 100644 --- a/CRM/Core/Permission.php +++ b/CRM/Core/Permission.php @@ -771,6 +771,10 @@ class CRM_Core_Permission { $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'), diff --git a/CRM/Core/xml/Menu/Contact.xml b/CRM/Core/xml/Menu/Contact.xml index b1ddb7d330..3da74d9070 100644 --- a/CRM/Core/xml/Menu/Contact.xml +++ b/CRM/Core/xml/Menu/Contact.xml @@ -389,6 +389,11 @@ CRM_Contact_Page_AJAX::toggleDedupeSelect merge duplicate contacts + + civicrm/ajax/flipDupePairs + CRM_Contact_Page_AJAX::flipDupePairs + merge duplicate contacts + civicrm/activity/sms/add action=add diff --git a/CRM/Dedupe/Merger.php b/CRM/Dedupe/Merger.php index 1d25f08410..2f5e5d7de7 100644 --- a/CRM/Dedupe/Merger.php +++ b/CRM/Dedupe/Merger.php @@ -731,6 +731,9 @@ INNER JOIN civicrm_membership membership2 ON membership1.membership_type_id = m // 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 @@ -798,15 +801,10 @@ INNER JOIN civicrm_membership membership2 ON membership1.membership_type_id = m // 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) { @@ -831,16 +829,11 @@ INNER JOIN civicrm_membership membership2 ON membership1.membership_type_id = m // 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']) { @@ -856,6 +849,7 @@ INNER JOIN civicrm_membership membership2 ON membership1.membership_type_id = m // 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 @@ -872,6 +866,10 @@ INNER JOIN civicrm_membership membership2 ON membership1.membership_type_id = m $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; } diff --git a/templates/CRM/Contact/Page/DedupeFind.tpl b/templates/CRM/Contact/Page/DedupeFind.tpl index 76f380bdbb..c3e11010c5 100644 --- a/templates/CRM/Contact/Page/DedupeFind.tpl +++ b/templates/CRM/Contact/Page/DedupeFind.tpl @@ -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} - {ts}Force Merge Selected Duplicates{/ts} + {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} + {ts}Force Merge Selected Duplicates{/ts} - {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} + {ts}Safe Merge Selected Duplicates{/ts} {/if} - {ts}Force Merge All Duplicates{/ts} {if $gid} {capture assign=backURL}{crmURL p="civicrm/contact/dedupefind" q="reset=1&action=update&rgid=`$rgid`&gid=`$gid`" a=1}{/capture} @@ -172,6 +174,8 @@ {/if}
{ts}Batch Merge All Duplicates{/ts}
+ {ts}Flip Selected Duplicates{/ts} + {capture assign=backURL}{crmURL p="civicrm/contact/deduperules" q="reset=1" a=1}{/capture}
{ts}Done{/ts}
@@ -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"}, ], @@ -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'); } }); @@ -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) { @@ -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) { -- 2.25.1