Applying patch from Civi v4.5 repo, dedupe-workflow-45 branch, till 97e53e0c63c9d0d4d...
authordeepak-srivastava <deepak.srivastava.0303@gmail.com>
Thu, 14 May 2015 10:01:42 +0000 (11:01 +0100)
committerdeepak-srivastava <deepak.srivastava.0303@gmail.com>
Sat, 8 Aug 2015 20:53:01 +0000 (21:53 +0100)
CRM/Contact/Page/AJAX.php
CRM/Contact/Page/DedupeFind.php
CRM/Contact/Page/DedupeMerge.php [new file with mode: 0644]
CRM/Core/BAO/PrevNextCache.php
CRM/Core/xml/Menu/Contact.xml
CRM/Dedupe/Merger.php
CRM/Utils/JSON.php
templates/CRM/Contact/Page/DedupeFind.tpl
templates/CRM/Contact/Page/DedupeMerge.tpl [new file with mode: 0644]

index a2a9b3bd9c54559ebc3f2dce95ba5092186e3795..e597802c87a47cdb7f240a3a5c9d7247c925e736 100644 (file)
@@ -668,53 +668,194 @@ LIMIT {$offset}, {$rowCount}
     CRM_Utils_JSON::output(array('status' => ($status) ? $oper : $status));
   }
 
