* Addressbook - new view to show duplicate contacts

This commit is contained in:
nathangray 2017-03-13 12:11:37 -06:00
parent f943ed471a
commit 23bf37b98e
6 changed files with 599 additions and 146 deletions

View File

@ -150,6 +150,11 @@ class addressbook_hooks
);
$contacts = new Api\Contacts();
$fileas_options = $contacts->fileas_options();
foreach(Api\Contacts\Storage::$duplicate_fields as $key => $label)
{
$duplicate_options[$key] = lang($label);
}
$settings['link_title'] = array(
'type' => 'select',
'label' => 'Link title for contacts show',
@ -197,6 +202,26 @@ class addressbook_hooks
'admin' => false,
'default'=> 'org_name: n_family, n_given',
);
$settings['duplicate_fields'] = array(
'type' => 'multiselect',
'label' => 'Fields to check for duplicates',
'name' => 'duplicate_fields',
'values' => $duplicate_options,
'help' => 'Fields to consider when looking for duplicate contacts.',
'admin' => false,
'default' => 'n_family, n_given, org_name, contact_email'
);
$settings['duplicate_threshold'] = array(
'type' => 'input',
'size' => 5,
'label' => 'Duplicate threshold',
'name' => 'duplicate_threshold',
'help' => 'How many fields must match for the record to be considered a duplicate.',
'xmlrpc' => True,
'default'=> 3,
'admin' => False
);
$crm_list_options = array(
'~edit~' => lang('Edit contact'),
'infolog' => lang('Open %1 CRM view', lang('infolog')),

View File

@ -85,10 +85,11 @@ class addressbook_ui extends addressbook_bo
$this->tmpl = new Etemplate();
$this->org_views = array(
$this->grouped_views = array(
'org_name' => lang('Organisations'),
'org_name,adr_one_locality' => lang('Organisations by location'),
'org_name,org_unit' => lang('Organisations by departments'),
'duplicates' => lang('Duplicates')
);
// make sure the hook for export_limit is registered
@ -149,9 +150,10 @@ class addressbook_ui extends addressbook_bo
{
$msg = lang('You need to select some contacts first');
}
elseif ($_content['nm']['action'] == 'view_org') // org-view via context menu
elseif ($_content['nm']['action'] == 'view_org' || $_content['nm']['action'] == 'view_duplicates')
{
$_content['nm']['org_view'] = array_shift($_content['nm']['selected']);
// grouped view via context menu
$_content['nm']['grouped_view'] = array_shift($_content['nm']['selected']);
}
else
{
@ -177,11 +179,11 @@ class addressbook_ui extends addressbook_bo
}
if ($_content['nm']['rows']['view']) // show all contacts of an organisation
{
list($org_view) = each($_content['nm']['rows']['view']);
list($grouped_view) = each($_content['nm']['rows']['view']);
}
else
{
$org_view = $_content['nm']['org_view'];
$grouped_view = $_content['nm']['grouped_view'];
}
$typeselection = $_content['nm']['col_filter']['tid'];
}
@ -245,7 +247,7 @@ class addressbook_ui extends addressbook_bo
//'actions' => $this->get_actions(), // set on each request, as it depends on some filters
'row_id' => 'id',
'row_modified' => 'modified',
'is_parent' => 'org_count',
'is_parent' => 'group_count',
'parent_id' => 'parent_id',
'favorites' => true,
'placeholder_actions' => array('add')
@ -338,27 +340,25 @@ class addressbook_ui extends addressbook_bo
{
$this->tmpl->disableElement('nm[col_filter][tid]');
}
// get the availible org-views plus the label of the contacts view of one org
$sel_options['org_view'] = $this->org_views;
if (isset($org_view))
// get the availible grouped-views plus the label of the contacts view of one group
$sel_options['grouped_view'] = $this->grouped_views;
if (isset($grouped_view))
{
$content['nm']['org_view'] = $org_view;
$content['nm']['grouped_view'] = $grouped_view;
}
$content['nm']['actions'] = $this->get_actions($content['nm']['col_filter']['tid'], $content['nm']['org_view']);
$content['nm']['actions'] = $this->get_actions($content['nm']['col_filter']['tid']);
if (!isset($sel_options['org_view'][(string) $content['nm']['org_view']]))
if (!isset($sel_options['grouped_view'][(string) $content['nm']['grouped_view']]))
{
$sel_options['org_view'] += $this->_get_org_name((string)$content['nm']['org_view']);
$sel_options['grouped_view'] += $this->_get_grouped_name((string)$content['nm']['grouped_view']);
}
// unset the filters regarding organisations, when there is no organisation selected
if (empty($sel_options['org_view'][(string) $content['nm']['org_view']]) || stripos($org_view,":") === false )
// unset the filters regarding grouped views, when there is no group selected
if (empty($sel_options['grouped_view'][(string) $content['nm']['grouped_view']]) || stripos($grouped_view,":") === false )
{
unset($content['nm']['col_filter']['org_name']);
unset($content['nm']['col_filter']['org_unit']);
unset($content['nm']['col_filter']['adr_one_locality']);
$this->unset_grouped_filters($content['nm']);
}
$content['nm']['org_view_label'] = $sel_options['org_view'][(string) $content['nm']['org_view']];
$content['nm']['grouped_view_label'] = $sel_options['grouped_view'][(string) $content['nm']['grouped_view']];
$this->tmpl->read($do_email ? 'addressbook.email' : 'addressbook.index');
return $this->tmpl->exec($do_email ? 'addressbook.addressbook_ui.emailpopup' : 'addressbook.addressbook_ui.index',
@ -381,7 +381,7 @@ class addressbook_ui extends addressbook_bo
'allowOnMultiple' => false,
'group' => $group=1,
'onExecute' => 'javaScript:app.addressbook.view',
'disableClass' => 'contact_organisation',
'enableClass' => 'contact_contact',
'hideOnDisabled' => true,
// Children added below
'children' => array(),
@ -391,7 +391,7 @@ class addressbook_ui extends addressbook_bo
'caption' => 'Open',
'default' => $GLOBALS['egw_info']['user']['preferences']['addressbook']['crm_list'] == '~edit~',
'allowOnMultiple' => false,
'disableClass' => 'contact_organisation',
'enableClass' => 'contact_contact',
'hideOnDisabled' => true,
'url' => 'menuaction=addressbook.addressbook_ui.edit&contact_id=$id',
'popup' => Link::get_registry('addressbook', 'edit_popup'),
@ -400,7 +400,7 @@ class addressbook_ui extends addressbook_bo
'add' => array(
'caption' => 'Add',
'group' => $group,
'disableClass' => 'contact_organisation',
'enableClass' => 'contact_contact',
'hideOnDisabled' => true,
'children' => array(
'new' => array(
@ -445,20 +445,39 @@ class addressbook_ui extends addressbook_bo
'default' => true,
'allowOnMultiple' => false,
'group' => $group=1,
'disableClass' => 'contact_contact',
'enableClass' => 'contact_organisation',
'hideOnDisabled' => true
),
'add_org' => array(
'caption' => 'Add',
'group' => $group,
'allowOnMultiple' => false,
'disableClass' => 'contact_contact',
'enableClass' => 'contact_organisation',
'hideOnDisabled' => true,
'url' => 'menuaction=addressbook.addressbook_ui.edit&org=$id',
'popup' => Link::get_registry('addressbook', 'add_popup'),
),
);
// Duplicates view
$actions += array(
'view_duplicates' => array(
'caption' => 'View',
'default' => true,
'allowOnMultiple' => false,
'group' => $group=1,
'enableClass' => 'contact_duplicate',
'hideOnDisabled' => true
),
'merge_duplicates' => array(
'caption' => 'Merge duplicates',
'group' => $group,
'allowOnMultiple' => true,
'enableClass' => 'contact_duplicate',
'hideOnDisabled' => true
)
);
++$group; // other AB related stuff group: lists, AB's, categories
// categories submenu
$actions['cat'] = array(
@ -587,6 +606,7 @@ class addressbook_ui extends addressbook_bo
'caption' => lang('View linked InfoLog entries'),
'icon' => 'infolog/navbar',
'onExecute' => 'javaScript:app.addressbook.view_infolog',
'enableClass' => 'contact_contact',
'allowOnMultiple' => true,
'hideOnDisabled' => true,
),
@ -607,6 +627,7 @@ class addressbook_ui extends addressbook_bo
'icon' => 'calendar/navbar',
'caption' => 'Calendar',
'group' => $group,
'enableClass' => 'contact_contact',
'children' => array(
'calendar_view' => array(
'caption' => 'Show',
@ -629,7 +650,7 @@ class addressbook_ui extends addressbook_bo
$actions['email'] = array(
'caption' => 'Email',
'icon' => 'mail/navbar',
'disableClass' => 'contact_organisation',
'enableClass' => 'contact_contact',
'hideOnDisabled' => true,
'group' => $group,
'children' => array(
@ -681,8 +702,8 @@ class addressbook_ui extends addressbook_bo
'url' => 'menuaction=filemanager.filemanager_ui.index&path=/apps/addressbook/$id&ajax=true',
'allowOnMultiple' => false,
'group' => $group,
// disable for for org-views, as it needs contact-ids
'disableClass' => 'contact_organisation',
// disable for for group-views, as it needs contact-ids
'enableClass' => 'contact_contact',
'hideOnMobile' => true
);
}
@ -691,6 +712,7 @@ class addressbook_ui extends addressbook_bo
'caption' => 'GeoLocation',
'icon' => 'map',
'group' => ++$group,
'enableClass' => 'contact_contact',
'children' => array (
'private' => array(
'caption' => 'Private Address',
@ -713,6 +735,7 @@ class addressbook_ui extends addressbook_bo
$actions['export'] = array(
'caption' => 'Export',
'icon' => 'filesave',
'enableClass' => 'contact_contact',
'group' => ++$group,
'children' => array(
'csv' => array(
@ -742,7 +765,7 @@ class addressbook_ui extends addressbook_bo
'icon' => 'filemanager/mail_post_to',
'group' => $group,
'onExecute' => 'javaScript:app.addressbook.adb_mail_vcard',
'disableClass' => 'contact_organisation',
'enableClass' => 'contact_contact',
'hideOnDisabled' => true,
'hideOnMobile' => true
);
@ -807,22 +830,99 @@ class addressbook_ui extends addressbook_bo
}
/**
* Get the name of an organization from an ID for the org_view filter
* Get a nice name for the given grouped view ID
*
* @param string $org
* @return Array ID => name
* @param String $view_id Some kind of indicator for a specific group, either
* organisation or duplicate. It looks like key:value pairs seperated by |||.
*
* @return Array(ID => name), where ID is the $view_id passed in
*/
private function _get_org_name($org)
protected function _get_grouped_name($view_id)
{
$org_name = array();
if (strpos($org,'*AND*')!== false) $org = str_replace('*AND*','&',$org);
foreach(explode('|||',$org) as $part)
$group_name = array();
if (strpos($view_id,'*AND*')!== false) $view_id = str_replace('*AND*','&',$view_id);
foreach(explode('|||',$view_id) as $part)
{
list(,$name) = explode(':',$part,2);
if ($name) $org_name[] = $name;
if ($name) $group_name[] = $name;
}
$name = implode(': ',$org_name);
return $name ? array($org => $name) : array();
$name = implode(': ',$group_name);
return $name ? array($view_id => $name) : array();
}
/**
* Unset the relevant column filters to clear a grouped view
*
* @param Array $query
*/
protected function unset_grouped_filters(&$query)
{
unset($query['col_filter']['org_name']);
unset($query['col_filter']['org_unit']);
unset($query['col_filter']['adr_one_locality']);
foreach(static::$duplicate_fields as $field => $label)
{
unset($query['col_filter'][$field]);
}
}
/**
* Adjust the query as needed and get the rows for the grouped views (organisation
* or duplicate contacts)
*
* @param Array $query Nextmatch query
* @return array rows found
*/
protected function get_grouped_rows(&$query)
{
// Query doesn't like empties
unset($query['col_filter']['parent_id']);
if($query['actions'] && $query['actions']['open'])
{
// Just switched from contact view, update actions
$query['actions'] = $this->get_actions($query['col_filter']['tid']);
}
unset($query['col_filter']['list']); // does not work together
$query['no_filter2'] = true; // switch the distribution list selection off
$query['template'] = $query['grouped_view'] == 'duplicates' ? 'addressbook.index.duplicate_rows' : 'addressbook.index.org_rows';
if ($query['advanced_search'])
{
$query['op'] = $query['advanced_search']['operator'];
unset($query['advanced_search']['operator']);
$query['wildcard'] = $query['advanced_search']['meth_select'];
unset($query['advanced_search']['meth_select']);
$original_search = $query['search'];
$query['search'] = $query['advanced_search'];
}
switch ($query['template'])
{
case 'addressbook.index.org_rows':
if ($query['order'] != 'org_name')
{
$query['sort'] = 'ASC';
$query['order'] = 'org_name';
}
$query['org_view'] = $query['grouped_view'];
$rows = parent::organisations($query);
break;
case 'addressbook.index.duplicate_rows':
$rows = parent::duplicates($query);
break;
}
if ($query['advanced_search'])
{
$query['search'] = $original_search;
unset($query['wildcard']);
unset($query['op']);
}
$GLOBALS['egw_info']['flags']['params']['manual'] = array('page' => 'ManualAddressbookIndexOrga');
return $rows;
}
/**
@ -886,7 +986,7 @@ window.egw_LAB.wait(function() {
if(!is_array($org)) $org = array($org);
foreach($org as $org_name)
{
$query['org_view'] = $org_name;
$query['grouped_view'] = $org_name;
$checked = array();
$readonlys = null;
$this->get_rows($query,$checked,$readonlys,true); // true = only return the id's
@ -907,7 +1007,7 @@ window.egw_LAB.wait(function() {
{
$query = Api\Cache::getSession('addressbook', 'index');
$query['num_rows'] = -1; // all
$query['org_view'] = $org;
$query['grouped_view'] = $org;
$query['searchletter'] = '';
$checked = $readonlys = null;
$this->get_rows($query,$checked,$readonlys,true); // true = only return the id's
@ -1047,26 +1147,8 @@ window.egw_LAB.wait(function() {
}
}
// replace org_name:* id's with all id's of that org
$org_contacts = array();
foreach((array)$checked as $n => $id)
{
if (substr($id,0,9) == 'org_name:')
{
if (count($checked) == 1 && !count($org_contacts) && $action == 'infolog')
{
return $this->infolog_org_view($id); // uses the org-name, instead of 'selected contacts'
}
unset($checked[$n]);
$query = Api\Cache::getSession('addressbook', $session_name);
$query['num_rows'] = -1; // all
$query['org_view'] = $id;
unset($query['filter2']);
$extra = $readonlys = null;
$this->get_rows($query,$extra,$readonlys,true); // true = only return the id's
if ($extra[0]) $org_contacts = array_merge($org_contacts,$extra);
}
}
if ($org_contacts) $checked = array_unique($checked ? array_merge($checked,$org_contacts) : $org_contacts);
$grouped_contacts = $this->find_grouped_ids($action, $checked, $use_all, $success,$failed,$action_msg,$session_name);
if ($grouped_contacts) $checked = array_unique($checked ? array_merge($checked,$grouped_contacts) : $grouped_contacts);
//_debug_array($checked); exit;
if (substr($action,0,8) == 'move_to_')
@ -1338,6 +1420,59 @@ window.egw_LAB.wait(function() {
return !$failed;
}
/**
* Find the individual contact IDs for a list of grouped contacts
*
* Successful lookups are removed from the checked array.
*
* Used for action on organisation and duplicate views
* @param string/int $action 'delete', 'vcard', 'csv' or nummerical account_id to move contacts to that addessbook
* @param array $checked contact id's to use if !$use_all
* @param boolean $use_all if true use all contacts of the current selection (in the session)
* @param int &$success number of succeded actions
* @param int &$failed number of failed actions (not enought permissions)
* @param string &$action_msg translated verb for the actions, to be used in a message like %1 contacts 'deleted'
* @param string/array $session_name 'index' or 'email', or array with session-data depending if we are in the main list or the popup
*
* @return array List of contact IDs in the provided groups
*/
protected function find_grouped_ids($action,&$checked,$use_all,&$success,&$failed,&$action_msg,$session_name,&$msg)
{
$grouped_contacts = array();
foreach((array)$checked as $n => $id)
{
if (substr($id,0,9) == 'org_name:' || substr($id, 0,10) == 'duplicate:')
{
if (count($checked) == 1 && !count($grouped_contacts) && $action == 'infolog')
{
return $this->infolog_org_view($id); // uses the org-name, instead of 'selected contacts'
}
unset($checked[$n]);
$query = Api\Cache::getSession('addressbook', $session_name);
$query['num_rows'] = -1; // all
$query['grouped_view'] = $id;
unset($query['filter2']);
$extra = $readonlys = null;
$this->get_rows($query,$extra,$readonlys,true); // true = only return the id's
// Merge them here, so we only merge the ones that are duplicates,
// not merge all selected together
if($action == 'merge_duplicates')
{
$loop_success = $loop_fail = 0;
$this->action('merge', $extra, false, $loop_success, $loop_fail, $action_msg,$session_name,$msg);
$success += $loop_success;
$failed += $loop_fail;
}
if ($extra[0])
{
$grouped_contacts = array_merge($grouped_contacts,$extra);
}
}
}
return $grouped_contacts;
}
/**
* Copy a given contact (not storing it!)
*
@ -1400,30 +1535,21 @@ window.egw_LAB.wait(function() {
{
$old_state = Api\Cache::getSession('addressbook', $what);
}
if (!isset($this->org_views[(string) $query['org_view']]) || strpos($query['org_view'],':') === false) // we dont have an org view, unset the according col_filters
if (!isset($this->grouped_views[(string) $query['grouped_view']]) || strpos($query['grouped_view'],':') === false)
{
if (isset($query['col_filter']['org_name'])) unset($query['col_filter']['org_name']);
if (isset($query['col_filter']['adr_one_locality'])) unset($query['col_filter']['adr_one_locality']);
if (isset($query['col_filter']['org_unit'])) unset($query['col_filter']['org_unit']);
// we don't have a grouped view, unset the according col_filters
$this->unset_grouped_filters($query);
}
if (isset($this->org_views[(string) $query['org_view']])) // we have an org view, reset the advanced search
if (isset($this->grouped_views[(string) $query['grouped_view']]))
{
//_debug_array(array('Search'=>$query['search'],
// 'AdvancedSearch'=>$query['advanced_search']));
//if (is_array($query['search'])) unset($query['search']);
//unset($query['advanced_search']);
// we have a grouped view, reset the advanced search
if(!$query['search'] && $old_state['advanced_search']) $query['advanced_search'] = $old_state['advanced_search'];
}
elseif(!$query['search'] && array_key_exists('advanced_search',$old_state)) // eg. paging in an advanced search
{
$query['advanced_search'] = $old_state['advanced_search'];
}
/* this cant work anymore, as Framework::set_onload no longer exists
if ($do_email && etemplate::$loop)
{ // remove previous addEmail() calls, otherwise they will be run again
Framework::set_onload(preg_replace('/addEmail\([^)]+\);/','',Framework::set_onload()),true);
}*/
// Make sure old lettersearch filter doesn't stay - current letter filter will be added later
foreach($query['col_filter'] as $key => $col_filter)
@ -1441,7 +1567,7 @@ window.egw_LAB.wait(function() {
if (!$id_only)
{
// check if accounts are stored in ldap, which does NOT yet support the org-views
if ($this->so_accounts && $query['filter'] === '0' && $query['org_view'])
if ($this->so_accounts && $query['filter'] === '0' && $query['grouped_view'])
{
if ($old_state['filter'] === '0') // user changed to org_view
{
@ -1449,21 +1575,21 @@ window.egw_LAB.wait(function() {
}
else // user changed to accounts
{
$query['org_view'] = ''; // --> change to regular contacts view
$query['grouped_view'] = ''; // --> change to regular contacts view
}
}
if ($query['org_view'] && isset($this->org_views[$old_state['org_view']]) && !isset($this->org_views[$query['org_view']]))
if ($query['grouped_view'] && isset($this->grouped_views[$old_state['grouped_view']]) && !isset($this->grouped_views[$query['grouped_view']]))
{
$query['searchletter'] = ''; // reset lettersearch if viewing the contacts of one organisation
$query['searchletter'] = ''; // reset lettersearch if viewing the contacts of one group (org or duplicates)
}
// save the state of the index in the user prefs
$state = serialize(array(
'filter' => $query['filter'],
'cat_id' => $query['cat_id'],
'order' => $query['order'],
'sort' => $query['sort'],
'col_filter' => array('tid' => $query['col_filter']['tid']),
'org_view' => $query['org_view'],
'filter' => $query['filter'],
'cat_id' => $query['cat_id'],
'order' => $query['order'],
'sort' => $query['sort'],
'col_filter' => array('tid' => $query['col_filter']['tid']),
'grouped_view' => $query['grouped_view'],
));
if ($state != $this->prefs[$what.'_state'] && !$query['csv_export'])
{
@ -1511,44 +1637,11 @@ window.egw_LAB.wait(function() {
// all backends allow now at least to use groups as distribution lists
$query['no_filter2'] = false;
if (isset($this->org_views[(string) $query['org_view']]) && !$query['col_filter']['parent_id']) // we have an org view
// Grouped view
if (isset($this->grouped_views[(string) $query['grouped_view']]) && !$query['col_filter']['parent_id'])
{
// Query doesn't like empties
unset($query['col_filter']['parent_id']);
if($query['actions'] && $query['actions']['open'])
{
// Just switched from contact view, update actions
$query['actions'] = $this->get_actions($query['col_filter']['tid'], $query['org_view']);
}
unset($query['col_filter']['list']); // does not work together
$query['no_filter2'] = true; // switch the distribution list selection off
$query['template'] = 'addressbook.index.org_rows';
if ($query['order'] != 'org_name')
{
$query['sort'] = 'ASC';
$query['order'] = 'org_name';
}
if ($query['advanced_search'])
{
$query['op'] = $query['advanced_search']['operator'];
unset($query['advanced_search']['operator']);
$query['wildcard'] = $query['advanced_search']['meth_select'];
unset($query['advanced_search']['meth_select']);
$original_search = $query['search'];
$query['search'] = $query['advanced_search'];
}
$rows = parent::organisations($query);
if ($query['advanced_search'])
{
$query['search'] = $original_search;
unset($query['wildcard']);
unset($query['op']);
}
$GLOBALS['egw_info']['flags']['params']['manual'] = array('page' => 'ManualAddressbookIndexOrga');
$query['grouped_view_label'] = '';
$rows = $this->get_grouped_rows($query);
}
else // contacts view
{
@ -1562,19 +1655,21 @@ window.egw_LAB.wait(function() {
}
if($query['col_filter']['parent_id'])
{
$query['org_view'] = $query['col_filter']['parent_id'];
$query['template'] = 'addressbook.index.org_rows';
$query['grouped_view'] = $query['col_filter']['parent_id'];
$query['template'] = strpos($query['grouped_view'], 'duplicate') === 0 ?
'addressbook.index.duplicate_rows' : 'addressbook.index.org_rows';
}
// Query doesn't like parent_id
unset($query['col_filter']['parent_id']);
if ($query['org_view']) // view the contacts of one organisation only
if ($query['grouped_view']) // view the contacts of one organisation only
{
if (strpos($query['org_view'],'*AND*') !== false) $query['org_view'] = str_replace('*AND*','&',$query['org_view']);
foreach(explode('|||',$query['org_view']) as $part)
if (strpos($query['grouped_view'],'*AND*') !== false) $query['grouped_view'] = str_replace('*AND*','&',$query['grouped_view']);
$fields = explode(',',$GLOBALS['egw_info']['user']['preferences']['addressbook']['duplicate_fields']);
foreach(explode('|||',$query['grouped_view']) as $part)
{
list($name,$value) = explode(':',$part,2);
// do NOT set invalid column, as this gives an SQL error ("AND AND" in sql)
if (in_array($name, array('org_name','org_unit','adr_one_location')))
if (static::$duplicate_fields[$name] && in_array($name, $fields) && $value)
{
$query['col_filter'][$name] = $value;
}
@ -1582,8 +1677,8 @@ window.egw_LAB.wait(function() {
}
else if($query['actions'] && !$query['actions']['edit'])
{
// Just switched from org view, update actions
$query['actions'] = $this->get_actions($query['col_filter']['tid'], $query['org_view']);
// Just switched from grouped view, update actions
$query['actions'] = $this->get_actions($query['col_filter']['tid']);
}
// translate the select order to the really used over all 3 columns
$sort = $query['sort'];
@ -1712,17 +1807,17 @@ window.egw_LAB.wait(function() {
list($row['line1'],$row['line2']) = explode(': ',$row['n_fileas']);
break;
}
if (isset($this->org_views[(string) $query['org_view']]))
if (isset($this->grouped_views[(string) $query['grouped_view']]))
{
$row['type'] = 'home';
$row['type_label'] = lang('Organisation');
$row['type_label'] = $query['grouped_view'] == 'duplicate' ? lang('Duplicates') : lang('Organisation');
if ($query['filter'] && !($this->grants[(int)$query['filter']] & Acl::DELETE))
{
$row['class'] .= 'rowNoDelete ';
}
$row['class'] .= 'rowNoEdit '; // no edit in OrgView
$row['class'] .= 'contact_organisation ';
$row['class'] .= $query['grouped_view'] == 'duplicates' ? 'contact_duplicate' : 'contact_organisation ';
}
else
{
@ -1802,7 +1897,7 @@ window.egw_LAB.wait(function() {
// full app-header with all search criteria specially for the print
$header = array();
if ($query['filter'] !== '' && !isset($this->org_views[$query['org_view']]))
if ($query['filter'] !== '' && !isset($this->grouped_views[$query['grouped_view']]))
{
$header[] = ($query['filter'] == '0' ? lang('accounts') :
($GLOBALS['egw']->accounts->get_type($query['filter']) == 'g' ?
@ -1810,14 +1905,14 @@ window.egw_LAB.wait(function() {
Api\Accounts::username((int)$query['filter']).
(substr($query['filter'],-1) == 'p' ? ' ('.lang('private').')' : '')));
}
if ($query['org_view'])
if ($query['grouped_view'])
{
$header[] = $query['org_view_label'];
$header[] = $query['grouped_view_label'];
// Make sure option is there
if(!array_key_exists($query['org_view'], $this->org_views))
if(!array_key_exists($query['grouped_view'], $this->grouped_views))
{
$this->org_views += $this->_get_org_name($query['org_view']);
$rows['sel_options']['org_view'] = $this->org_views;
$this->grouped_views += $this->_get_grouped_name($query['grouped_view']);
$rows['sel_options']['grouped_view'] = $this->grouped_views;
}
}
if($query['advanced_search'])
@ -2103,9 +2198,9 @@ window.egw_LAB.wait(function() {
if ($org[0] == '"') $org = substr($org, 1, -1);
$content = $this->read_org($org);
}
elseif ($state['org_view'] && !isset($this->org_views[$state['org_view']]))
elseif ($state['grouped_view'] && !isset($this->grouped_views[$state['grouped_view']]))
{
$content = $this->read_org($state['org_view']);
$content = $this->read_org($state['grouped_view']);
}
else
{
@ -2491,12 +2586,14 @@ window.egw_LAB.wait(function() {
public function ajax_check_values($values, $name, $own_id=0)
{
$matches = null;
$fields = explode(',',$GLOBALS['egw_info']['user']['preferences']['addressbook']['duplicate_fields']);
if (preg_match('/^exec\[([^\]]+)\]$/', $name, $matches)) $name = $matches[1]; // remove exec[ ]
$ret = array('doublicates' => array(), 'msg' => null);
// if email changed, check for doublicates
if (in_array($name, array('email', 'email_home')))
if (in_array($name, array('email', 'email_home')) && in_array($name, $fields))
{
if (preg_match(Etemplate\Widget\Url::EMAIL_PREG, $values[$name])) // only search for real email addresses, to not return to many contacts
{
@ -2513,12 +2610,19 @@ window.egw_LAB.wait(function() {
// Full options for et2
$ret['fileas_sel_options'] = $this->fileas_options($values);
// if name, firstname or org changed and at least 2 are specified, check for doublicates
if (in_array($name, array('n_given', 'n_family', 'org_name')) &&
!empty($values['n_given'])+!empty($values['n_family'])+!empty($values['org_name']) >= 2)
// if name, firstname or org changed and enough are specified, check for doublicates
$specified_count = 0;
foreach($fields as $field)
{
if($values[$field])
{
$specified_count++;
}
}
if (in_array($name,$fields) && $specified_count >= 2)
{
$filter = array();
foreach(array('email', 'n_given', 'n_family', 'org_name') as $n) // use email too, to exclude obvious false positives
foreach($fields as $n) // use email too, to exclude obvious false positives
{
if (!empty($values[$n])) $filter[$n] = $values[$n];
}

View File

@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE overlay PUBLIC "-//EGroupware GmbH//eTemplate 2//EN" "http://www.egroupware.org/etemplate2.dtd">
<!-- $Id$ -->
<overlay>
<template id="addressbook.index.duplicate_rows" template="" lang="" group="0" version="16.001">
<grid width="100%">
<columns>
<column width="75"/>
<column width="25"/>
<column width="40%"/>
<column width="30%"/>
<column width="30%"/>
<column width="180"/>
<column width="180"/>
</columns>
<rows>
<row class="th">
<nextmatch-header label="Type" id="type"/>
<nextmatch-header label="#" align="center" id="group_count"/>
<grid>
<columns>
<column/>
<column/>
</columns>
<rows>
<row disabled="!@order=n_fileas">
<nextmatch-sortheader label="own sorting" id="n_fileas" span="all"/>
</row>
<row disabled="!@order=n_given">
<nextmatch-sortheader label="Firstname" id="n_given"/>
<nextmatch-sortheader label="Name" id="n_family"/>
</row>
<row disabled="!@order=n_family">
<nextmatch-sortheader label="Name" id="n_family"/>
<nextmatch-sortheader label="Firstname" id="n_given"/>
</row>
<row>
<nextmatch-sortheader label="Organisation" id="org_name" span="all"/>
</row>
<row disabled="!@order=/^(org_name|n_fileas|adr_one_postalcode|contact_modified|contact_created|#)/">
<nextmatch-sortheader label="Name" id="n_family"/>
<nextmatch-sortheader label="Firstname" id="n_given" class="leftPad5"/>
</row>
<row disabled="@order=n_fileas">
<nextmatch-sortheader label="own sorting" id="n_fileas" span="all"/>
</row>
</rows>
</grid>
<nextmatch-header label="Business address" id="business"/>
<!--
<vbox>
<nextmatch-header label="Business phone" id="tel_work"/>
<nextmatch-header label="Mobile phone" id="tel_cell"/>
<nextmatch-header label="Home phone" id="tel_home"/>
<description value="Fax"/>
</vbox>
-->
<vbox>
<nextmatch-header label="Business email" id="email"/>
<nextmatch-header label="Home email" id="email_home"/>
</vbox>
</row>
<row class="$row_cont[cat_id] $row_cont[class]" valign="top">
<image label="$row_cont[type_label]" src="${row}[type]" align="center" no_lang="1"/>
<int id="${row}[group_count]" readonly="true" align="center"/>
<vbox id="${row}[id]">
<description id="${row}[line1]" no_lang="1"/>
<description id="${row}[line2]" no_lang="1"/>
<description id="${row}[org_unit]" no_lang="1"/>
<description id="${row}[title]" no_lang="1"/>
<description id="${row}[first_org]" no_lang="1"/>
</vbox>
<vbox>
<description value=" " id="${row}[adr_one_locality]" no_lang="1" class="leftPad5"/>
<menulist>
<menupopup type="select-country" id="${row}[adr_one_countrycode]" readonly="true"/>
</menulist>
</vbox>
<!--
<vbox>
<url-phone id="${row}[tel_work]" readonly="true" class="telNumbers"/>
<url-phone id="${row}[tel_cell]" readonly="true" class="telNumbers"/>
<url-phone id="${row}[tel_home]" readonly="true" class="telNumbers"/>
<url-phone id="${row}[tel_fax]" readonly="true"/>
<description id="${row}[tel_prefered]" no_lang="1" href="$row_cont[tel_prefered_link]" extra_link_target="calling" extra_link_popup="$cont[call_popup]"/>
</vbox>
-->
<vbox>
<url-email id="${row}[email]" readonly="true" class="fixedHeight"/>
<url-email id="${row}[email_home]" readonly="true" class="fixedHeight"/>
</vbox>
</row>
</rows>
</grid>
</template>
</overlay>

View File

@ -174,7 +174,7 @@
</template>
<template id="addressbook.index.row" template="" lang="" group="0" version="1.3.001">
<buttononly align="right" statustext="Advanced search" image="advanced-search" background_image="1" id="advanced-search" onclick="egw(window).openPopup(egw::link('/index.php','menuaction=addressbook.addressbook_ui.search'),'870','610','_blank','addressbook',null,true); return false;"/>
<select statustext="Select a view" id="org_view" no_lang="1" rows="1" empty_label="All contacts"/>
<select statustext="Select a view" id="grouped_view" no_lang="1" rows="1" empty_label="All contacts"/>
</template>
<template id="addressbook.index.right" template="" lang="" group="0" version="1.7.001">
<select align="right" id="col_filter[tid]" empty_label="All types"/>

View File

@ -207,6 +207,7 @@ class Sql extends Api\Storage
$filter['org_name'][$row['org_name']] = $row['org_name']; // use as key too to have every org only once
}
$org_key = $row['org_name'].($by ? '|||'.($row[$by] || $row[$by.'_count']==1 ? $row[$by] : '|||') : '');
$row['group_count'] = $row['org_count'];
$orgs[$org_key] = $row;
}
unset($rows);
@ -242,6 +243,141 @@ class Sql extends Api\Storage
return array_values($orgs);
}
/**
* Query for duplicate contacts according to given parameters
*
* We join egw_addressbook to itself, and count how many fields match. If
* enough of the fields we care about match, we count those two records as
* duplicates.
*
* @var array $param
* @var string $param[grouped_view] 'duplicate', 'duplicate,adr_one_location', 'duplicate,org_name' how to group
* @var int $param[owner] addressbook to search
* @var string $param[search] search pattern for org_name
* @var string $param[searchletter] letter the name need to start with
* @var array $param[col_filter] filter
* @var string $param[search] or'ed search pattern
* @var array $param[advanced_search] indicator that advanced search is active
* @var string $param[op] (operator like AND or OR; will be passed when advanced search is active)
* @var string $param[wildcard] (wildcard like % or empty or not set (for no wildcard); will be passed when advanced search is active)
* @var int $param[start]
* @var int $param[num_rows]
* @var string $param[sort] ASC or DESC
* @return array or arrays with keys org_name,count and evtl. adr_one_location or org_unit
*/
function duplicates($param)
{
$join = 'JOIN ' . $this->table_name . ' AS a2 ON ';
$filter = array(
$this->table_name.'.contact_tid != "D"'
);
$op = 'OR';
if (isset($param['op']) && !empty($param['op'])) $op = $param['op'];
$advanced_search = false;
if (isset($param['advanced_search']) && !empty($param['advanced_search'])) $advanced_search = true;
$wildcard ='%';
if ($advanced_search || (isset($param['wildcard']) && !empty($param['wildcard']))) $wildcard = ($param['wildcard']?$param['wildcard']:'');
// fix cat_id filter to search in comma-separated multiple cats and return subcats
if ($param['cat_id'])
{
$cat_filter = $this->_cat_filter($filter['cat_id']);
$filter[] = str_replace('cat_id', $this->table_name . '.cat_id', $cat_filter);
$join .= str_replace('cat_id', 'a2.cat_id', $cat_filter) . ' AND ';
unset($filter['cat_id']);
}
// add filter for read ACL in sql, if user is NOT the owner of the addressbook
if ($param['owner'] && $param['owner'] == $GLOBALS['egw_info']['user']['account_id'])
{
$filter['owner'] = $param['owner'];
$join .= 'a2.owner = ' . $this->db->quote($filter['owner']) . ' AND ';
}
else
{
// we have no private grants in addressbook at the moment, they have then to be added here too
if ($param['owner'])
{
if (!$this->grants[(int) $filter['owner']]) return false; // we have no access to that addressbook
$filter['owner'] = $param['owner'];
$filter['private'] = 0;
$join .= 'a2.owner = ' . $this->db->quote($filter['owner']) . ' AND ';
$join .= 'a2.private = ' . $this->db->quote($filter['private']) . ' AND ';
}
else // search all addressbooks, incl. accounts
{
if ($this->account_repository != 'sql' && $this->contact_repository != 'sql-ldap')
{
$filter[] = $this->table_name.'.contact_owner != 0'; // in case there have been accounts in sql previously
}
$filter[] = $access = "(".$this->table_name.".contact_owner=".(int)$GLOBALS['egw_info']['user']['account_id'].
" OR {$this->table_name}.contact_private=0 AND ".$this->table_name.".contact_owner IN (".
implode(',',array_keys($this->grants))."))";
$join .= str_replace($this->table_name, 'a2', $access) . ' AND ';
}
}
if ($param['searchletter'])
{
$filter[] = $this->table_name.'.n_fn '.$this->db->capabilities[Api\Db::CAPABILITY_CASE_INSENSITIV_LIKE].' '.$this->db->quote($param['searchletter'].'%');
}
$sort = $param['sort'] == 'DESC' ? 'DESC' : 'ASC';
$group = $GLOBALS['egw_info']['user']['preferences']['addressbook']['duplicate_fields'] ?
explode(',',$GLOBALS['egw_info']['user']['preferences']['addressbook']['duplicate_fields']):
array('n_family', 'n_given', 'org_name', 'contact_email');
$match_count = $GLOBALS['egw_info']['user']['preferences']['addressbook']['duplicate_threshold'] ?
$GLOBALS['egw_info']['user']['preferences']['addressbook']['duplicate_threshold'] : 3;
$extra = Array();
$order = in_array($param['order'], $group) ? $param['order'] : $group[0];
$join .= $this->table_name .'.contact_id != a2.contact_id AND a2.contact_tid != "D" AND (';
$join_fields = Array();
foreach($group as &$field)
{
$extra[] = "IF({$this->table_name}.$field = a2.$field, 1, 0)";
$join_fields[] = $this->table_name . ".$field = a2.$field";
$field = $this->table_name . ".$field AS $field";
}
$extra = Array(
'a2.contact_id AS matched',
implode('+', $extra) . ' AS match_count'
);
$join .= $this->db->column_data_implode(' OR ',$join_fields) . ')';
$append = " HAVING match_count >= $match_count ORDER BY {$this->table_name}.{$order} $sort, $this->table_name.contact_id";
$group[] = $this->table_name.'.contact_id AS contact_id';
$rows = parent::search($param['search'],$group,
$append,$extra,$wildcard,false,$op/*'OR'*/,
array($param['start'],$param['num_rows']),$filter, $join);
// Go through rows and only return one for each pair/triplet/etc. of matches
$dupes = array();
foreach($rows as $key => &$row)
{
if(array_key_exists($row['contact_id'], $dupes))
{
$kept_row =& $rows[$dupes[$row['contact_id']]];
$kept_row['group_count']++;
// Clear not matching fields, or we won't be able to find children
foreach($kept_row as $sub_key => $sub_value)
{
if(in_array($sub_key, array('contact_id','group_count'))) continue;
if($row[$sub_key] != $sub_value)
{
unset($kept_row[$sub_key]);
}
}
unset($rows[$key]);
$this->total--;
}
$dupes[$row['matched']] = $key;
$row['group_count'] = 1;
}
return array_values($rows);
}
/**
* searches db for rows matching searchcriteria
*

View File

@ -159,6 +159,24 @@ class Storage
*/
var $content_types = array();
/**
* These fields are options for checking for duplicate contacts
*
* @var array
*/
public static $duplicate_fields = array(
'n_given' => 'first name',
'n_middle' => 'middle name',
'n_family' => 'last name',
'contact_bday' => 'birthday',
'org_name' => 'Organisation',
'org_unit' => 'Department',
'adr_one_locality' => 'Location',
'contact_title' => 'title',
'contact_email' => 'business email',
'contact_email_home'=> 'email (private)',
);
/**
* Special content type to indicate a deleted addressbook
*
@ -796,6 +814,80 @@ class Storage
return $rows;
}
/**
* Find contacts that appear to be duplicates
*
* @param Array $param
* @param string $param[org_view] 'org_name', 'org_name,adr_one_location', 'org_name,org_unit' how to group
* @param int $param[owner] addressbook to search
* @param string $param[search] search pattern for org_name
* @param string $param[searchletter] letter the org_name need to start with
* @param int $param[start]
* @param int $param[num_rows]
* @param string $param[sort] ASC or DESC
*
* @return array of arrays
*/
public function duplicates($param)
{
if (!method_exists($this->somain,'duplicates'))
{
$this->total = 0;
return false;
}
if ($param['search'] && !is_array($param['search']))
{
$search = $param['search'];
$param['search'] = array();
if($this->somain instanceof Sql)
{
// Keep the string, let the parent deal with it
$param['search'] = $search;
}
else
{
foreach($this->columns_to_search as $col)
{
// we don't search the customfields
if ($col != 'contact_value') $param['search'][$col] = $search;
}
}
}
if (is_array($param['search']) && count($param['search']))
{
$param['search'] = $this->data2db($param['search']);
}
if(!array_key_exists('tid', $param['col_filter']) || $param['col_filter']['tid'] === '')
{
$param['col_filter'][] = 'contact_tid != \'' . self::DELETED_TYPE . '\'';
}
elseif(is_null($param['col_filter']['tid']))
{
// return all entries including deleted
unset($param['col_filter']['tid']);
}
$rows = $this->somain->duplicates($param);
$this->total = $this->somain->total;
if (!$rows) return array();
foreach($rows as $n => $row)
{
$rows[$n]['id'] = 'duplicate:';
foreach(static::$duplicate_fields as $by => $by_label)
{
if (strpos($row[$by],'&')!==false) $row[$by] = str_replace('&','*AND*',$row[$by]);
if($row[$by])
{
$rows[$n]['id'] .= '|||'.$by.':'.$row[$by];
}
}
}
return $rows;
}
/**
* gets all contact fields from database
*