-  public static function getDedupes() {
-
-    $sEcho = CRM_Utils_Type::escape($_REQUEST['sEcho'], 'Integer');
-    $offset = isset($_REQUEST['iDisplayStart']) ? CRM_Utils_Type::escape($_REQUEST['iDisplayStart'], 'Integer') : 0;
-    $rowCount = isset($_REQUEST['iDisplayLength']) ? CRM_Utils_Type::escape($_REQUEST['iDisplayLength'], 'Integer') : 25;
-    $sort = 'sort_name';
-    $sortOrder = isset($_REQUEST['sSortDir_0']) ? CRM_Utils_Type::escape($_REQUEST['sSortDir_0'], 'String') : 'asc';
-
-    $gid = isset($_REQUEST['gid']) ? CRM_Utils_Type::escape($_REQUEST['gid'], 'Integer') : 0;
-    $rgid = isset($_REQUEST['rgid']) ? CRM_Utils_Type::escape($_REQUEST['rgid'], 'Integer') : 0;
+  static function getDedupes() {
+    $offset    = isset($_REQUEST['start']) ? CRM_Utils_Type::escape($_REQUEST['start'], 'Integer') : 0;
+    $rowCount  = isset($_REQUEST['length']) ? CRM_Utils_Type::escape($_REQUEST['length'], 'Integer') : 25;
+
+    $gid         = isset($_REQUEST['gid']) ? CRM_Utils_Type::escape($_REQUEST['gid'], 'Integer') : 0;
+    $rgid        = isset($_REQUEST['rgid']) ? CRM_Utils_Type::escape($_REQUEST['rgid'], 'Integer') : 0;
+    $selected    = isset($_REQUEST['selected']) ? CRM_Utils_Type::escape($_REQUEST['selected'], 'Integer') : 0;
+    if ($rowCount < 0) {
+      $rowCount = 0;
+    }
     $contactType = '';
     if ($rgid) {
       $contactType = CRM_Core_DAO::getFieldValue('CRM_Dedupe_DAO_RuleGroup', $rgid, 'contact_type');
     }
 
-    $cacheKeyString = "merge {$contactType}_{$rgid}_{$gid}";
-    $searchRows = array();
-    $selectorElements = array('src', 'dst', 'weight', 'actions');
+    $cacheKeyString   = "merge {$contactType}_{$rgid}_{$gid}";
+    $searchRows       = array();
+    $selectorElements = array('is_selected', 'is_selected_input', 'src_image', 'src', 'src_email', 'src_street', 'src_postcode', 'dst_image', 'dst', 'dst_email', 'dst_street', 'dst_postcode', 'conflicts', 'weight', 'actions');
 
-    $join = "LEFT JOIN civicrm_dedupe_exception de ON ( pn.entity_id1 = de.contact_id1 AND
-                                                             pn.entity_id2 = de.contact_id2 )";
-    $where = "de.id IS NULL";
+    foreach ($_REQUEST['columns'] as $columnInfo) {
+      if (!empty($columnInfo['search']['value'])) {
+        ${$columnInfo['data']} = CRM_Utils_Type::escape($columnInfo['search']['value'], 'String');
+      }
+    }
+    $join  = '';
+    $where = array();
+    $searchData = CRM_Utils_Array::value('search', $_REQUEST);
+    if ($src || !empty($searchData['value']) ) {
+      $src = $src ? $src : $searchData['value'];
+      $where[] = " cc1.display_name LIKE '%{$src}%'";
+    }
+    if ($dst || !empty($searchData['value'])) {
+      $dst = $dst ? $dst : $searchData['value'];
+      $where[] = " cc2.display_name LIKE '%{$dst}%'";
+    }
+    if ($src_email || !empty($searchData['value'])) {
+      $src_email = $src_email ? $src_email : $searchData['value'];
+      $where[] = " (ce1.is_primary = 1 AND ce1.email LIKE '%{$src_email}%')";
+    }
+    if ($dst_email || !empty($searchData['value'])) {
+      $dst_email = $dst_email ? $dst_email : $searchData['value'];
+      $where[] = " (ce2.is_primary = 1 AND ce2.email LIKE '%{$dst_email}%')";
+    }
+    if ($src_postcode || !empty($searchData['value'])) {
+      $src_postcode = $src_postcode ? $src_postcode : $searchData['value'];
+      $where[] = " (ca1.is_primary = 1 AND ca1.postal_code LIKE '%{$src_postcode}%')";
+    }
+    if ($dst_postcode || !empty($searchData['value'])) {
+      $dst_postcode = $dst_postcode ? $dst_postcode : $searchData['value'];
+      $where[] = " (ca2.is_primary = 1 AND ca2.postal_code LIKE '%{$dst_postcode}%')";
+    }
+    if ($src_street || !empty($searchData['value'])) {
+      $src_street = $src_street ? $src_street : $searchData['value'];
+      $where[] = " (ca1.is_primary = 1 AND ca1.street_address LIKE '%{$src_street}%')";
+    }
+    if ($dst_street || !empty($searchData['value'])) {
+      $dst_street = $dst_street ? $dst_street : $searchData['value'];
+      $where[] = " (ca2.is_primary = 1 AND ca2.street_address LIKE '%{$dst_street}%')";
+    }
+    if (!empty($searchData['value'])) {
+      $whereClause   = ' ( '.implode(' OR ', $where).' ) ';
+    }
+    else {
+      if (!empty($where)) {
+        $whereClause  = implode(' AND ', $where);
+      }
+    } 
+    $whereClause .= $whereClause ? ' AND de.id IS NULL' : ' de.id IS NULL';
 
-    $iFilteredTotal = $iTotal = CRM_Core_BAO_PrevNextCache::getCount($cacheKeyString, $join, $where);
-    $mainContacts = CRM_Core_BAO_PrevNextCache::retrieve($cacheKeyString, $join, $where, $offset, $rowCount);
+    if ($selected) {
+      $whereClause .= ' AND pn.is_selected = 1';
+    }
+    $join .= " LEFT JOIN civicrm_dedupe_exception de ON ( pn.entity_id1 = de.contact_id1 AND pn.entity_id2 = de.contact_id2 )";
+
+
+    $select = array(
+      'cc1.contact_type'    => 'src_contact_type',
+      'cc1.display_name'    => 'src_display_name',
+      'cc1.contact_sub_type'=> 'src_contact_sub_type',
+      'cc2.contact_type'    => 'dst_contact_type',
+      'cc2.display_name'    => 'dst_display_name',
+      'cc2.contact_sub_type'=> 'dst_contact_sub_type',
+      'ce1.email'           => 'src_email',
+      'ce2.email'           => 'dst_email',
+      'ca1.postal_code'     => 'src_postcode', 
+      'ca2.postal_code'     => 'dst_postcode',
+      'ca1.street_address'  => 'src_street',
+      'ca2.street_address'  => 'dst_street'
+    );
+
+    if($select) {
+      $join .= " INNER JOIN civicrm_contact cc1 ON cc1.id = pn.entity_id1";
+      $join .= " INNER JOIN civicrm_contact cc2 ON cc2.id = pn.entity_id2";
+      $join .= " LEFT JOIN civicrm_email ce1 ON (ce1.contact_id = pn.entity_id1 AND ce1.is_primary = 1 )";
+      $join .= " LEFT JOIN civicrm_email ce2 ON (ce2.contact_id = pn.entity_id2 AND ce2.is_primary = 1 )";
+      $join .= " LEFT JOIN civicrm_address ca1 ON (ca1.contact_id = pn.entity_id1 AND ca1.is_primary = 1 )";
+      $join .= " LEFT JOIN civicrm_address ca2 ON (ca2.contact_id = pn.entity_id2 AND ca2.is_primary = 1 )";
+    }
+    $iTotal = CRM_Core_BAO_PrevNextCache::getCount($cacheKeyString, $join, $whereClause);
+    foreach ($_REQUEST['order'] as $orderInfo) {
+      if (!empty($orderInfo['column'])) {
+        $orderColumnNumber = $orderInfo['column'];
+        $dir               = $orderInfo['dir'];
+      }
+    }
+    $columnDetails = CRM_Utils_Array::value($orderColumnNumber, $_REQUEST['columns']);
+    if(!empty($columnDetails)) {
+      switch ($columnDetails['data']) {
+      case 'src':
+        $whereClause .= " ORDER BY cc1.display_name {$dir}";
+        break;
+      case 'src_email':
+        $whereClause .= " ORDER BY ce1.email {$dir}";
+        break;
+      case 'src_street':
+        $whereClause .= " ORDER BY ca1.street_address {$dir}";
+        break;
+      case 'src_postcode':
+        $whereClause .= " ORDER BY ca1.postal_code {$dir}";
+        break;
+      case 'dst':
+        $whereClause .= " ORDER BY cc2.display_name {$dir}";
+        break;
+      case 'dst_email':
+        $whereClause .= " ORDER BY ce2.email {$dir}";
+        break;
+      case 'dst_street':
+        $whereClause .= " ORDER BY ca2.street_address {$dir}";
+        break;
+      case 'dst_postcode':
+        $whereClause .= " ORDER BY ca2.postal_code {$dir}";
+        break;
+      default:
+        break;
+      }
+    }
 
-    foreach ($mainContacts as $mainId => $main) {
-      $searchRows[$mainId]['src'] = CRM_Utils_System::href($main['srcName'], 'civicrm/contact/view', "reset=1&cid={$main['srcID']}");
-      $searchRows[$mainId]['dst'] = CRM_Utils_System::href($main['dstName'], 'civicrm/contact/view', "reset=1&cid={$main['dstID']}");
-      $searchRows[$mainId]['weight'] = CRM_Utils_Array::value('weight', $main);
+    $dupePairs = CRM_Core_BAO_PrevNextCache::retrieve($cacheKeyString, $join, $whereClause, $offset, $rowCount, $select);
+    $iFilteredTotal = CRM_Core_DAO::singleValueQuery("SELECT FOUND_ROWS()");
+
+    $count = 0;
+    foreach ($dupePairs as $key => $pairInfo) {
+      $pair =& $pairInfo['data'];
+      $srcContactSubType  = CRM_Utils_Array::value('src_contact_sub_type', $pairInfo);
+      $dstContactSubType  = CRM_Utils_Array::value('dst_contact_sub_type', $pairInfo);
+      $srcTypeImage = CRM_Contact_BAO_Contact_Utils::getImage($srcContactSubType ?
+        $srcContactSubType : $pairInfo['src_contact_type'],
+        FALSE,
+        $pair['srcID']
+      );
+      $dstTypeImage = CRM_Contact_BAO_Contact_Utils::getImage($dstContactSubType ?
+        $dstContactSubType : $pairInfo['dst_contact_type'],
+        FALSE,
+        $pair['dstID']
+      );
 
-      if (!empty($main['canMerge'])) {
-        $mergeParams = "reset=1&cid={$main['srcID']}&oid={$main['dstID']}&action=update&rgid={$rgid}";
+      $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_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_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 ($gid) {
           $mergeParams .= "&gid={$gid}";
         }
 
-        $searchRows[$mainId]['actions'] = '<a class="action-item crm-hover-button" href="' . CRM_Utils_System::url('civicrm/contact/merge', $mergeParams) . '">' . ts('merge') . '</a>';
-        $searchRows[$mainId]['actions'] .= "<a class='action-item crm-hover-button crm-notDuplicate' href='#' onClick=\"processDupes( {$main['srcID']}, {$main['dstID']}, 'dupe-nondupe', 'dupe-listing'); return false;\">" . ts('not a duplicate') . "</a>";
+        $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>";
       }
       else {
-        $searchRows[$mainId]['actions'] = '<em>' . ts('Insufficient access rights - cannot merge') . '</em>';
+        $searchRows[$count]['actions'] = '<em>' . ts('Insufficient access rights - cannot merge') . '</em>';
       }
+      $count++;
     }
 
-    CRM_Utils_System::setHttpHeader('Content-Type', 'application/json');
-    echo CRM_Utils_JSON::encodeDataTableSelector($searchRows, $sEcho, $iTotal, $iFilteredTotal, $selectorElements);
+    header('Content-Type: application/json');
+    echo CRM_Utils_JSON::encodeDataTable($searchRows, $iTotal, $iFilteredTotal, $selectorElements);
 
     CRM_Utils_System::civiExit();
   }
@@ -806,6 +947,41 @@ LIMIT {$offset}, {$rowCount}
     CRM_Utils_JSON::output($addressVal);
   }
 
+  static function toggleDedupeSelect() {
+    $rgid = CRM_Utils_Type::escape($_REQUEST['rgid'], 'Integer');
+    $gid  = CRM_Utils_Type::escape($_REQUEST['gid'], 'Integer');
+    $pnid = $_REQUEST['pnid'];
+    $isSelected = CRM_Utils_Type::escape($_REQUEST['is_selected'], 'Boolean');
+
+    $contactType = CRM_Core_DAO::getFieldValue('CRM_Dedupe_DAO_RuleGroup', $rgid, 'contact_type');
+    $cacheKeyString  = "merge $contactType";
+    $cacheKeyString .= $rgid ? "_{$rgid}" : '_0';
+    $cacheKeyString .= $gid ? "_{$gid}" : '_0';
+
+    $params = array(
+      1 => array($isSelected, 'Boolean'),
+      3 => array("$cacheKeyString%", 'String') // using % to address rows with conflicts as well
+    );
+
+    //check pnid is_array or integer
+    $whereClause = NULL;
+    if (is_array($pnid) && !CRM_Utils_Array::crmIsEmptyArray($pnid)) {
+      $pnid = implode(', ', $pnid);
+      $pnid = CRM_Utils_Type::escape($pnid, 'String');
+      $whereClause = " id IN ( {$pnid} ) ";
+    }
+    else {
+      $pnid = CRM_Utils_Type::escape($pnid, 'Integer');
+      $whereClause = " id = %2";
+      $params[2]   = array($pnid, 'Integer');
+    }
+
+    $sql = "UPDATE civicrm_prevnext_cache SET is_selected = %1 WHERE {$whereClause} AND cacheKey LIKE %3";
+    CRM_Core_DAO::executeQuery($sql, $params);
+
+    CRM_Utils_System::civiExit();
+  }
+
   /**
    * Retrieve contact relationships.
    */
index f401b0140579a18e60b461983c1283cb08226e6a..0129be19d57f0ebbf402f033db28ab20e5c030fc 100644 (file)
@@ -94,7 +94,7 @@ class CRM_Contact_Page_DedupeFind extends CRM_Core_Page_Basic {
     elseif ($action & CRM_Core_Action::MAP) {
       // do a batch merge if requested
       $rgid = CRM_Utils_Request::retrieve('rgid', 'Positive', $this, FALSE, 0);
-      $result = CRM_Dedupe_Merger::batchMerge($rgid, $gid, 'safe', TRUE, TRUE);
+      $result = CRM_Dedupe_Merger::batchMerge($rgid, $gid, 'safe', TRUE, 75);
 
       $skippedCount = CRM_Utils_Request::retrieve('skipped', 'Positive', $this, FALSE, 0);
       $skippedCount = $skippedCount + count($result['skipped']);
@@ -157,6 +157,9 @@ class CRM_Contact_Page_DedupeFind extends CRM_Core_Page_Basic {
       if ($rgid) {
         $sourceParams .= "&rgid={$rgid}";
       }
+      if ($context == 'conflicts') {
+        $sourceParams .= "&selected=1";
+      }
 
       $this->assign('sourceUrl', CRM_Utils_System::url('civicrm/ajax/dedupefind', $sourceParams, FALSE, NULL, FALSE));
 
@@ -165,11 +168,31 @@ class CRM_Contact_Page_DedupeFind extends CRM_Core_Page_Basic {
       $cacheKeyString .= $rgid ? "_{$rgid}" : '_0';
       $cacheKeyString .= $gid ? "_{$gid}" : '_0';
 
+      $stats = CRM_Dedupe_Merger::getMergeStatsMsg($cacheKeyString);
+      if ($stats) {
+        CRM_Core_Session::setStatus($stats);
+        // reset so we not displaying same message again
+        CRM_Dedupe_Merger::resetMergeStats($cacheKeyString);
+      }
       $join = "LEFT JOIN civicrm_dedupe_exception de ON ( pn.entity_id1 = de.contact_id1 AND
                                                                  pn.entity_id2 = de.contact_id2 )";
       $where = "de.id IS NULL";
+      if ($context == 'conflicts') {
+        $where .= " AND pn.is_selected = 1";
+      }
       $this->_mainContacts = CRM_Core_BAO_PrevNextCache::retrieve($cacheKeyString, $join, $where);
       if (empty($this->_mainContacts)) {
+        if ($context == 'conflicts') {
+          // if the current screen was intended to list only selected contacts, move back to full dupe list
+          $sourceParams = 'reset=1&action=update';
+          if ($gid) {
+            $sourceParams .= "&gid={$gid}";
+          }
+          if ($rgid) {
+            $sourceParams .= "&rgid={$rgid}";
+          }
+          CRM_Utils_System::redirect(CRM_Utils_System::url(CRM_Utils_System::currentPath(), $sourceParams));
+        }
         if ($gid) {
           $foundDupes = $this->get("dedupe_dupes_$gid");
           if (!$foundDupes) {
diff --git a/CRM/Contact/Page/DedupeMerge.php b/CRM/Contact/Page/DedupeMerge.php
new file mode 100644 (file)
index 0000000..a3e393f
--- /dev/null
@@ -0,0 +1,129 @@
+<?php
+/*
+ +--------------------------------------------------------------------+
+ | CiviCRM version 4.5                                                |
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC (c) 2004-2014                                |
+ +--------------------------------------------------------------------+
+ | This file is a part of CiviCRM.                                    |
+ |                                                                    |
+ | CiviCRM is free software; you can copy, modify, and distribute it  |
+ | under the terms of the GNU Affero General Public License           |
+ | Version 3, 19 November 2007 and the CiviCRM Licensing Exception.   |
+ |                                                                    |
+ | CiviCRM is distributed in the hope that it will be useful, but     |
+ | WITHOUT ANY WARRANTY; without even the implied warranty of         |
+ | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.               |
+ | See the GNU Affero General Public License for more details.        |
+ |                                                                    |
+ | You should have received a copy of the GNU Affero General Public   |
+ | License and the CiviCRM Licensing Exception along                  |
+ | with this program; if not, contact CiviCRM LLC                     |
+ | at info[AT]civicrm[DOT]org. If you have questions about the        |
+ | GNU Affero General Public License or the licensing of CiviCRM,     |
+ | see the CiviCRM license FAQ at http://civicrm.org/licensing        |
+ +--------------------------------------------------------------------+
+*/
+
+/**
+ *
+ * @package CRM
+ * @copyright CiviCRM LLC (c) 2004-2014
+ * $Id$
+ *
+ */
+class CRM_Contact_Page_DedupeMerge extends CRM_Core_Page{
+
+  const BATCHLIMIT = 2;
+
+  /**
+   * Browse all rule groups
+   *
+   * @return void
+   * @access public
+   */
+  function run() {
+    $runner = self::getRunner();
+    if ($runner) {
+      // Run Everything in the Queue via the Web.
+      $runner->runAllViaWeb();
+    } else {
+      CRM_Core_Session::setStatus(ts('Nothing to merge.'));
+    }
+
+    // parent run
+    return parent::run();
+  }
+
+  static function getRunner() {
+    $rgid = CRM_Utils_Request::retrieve('rgid', 'Positive', $this, FALSE, 0);
+    $gid  = CRM_Utils_Request::retrieve('gid', 'Positive', $this, FALSE, 0);
+    $action = CRM_Utils_Request::retrieve('action', 'String', CRM_Core_DAO::$_nullObject);
+    $mode   = CRM_Utils_Request::retrieve('mode', 'String', CRM_Core_DAO::$_nullObject, FALSE, 'safe');
+
+    $contactType = CRM_Core_DAO::getFieldValue('CRM_Dedupe_DAO_RuleGroup', $rgid, 'contact_type');
+    $cacheKeyString = "merge {$contactType}";
+    $cacheKeyString .= $rgid ? "_{$rgid}" : '_0';
+    $cacheKeyString .= $gid ? "_{$gid}" : '_0';
+
+    // Setup the Queue
+    $queue = CRM_Queue_Service::singleton()->create(array(
+      'name'  => $cacheKeyString,
+      'type'  => 'Sql',
+      'reset' => TRUE,
+    ));
+
+    $where = NULL;
+    if ($action == CRM_Core_Action::MAP) {
+      $where = "pn.is_selected = 1";
+      $isSelected = 1;
+    } else {
+      // else merge all (2)
+      $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.
+      CRM_Utils_System::redirect(CRM_Utils_System::url('civicrm/contact/dedupefind', $urlQry));
+    }
+
+    // reset merge stats, so we compute new stats
+    CRM_Dedupe_Merger::resetMergeStats($cacheKeyString);
+
+    for ($i = 1; $i <= ceil($total/self::BATCHLIMIT); $i++) {
+      $task  = new CRM_Queue_Task(
+        array ('CRM_Contact_Page_DedupeMerge', 'callBatchMerge'),
+        array($rgid, $gid, $mode, TRUE, self::BATCHLIMIT, $isSelected),
+        "Processed " . $i*self::BATCHLIMIT . " pair of duplicates out of " . $total
+      );
+
+      // Add the Task to the Queue
+      $queue->createItem($task);
+    }
+
+    // Setup the Runner
+    $urlQry .= "&context=conflicts";
+    $runner = new CRM_Queue_Runner(array(
+      'title' => ts('Merging Duplicates..'),
+      'queue' => $queue,
+      'errorMode'=> CRM_Queue_Runner::ERROR_ABORT,
+      'onEndUrl' => CRM_Utils_System::url('civicrm/contact/dedupefind', $urlQry, TRUE, NULL, FALSE),
+    ));
+
+    return $runner;
+  }
+
+  /**
+   * Collect Mailchimp data into temporary working table.
+   */
+  static function callBatchMerge(CRM_Queue_TaskContext $ctx, $rgid, $gid = NULL, $mode = 'safe', $autoFlip = TRUE, $batchLimit = 1, $isSelected = 2) {
+    $result = CRM_Dedupe_Merger::batchMerge($rgid, $gid, $mode, $autoFlip, $batchLimit, $isSelected);
+
+    return CRM_Queue_Task::TASK_SUCCESS;
+  }
+}
+
index b9a3e8ea1474bf15dc698c2769a2899c20ebe496..90bc832f07b1d04ed297fffe778a9ea4081156b2 100644 (file)
@@ -153,12 +153,48 @@ WHERE  cacheKey     = %3 AND
 
     if (isset($cacheKey)) {
       $sql .= " AND cacheKey LIKE %4";
-      $params[4] = array("{$cacheKey}%", 'String');
+      $params[4] = array("{$cacheKey}%", 'String'); // used % to address any row with conflict-cacheKey e.g "merge Individual_8_0_conflicts"
     }
 
     CRM_Core_DAO::executeQuery($sql, $params);
   }
 
+  static function markConflict($id1, $id2, $cacheKey, $conflicts) {
+    if (empty($cacheKey) || empty($conflicts)) {
+      return FALSE;
+    }
+
+    $sql  = "SELECT pn.* 
+      FROM  civicrm_prevnext_cache pn
+      WHERE 
+      ((pn.entity_id1 = %1 AND pn.entity_id2 = %2) OR (pn.entity_id1 = %2 AND pn.entity_id2 = %1)) AND 
+      (cacheKey = %3 OR cacheKey = %4)";
+    $params = array(
+      1 => array($id1, 'Integer'),
+      2 => array($id2, 'Integer'),
+      3 => array("{$cacheKey}", 'String'),
+      4 => array("{$cacheKey}_conflicts", 'String'),
+    );
+    $pncFind = CRM_Core_DAO::executeQuery($sql, $params);
+
+    while ($pncFind->fetch()) {
+      $data = $pncFind->data;
+      if (!empty($data)) {
+        $data = unserialize($data);
+        $data['conflicts'] = implode(",", array_keys($conflicts));
+
+        $pncUp = new CRM_Core_DAO_PrevNextCache();
+        $pncUp->id = $pncFind->id;
+        if ($pncUp->find(TRUE)) {
+          $pncUp->data     = serialize($data);
+          $pncUp->cacheKey = "{$cacheKey}_conflicts";
+          $pncUp->save();
+        }
+      }
+    }
+    return TRUE;
+  }
+
   /**
    * @param $cacheKey
    * @param NULL $join
@@ -168,14 +204,25 @@ WHERE  cacheKey     = %3 AND
    *
    * @return array
    */
-  public static function retrieve($cacheKey, $join = NULL, $where = NULL, $offset = 0, $rowCount = 0) {
+  public static function retrieve($cacheKey, $join = NULL, $where = NULL, $offset = 0, $rowCount = 0, $select = array()) {
+    $selectString = 'pn.*';
+    if(!empty($select)) {
+      $aliasArray = array();
+      foreach ($select as $column => $alias) {
+        $aliasArray[] = $column.' as '.$alias;
+      }
+      $selectString .= " , ".implode(' , ', $aliasArray);
+    }
     $query = "
-SELECT data
+SELECT SQL_CALC_FOUND_ROWS {$selectString}
 FROM   civicrm_prevnext_cache pn
        {$join}
-WHERE  cacheKey = %1
+WHERE  (pn.cacheKey = %1 OR pn.cacheKey = %2)
 ";
-    $params = array(1 => array($cacheKey, 'String'));
+    $params = array(
+      1 => array($cacheKey, 'String'),
+      2 => array("{$cacheKey}_conflicts", 'String')
+    );
 
     if ($where) {
       $query .= " AND {$where}";
@@ -190,19 +237,36 @@ WHERE  cacheKey = %1
 
     $dao = CRM_Core_DAO::executeQuery($query, $params);
 
-    $main = array();
+    $main  = array();
+    $count = 0;
     while ($dao->fetch()) {
       if (self::is_serialized($dao->data)) {
-        $main[] = unserialize($dao->data);
+        $main[$count] = unserialize($dao->data);
       }
       else {
-        $main[] = $dao->data;
+        $main[$count] = $dao->data;
+      }
+
+      if (!empty($select)) {
+        $extraData = array();
+        foreach ($select as $dfield => $sfield) {
+          $extraData[$sfield]  = $dao->$sfield;
+        }
+        $main[$count] = array(
+          'prevnext_id' => $dao->id, 
+          'is_selected' => $dao->is_selected, 
+          'entity_id1'  => $dao->entity_id1, 
+          'entity_id2'  => $dao->entity_id2, 
+          'data'        => $main[$count],
+        );
+        $main[$count] = array_merge($main[$count], $extraData);
       }
+      $count++;
     }
 
     return $main;
   }
-
   /**
    * @param $string
    *
@@ -235,13 +299,16 @@ WHERE  cacheKey = %1
     $query = "
 SELECT COUNT(*) FROM civicrm_prevnext_cache pn
        {$join}
-WHERE cacheKey $op %1
+WHERE (pn.cacheKey $op %1 OR pn.cacheKey $op %2)
 ";
     if ($where) {
       $query .= " AND {$where}";
     }
 
-    $params = array(1 => array($cacheKey, 'String'));
+    $params = array(
+      1 => array($cacheKey, 'String'),
+      2 => array("{$cacheKey}_conflicts", 'String')
+    );
     return (int) CRM_Core_DAO::singleValueQuery($query, $params, TRUE, FALSE);
   }
 
index 729452ed1ace019f2ac4336dbd1dc861c2b34cbb..b1ddb7d330ecd2ce15316bb5929ac72c8b51ffe9 100644 (file)
    <page_callback>CRM_Contact_Page_AJAX::getDedupes</page_callback>
    <access_arguments>merge duplicate contacts</access_arguments>
 </item>
+<item>
+  <path>civicrm/contact/dedupemerge</path>
+   <title>Batch Merge Duplicate Contacts</title>
+   <page_callback>CRM_Contact_Page_DedupeMerge</page_callback>
+   <access_arguments>merge duplicate contacts</access_arguments>
+</item>
 <item>
    <path>civicrm/dedupe/exception</path>
    <title>Dedupe Exceptions</title>
      <page_callback>CRM_Contact_Page_AJAX::selectUnselectContacts</page_callback>
      <access_arguments>access CiviCRM</access_arguments>
 </item>
+<item>
+     <path>civicrm/ajax/toggleDedupeSelect</path>
+     <page_callback>CRM_Contact_Page_AJAX::toggleDedupeSelect</page_callback>
+     <access_arguments>merge duplicate contacts</access_arguments>
+</item>
 <item>
   <path>civicrm/activity/sms/add</path>
   <path_arguments>action=add</path_arguments>
index 119164cbbb178b02317546519464ca20059bfadd..1d25f0841038995d823860256e7738b2b5de9a9f 100644 (file)
@@ -569,7 +569,7 @@ INNER JOIN  civicrm_membership membership2 ON membership1.membership_type_id = m
    *
    * @return array|bool
    */
-  public static function batchMerge($rgid, $gid = NULL, $mode = 'safe', $autoFlip = TRUE, $redirectForPerformance = FALSE) {
+  public static function batchMerge($rgid, $gid = NULL, $mode = 'safe', $autoFlip = TRUE, $batchLimit = 1, $isSelected = 2) {
     $contactType = CRM_Core_DAO::getFieldValue('CRM_Dedupe_DAO_RuleGroup', $rgid, 'contact_type');
     $cacheKeyString = "merge {$contactType}";
     $cacheKeyString .= $rgid ? "_{$rgid}" : '_0';
@@ -577,11 +577,16 @@ INNER JOIN  civicrm_membership membership2 ON membership1.membership_type_id = m
     $join = "LEFT JOIN civicrm_dedupe_exception de ON ( pn.entity_id1 = de.contact_id1 AND
                                                              pn.entity_id2 = de.contact_id2 )";
 
-    $limit = $redirectForPerformance ? 75 : 1;
-    $where = "de.id IS NULL LIMIT {$limit}";
+    $where = "de.id IS NULL";
+    if ($isSelected === 0 || $isSelected === 1) {
+      $where .= " AND pn.is_selected = {$isSelected}";
+    }// else consider all dupe pairs
+    $where .= " LIMIT {$batchLimit}";
+
+    $redirectForPerformance = ($batchLimit > 1) ? TRUE : FALSE;
 
     $dupePairs = CRM_Core_BAO_PrevNextCache::retrieve($cacheKeyString, $join, $where);
-    if (empty($dupePairs) && !$redirectForPerformance) {
+    if (empty($dupePairs) && !$redirectForPerformance && $isSelected == 2) {
       // If we haven't found any dupes, probably cache is empty.
       // Try filling cache and give another try.
       CRM_Core_BAO_PrevNextCache::refillCache($rgid, $gid, $cacheKeyString);
@@ -596,6 +601,65 @@ INNER JOIN  civicrm_membership membership2 ON membership1.membership_type_id = m
     return CRM_Dedupe_Merger::merge($dupePairs, $cacheParams, $mode, $autoFlip, $redirectForPerformance);
   }
 
+  static function updateMergeStats($cacheKeyString, $result = array()) {
+    // gather latest stats
+    $merged  = count($result['merged']);
+    $skipped = count($result['skipped']);
+
+    if ($merged <= 0 && $skipped <= 0) {
+      return;
+    }
+
+    // get previous stats
+    $previousStats = CRM_Core_BAO_PrevNextCache::retrieve("{$cacheKeyString}_stats");
+    if (!empty($previousStats)) {
+      if ($previousStats[0]['merged']) {
+        $merged = $merged + $previousStats[0]['merged'];
+      }
+      if ($previousStats[0]['skipped']) {
+        $skipped = $skipped + $previousStats[0]['skipped'];
+      }
+    }
+
+    // delete old stats
+    CRM_Dedupe_Merger::resetMergeStats($cacheKeyString);
+
+    // store the updated stats
+    $data = array(
+      'merged'  => $merged,
+      'skipped' => $skipped,
+    );
+    $data = CRM_Core_DAO::escapeString(serialize($data));
+
+    $values   = array();
+    $values[] = " ( 'civicrm_contact', 0, 0, '{$cacheKeyString}_stats', '$data' ) ";
+    CRM_Core_BAO_PrevNextCache::setItem($values);
+  }
+
+  static function resetMergeStats($cacheKeyString) {
+    return CRM_Core_BAO_PrevNextCache::deleteItem(NULL, "{$cacheKeyString}_stats");
+  }
+
+  static function getMergeStats($cacheKeyString) {
+    $stats = CRM_Core_BAO_PrevNextCache::retrieve("{$cacheKeyString}_stats");
+    if (!empty($stats)) {
+      $stats = $stats[0];
+    }
+    return $stats;
+  }
+
+  static function getMergeStatsMsg($cacheKeyString) {
+    $msg   = '';
+    $stats = CRM_Dedupe_Merger::getMergeStats($cacheKeyString);
+    if (!empty($stats['merged'])) {
+      $msg = "{$stats['merged']} " . ts(' Contact(s) were merged. ');
+    }
+    if (!empty($stats['skipped'])) {
+      $msg .= $stats['skipped'] . ts(' Contact(s) were skipped.');
+    }
+    return $msg;
+  }
+
   /**
    * Merge given set of contacts. Performs core operation.
    *
@@ -656,7 +720,8 @@ INNER JOIN  civicrm_membership membership2 ON membership1.membership_type_id = m
         $migrationInfo['rows'] = &$rowsElementsAndInfo['rows'];
 
         // go ahead with merge if there is no conflict
-        if (!CRM_Dedupe_Merger::skipMerge($mainId, $otherId, $migrationInfo, $mode)) {
+        $conflicts = array();
+        if (!CRM_Dedupe_Merger::skipMerge($mainId, $otherId, $migrationInfo, $mode, $conflicts)) {
           CRM_Dedupe_Merger::moveAllBelongings($mainId, $otherId, $migrationInfo);
           $resultStats['merged'][] = array('main_id' => $mainId, 'other_id' => $otherId);
         }
@@ -664,10 +729,14 @@ INNER JOIN  civicrm_membership membership2 ON membership1.membership_type_id = m
           $resultStats['skipped'][] = array('main_id' => $mainId, 'other_id' => $otherId);
         }
 
-        // delete entry from PrevNextCache table so we don't consider the pair next time
-        // pair may have been flipped, so make sure we delete using both orders
-        CRM_Core_BAO_PrevNextCache::deletePair($mainId, $otherId, $cacheKeyString);
-        CRM_Core_BAO_PrevNextCache::deletePair($otherId, $mainId, $cacheKeyString);
+        // store  any conflicts
+        if (!empty($conflicts)) {
+          CRM_Core_BAO_PrevNextCache::markConflict($mainId, $otherId, $cacheKeyString, $conflicts);
+        } else {
+          // delete entry from PrevNextCache table so we don't consider the pair next time
+          // pair may have been flipped, so make sure we delete using both orders
+          CRM_Core_BAO_PrevNextCache::deletePair($mainId, $otherId, $cacheKeyString, TRUE);
+        }
 
         CRM_Core_DAO::freeResult();
         unset($rowsElementsAndInfo, $migrationInfo);
@@ -685,6 +754,8 @@ INNER JOIN  civicrm_membership membership2 ON membership1.membership_type_id = m
         unset($dupePairs);
       }
     }
+
+    CRM_Dedupe_Merger::updateMergeStats($cacheKeyString, $resultStats);
     return $resultStats;
   }
 
@@ -705,8 +776,8 @@ INNER JOIN  civicrm_membership membership2 ON membership1.membership_type_id = m
    *
    * @return bool
    */
-  public static function skipMerge($mainId, $otherId, &$migrationInfo, $mode = 'safe') {
-    $conflicts = array();
+  public static function skipMerge($mainId, $otherId, &$migrationInfo, $mode = 'safe', &$conflicts = array()) {
+    //$conflicts = array();
     $migrationData = array(
       'old_migration_info' => $migrationInfo,
       'mode' => $mode,
@@ -787,11 +858,13 @@ INNER JOIN  civicrm_membership membership2 ON membership1.membership_type_id = m
     $migrationData['fields_in_conflict'] = $conflicts;
     CRM_Utils_Hook::merge('batch', $migrationData, $mainId, $otherId);
     $conflicts = $migrationData['fields_in_conflict'];
+    // allow hook to override / manipulate migrationInfo as well
+    $migrationInfo = $migrationData['old_migration_info'];
 
     if (!empty($conflicts)) {
       foreach ($conflicts as $key => $val) {
         if ($val === NULL and $mode == 'safe') {
-          // un-resolved conflicts still present. Lets skip this merge.
+          // un-resolved conflicts still present. Lets skip this merge after saving the conflict / reason.
           return TRUE;
         }
         else {
index 01e0b60bbf32f4fd108d0ad2a2611aad7c50c037..169ac62924437c7ea5756367362ff470b764a50d 100644 (file)
@@ -94,4 +94,37 @@ class CRM_Utils_JSON {
     return $sOutput;
   }
 
+  /**
+   * This function is used to encode data for new dataTable plugin v1.10 and greater
+   * @return string
+   *
+   */
+  static function encodeDataTable($params, $iTotal, $iFilteredTotal, $selectorElements) {
+    $sOutput = '{';
+    $sOutput .= '"recordsTotal": ' . $iTotal . ', ';
+    $sOutput .= '"recordsFiltered": ' . $iFilteredTotal . ', ';
+    $sOutput .= '"data": [ ';
+    foreach ($params as $key => $value) {
+      $addcomma = FALSE;
+      $sOutput .= "{";
+      foreach ($selectorElements as $element) {
+        if ($addcomma) {
+          $sOutput .= ",";
+        }
+        //CRM-7130 --lets addslashes to only double quotes,
+        //since we are using it to quote the field value.
+        //str_replace helps to provide a break for new-line
+        $sOutput .= '"' . $element . '":' . '"' . addcslashes(str_replace(array("\r\n", "\n", "\r"), '<br />', $value[$element]), '"\\') . '"';
+
+        //remove extra spaces and tab character that breaks dataTable CRM-12551
+        $sOutput  = preg_replace("/\s+/", " ", $sOutput);
+        $addcomma = TRUE;
+      }
+      $sOutput .= "},";
+    }
+    $sOutput = substr_replace($sOutput, "", -1);
+    $sOutput .= '] }';
+
+    return $sOutput;
+  }
 }
index bd07359313984a8b3a7393d909399a8f5c848318..76f380bdbb52c03b2d3e0facbafd3e64791f32f8 100644 (file)
 *}
 {if $action eq 2 || $action eq 16}
 <div class="form-item">
-  <table class='pagerDisplay'>
+  <div class="crm-accordion-wrapper crm-search_filters-accordion">
+    <div class="crm-accordion-header">
+    {ts}Filter Contacts{/ts}</a>
+    </div><!-- /.crm-accordion-header -->
+    <div class="crm-accordion-body">
+      <table class="no-border form-layout-compressed" id="searchOptions" style="width:100%;">
+        <tr>
+          <td class="crm-contact-form-block-contact1">
+            <label for="contact1">{ts}Contact 1{/ts}</label><br />
+            <input type="text" placeholder="Search Contact1" search-column="2" />
+          </td>
+          <td class="crm-contact-form-block-contact2">
+            <label for="contact2">{ts}Contact 2{/ts}</label><br />
+            <input type="text" placeholder="Search Contact2" search-column="5" />
+          </td>
+          <td class="crm-contact-form-block-email1">
+            <label for="email1">{ts}Email 1{/ts}</label><br />
+            <input type="text" placeholder="Search Email1" search-column="3" />
+          </td>
+          <td class="crm-contact-form-block-email2">
+            <label for="email2">{ts}Email 2{/ts}</label><br />
+            <input type="text" placeholder="Search Email2" search-column="6" />
+          </td>
+        </tr>
+        <tr>
+          <td class="crm-contact-form-block-street-address1">
+            <label for="street-adddress1">{ts}Street Address 1{/ts}</label><br />
+            <input type="text" placeholder="Search Street Address1" search-column="7" />
+          </td>
+          <td class="crm-contact-form-block-street-address2">
+            <label for="street-adddress2">{ts}Street Address 2{/ts}</label><br />
+            <input type="text" placeholder="Search Street Address2" search-column="8" />
+          </td>
+          <td class="crm-contact-form-block-postcode1">
+            <label for="postcode1">{ts}Postcode 1{/ts}</label><br />
+            <input type="text" placeholder="Search Postcode1" search-column="9" />
+          </td>
+          <td class="crm-contact-form-block-postcode2">
+            <label for="postcode2">{ts}Postcode 2{/ts}</label><br />
+            <input type="text" placeholder="Search Postcode2" search-column="10" />
+          </td>
+        </tr>
+      </table>
+    </div><!-- /.crm-accordion-body -->
+  </div><!-- /.crm-accordion-wrapper -->
+  <div>
+    Show / Hide columns:
+    <input type='checkbox' id ='steet-address' class='toggle-vis' data-column-main="7" data-column-dupe="8" >  
+        <label for="steet-address">{ts}Street Address{/ts}&nbsp;</label>
+    <input type='checkbox' id ='post-code' class='toggle-vis' data-column-main="9" data-column-dupe="10" >  
+        <label for="post-code">{ts}Post Code{/ts}&nbsp;</label>
+    <input type='checkbox' id ='conflicts' class='toggle-vis' data-column-main="11"  >  
+        <label for="conflicts">{ts}Conflicts{/ts}&nbsp; </label>
+    <input type='checkbox' id ='threshold' class='toggle-vis' data-column-main="12"  >  
+        <label for="threshold">{ts}Threshold{/ts}&nbsp;</label>
+  </div><br/>
+  <span id="dupePairs_length_selection">
+    <input type='checkbox' id ='crm-dedupe-display-selection' name="display-selection">
+    <label for="display-selection">{ts}Within Selections{/ts}&nbsp;</label>  
+  </span>
+  <table id="dupePairs" class="nestedActivitySelector form-layout-compressed" cellspacing="0" width="100%">
     <thead>
-    <tr class="columnheader"><th id="nosort">{ts}Contact{/ts} 1</th><th id="nosort">{ts}Contact{/ts} 2 ({ts}Duplicate{/ts})</th><th id="nosort">{ts}Threshold{/ts}</th><th id="nosort">&nbsp;</th></tr>
+      <tr class="columnheader"> 
+        <th class="crm-dedupe-selection"><input type="checkbox" value="0" name="pnid_all" class="crm-dedupe-select-all"></th>
+        <th class="crm-empty">&nbsp;</th>
+        <th class="crm-contact">{ts}Contact{/ts} 1</th>
+        <th class="crm-empty">&nbsp;</th>
+        <th class="crm-contact-duplicate">{ts}Contact{/ts} 2 ({ts}Duplicate{/ts})</th>
+        <th class="crm-contact">{ts}Email{/ts} 1</th>
+        <th class="crm-contact-duplicate">{ts}Email{/ts} 2 ({ts}Duplicate{/ts})</th>
+        <th class="crm-contact">{ts}Street Address{/ts} 1</th>
+        <th class="crm-contact-duplicate">{ts}Street Address{/ts} 2 ({ts}Duplicate{/ts})</th>
+        <th class="crm-contact">{ts}Postcode{/ts} 1</th>
+        <th class="crm-contact-duplicate">{ts}Postcode{/ts} 2 ({ts}Duplicate{/ts})</th>
+        <th class="crm-contact-conflicts">{ts}Conflicts{/ts}</th>
+        <th class="crm-threshold">{ts}Threshold{/ts}</th>
+        <th class="crm-empty">&nbsp;</th>
+      </tr>
     </thead>
+    <tbody>
+    </tbody>
   </table>
-  {include file="CRM/common/jsortable.tpl" sourceUrl=$sourceUrl useAjax=1 }
   {if $cid}
     <table style="width: 45%; float: left; margin: 10px;">
       <tr class="columnheader"><th colspan="2">{ts 1=$main_contacts[$cid]}Merge %1 with{/ts}</th></tr>
 
 {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 $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}
+   <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}
+   {else}
+      {capture assign=backURL}{crmURL p="civicrm/contact/dedupefind" q="reset=1&action=update&rgid=`$rgid`" a=1}{/capture}
+   {/if}
+   <a href="{$backURL}" title="{ts}List All Duplicates{/ts}" class="button"><span>{ts}List All Duplicates{/ts}</span></a>
 {else}
    {if $gid}
       {capture assign=backURL}{crmURL p="civicrm/contact/dedupefind" q="reset=1&rgid=`$rgid`&gid=`$gid`&action=renew" a=1}{/capture}
    </a>
 
    {if $gid}
-      {capture assign=backURL}{crmURL p="civicrm/contact/dedupefind" q="reset=1&rgid=`$rgid`&gid=`$gid`&action=map" a=1}{/capture}
+      {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/dedupefind" q="reset=1&rgid=`$rgid`&action=map" a=1}{/capture}
+      {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 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 Duplicates{/ts}</span>
-   </a>
+   <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><div class="icon ui-icon-script"></div>{ts}Batch Merge Selected Duplicates{/ts}</span></a>
+
+   {if $gid}
+      {capture assign=backURL}{crmURL p="civicrm/contact/dedupemerge" q="reset=1&rgid=`$rgid`&gid=`$gid`" a=1}{/capture}
+   {else}
+      {capture assign=backURL}{crmURL p="civicrm/contact/dedupemerge" q="reset=1&rgid=`$rgid`" 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 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>
 
    {capture assign=backURL}{crmURL p="civicrm/contact/deduperules" q="reset=1" a=1}{/capture}
    <a href="{$backURL}" class="button crm-button-type-cancel">
 {include file='CRM/common/dedupe.tpl'}
 {literal}
 <script type="text/javascript">
-var oTable = null;
+CRM.$(function($) {
+  var sourceUrl = {/literal}'{$sourceUrl}'{literal};
+  var context   = {/literal}'{$context}'{literal};
+  $('#dupePairs').DataTable({
+    //"scrollX": true, // doesn't work with hover popup for for icons
+    "lengthMenu": [[10, 25, 50, 100, 1000, 2000, -1], [10, 25, 50, 100, 1000, 2000, "All"]],
+    "dom": 'flrtip',
+    "processing": true,
+    "serverSide": true,
+    "ajax": sourceUrl,
+    "columns"  : [
+      {data: "is_selected_input"},
+      {data: "src_image"},
+      {data: "src"},
+      {data: "dst_image"},
+      {data: "dst"},
+      {data: "src_email"},
+      {data: "dst_email"},
+      {data: "src_street"},
+      {data: "dst_street"},
+      {data: "src_postcode"},
+      {data: "dst_postcode"},
+      {data: "conflicts"},
+      {data: "weight"},
+      {data: "actions"},
+    ],
+    "order": [], // removes default order by for column 1 (checkbox)
+    "columnDefs": [ 
+      {
+        "targets": [0, 1, 3, 13],
+        "orderable": false
+      },
+      {
+        "targets": [7, 8, 9, 10, 11, 12],
+        "visible": false
+      }
+    ],
+    rowCallback: function (row, data) {
+      // Set the checked state of the checkbox in the table
+      $('input.crm-dedupe-select', row).prop('checked', data.is_selected == 1);
+      if (data.is_selected == 1) {
+        $(row).toggleClass('crm-row-selected');
+      }
+      // for action column at the last, set nowrap
+      $('td:last', row).attr('nowrap','nowrap');
+    }
+  });
+
+  // redraw datatable if searching within selected records
+  $('#crm-dedupe-display-selection').on('click', function(){
+    reloadUrl = sourceUrl;
+    if($(this).prop('checked')){
+      reloadUrl = sourceUrl+'&selected=1';
+    }
+    $('#dupePairs').DataTable().ajax.url(reloadUrl).draw();
+  });
+
+  $('#dupePairs_length_selection').appendTo('#dupePairs_length');
+  
+  // apply selected class on click of a row
+  $('#dupePairs tbody').on('click', 'tr', function() {
+    $(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);
+  });
+  
+  $('#dupePairs thead tr .crm-dedupe-selection').on('click', function() {
+    var checked = $('.crm-dedupe-select-all').prop('checked');
+    if (checked) {
+      $("#dupePairs tbody tr input[type='checkbox']").prop('checked', true);
+      $("#dupePairs tbody tr").addClass('crm-row-selected');
+    }
+    else{
+      $("#dupePairs tbody tr input[type='checkbox']").prop('checked', false);
+      $("#dupePairs tbody tr").removeClass('crm-row-selected');
+    }
+    var ele = $('#dupePairs tbody tr');
+    toggleDedupeSelect(ele, 1);
+  });
+    
+  // inline search boxes placed in tfoot
+  $('#dupePairsColFilters thead th').each( function () {
+    var title = $('#dupePairs thead th').eq($(this).index()).text();
+    if (title.length > 1) {
+      $(this).html( '<input type="text" placeholder="Search '+title+'" />' );
+    }
+  });
+
+  // apply dataTable
+  var table = $('#dupePairs').DataTable();
+
+  // apply the search
+  $('#searchOptions input').on( 'keyup change', function () {
+    table
+      .column($(this).attr('search-column'))
+      .search(this.value)
+      .draw();
+  });
+
+  // show / hide columns
+  $('input.toggle-vis').on('click', function (e) {
+    var column = table.column( $(this).attr('data-column-main') );
+    column.visible( ! column.visible() );
+
+    if ($(this).attr('data-column-dupe')) {
+      column = table.column( $(this).attr('data-column-dupe') );
+      column.visible( ! column.visible() );
+    }
+  });
+  
+  if(context == 'conflicts') {
+    $('#conflicts').attr('checked', true);  
+    var column = table.column( $('#conflicts').attr('data-column-main') );
+    column.visible( ! column.visible() );
+  }
+});
+
+function toggleDedupeSelect(element, isMultiple) {
+  if (!isMultiple) {
+    var is_selected = CRM.$(element).prop('checked') ? 1: 0;
+    var id = CRM.$(element).prop('name').substr(5);
+  }
+  else {
+    var id = [];
+    CRM.$(element).each(function() {
+      var sth = CRM.$('input.crm-dedupe-select', this);
+      id.push(CRM.$(sth).prop('name').substr(5));
+    });
+    var is_selected = CRM.$('.crm-dedupe-select-all').prop('checked') ? 1 : 0;
+  }
+
+  var dataUrl = {/literal}"{crmURL p='civicrm/ajax/toggleDedupeSelect' h=0 q='snippet=4'}"{literal};
+  var rgid = {/literal}"{$rgid}"{literal};
+  var gid = {/literal}"{$gid}"{literal};
+
+  rgid = rgid.length > 0 ? rgid : 0;
+  gid  = gid.length > 0 ? gid : 0;
+  
+  CRM.$.post(dataUrl, {pnid: id, rgid: rgid, gid: gid, is_selected: is_selected}, function (data) {
+    // nothing to do for now
+  }, 'json');
+}
 </script>
 {/literal}
diff --git a/templates/CRM/Contact/Page/DedupeMerge.tpl b/templates/CRM/Contact/Page/DedupeMerge.tpl
new file mode 100644 (file)
index 0000000..a270e4f
--- /dev/null
@@ -0,0 +1,25 @@
+{*
+ +--------------------------------------------------------------------+
+ | CiviCRM version 4.5                                                |
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC (c) 2004-2014                                |
+ +--------------------------------------------------------------------+
+ | This file is a part of CiviCRM.                                    |
+ |                                                                    |
+ | CiviCRM is free software; you can copy, modify, and distribute it  |
+ | under the terms of the GNU Affero General Public License           |
+ | Version 3, 19 November 2007 and the CiviCRM Licensing Exception.   |
+ |                                                                    |
+ | CiviCRM is distributed in the hope that it will be useful, but     |
+ | WITHOUT ANY WARRANTY; without even the implied warranty of         |
+ | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.               |
+ | See the GNU Affero General Public License for more details.        |
+ |                                                                    |
+ | You should have received a copy of the GNU Affero General Public   |
+ | License and the CiviCRM Licensing Exception along                  |
+ | with this program; if not, contact CiviCRM LLC                     |
+ | at info[AT]civicrm[DOT]org. If you have questions about the        |
+ | GNU Affero General Public License or the licensing of CiviCRM,     |
+ | see the CiviCRM license FAQ at http://civicrm.org/licensing        |
+ +--------------------------------------------------------------------+
+*}