mirror of
https://github.com/EGroupware/egroupware.git
synced 2025-06-24 03:41:53 +02:00
* Calendar - Improve parsing of users when importing to be able to handle parentheses in names
This commit is contained in:
parent
56f9c9478d
commit
20e75c17db
@ -97,7 +97,7 @@ class calendar_import_csv extends importexport_basic_import_csv {
|
|||||||
{
|
{
|
||||||
$record->owner = $options['owner'];
|
$record->owner = $options['owner'];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle errors in length or start/end date
|
// Handle errors in length or start/end date
|
||||||
if($record->start > $record->end)
|
if($record->start > $record->end)
|
||||||
{
|
{
|
||||||
@ -107,76 +107,12 @@ class calendar_import_csv extends importexport_basic_import_csv {
|
|||||||
|
|
||||||
// Parse particpants
|
// Parse particpants
|
||||||
if ($record->participants && !is_array($record->participants)) {
|
if ($record->participants && !is_array($record->participants)) {
|
||||||
// Importing participants in human friendly format:
|
$warning = '';
|
||||||
// Name (quantity)? (status) Role[, Name (quantity)? (status) Role]+
|
$record->participants = $this->parse_participants($record, $warning);
|
||||||
preg_match_all('/(([^(]+?)(?: \(([\d]+)\))? \(([^,)]+)\)(?: ([^ ,]+))?)(?:, )?/',$record->participants,$participants);
|
if($warning)
|
||||||
$p_participants = array();
|
{
|
||||||
$missing = array();
|
$this->warnings[$import_csv->get_current_position()] = $warning;
|
||||||
list($lines, $p, $names, $quantity, $status, $role) = $participants;
|
|
||||||
foreach($names as $key => $name) {
|
|
||||||
//error_log("Name: $name Quantity: {$quantity[$key]} Status: {$status[$key]} Role: {$role[$key]}");
|
|
||||||
|
|
||||||
// Search for direct account name, then user in accounts first
|
|
||||||
$search = "\"$name\"";
|
|
||||||
$id = importexport_helper_functions::account_name2id($name);
|
|
||||||
|
|
||||||
// If not found, or not an exact match to a user (account_name2id is pretty generous)
|
|
||||||
if(!$id || $names[$key] !== $this->bo->participant_name($id)) {
|
|
||||||
$contacts = ExecMethod2('addressbook.addressbook_bo.search', $search,array('contact_id','account_id'),'org_name,n_family,n_given,cat_id,contact_email','','%',false,'OR',array(0,1));
|
|
||||||
if($contacts) $id = $contacts[0]['account_id'] ? $contacts[0]['account_id'] : 'c'.$contacts[0]['contact_id'];
|
|
||||||
}
|
|
||||||
if(!$id)
|
|
||||||
{
|
|
||||||
// Use calendar's registered resources to find participant
|
|
||||||
foreach($this->bo->resources as $resource)
|
|
||||||
{
|
|
||||||
// Can't search for email
|
|
||||||
if($resource['app'] == 'email') continue;
|
|
||||||
// Special resource search, since it does special stuff in link_query
|
|
||||||
if($resource['app'] == 'resources')
|
|
||||||
{
|
|
||||||
if(!$this->resource_so)
|
|
||||||
{
|
|
||||||
$this->resource_so = new resources_so();
|
|
||||||
}
|
|
||||||
$result = $this->resource_so->search($search,'res_id');
|
|
||||||
if(count($result) >= 1) {
|
|
||||||
$id = $resource['type'].$result[0]['res_id'];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Search app via link query
|
|
||||||
$link_options = array();
|
|
||||||
$result = Link::query($resource['app'], $search, $link_options);
|
|
||||||
|
|
||||||
if($result)
|
|
||||||
{
|
|
||||||
$id = $resource['type'] . key($result);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if($id) {
|
|
||||||
$p_participants[$id] = calendar_so::combine_status(
|
|
||||||
$this->status_map[lang($status[$key])] ? $this->status_map[lang($status[$key])] : $status[$key][0],
|
|
||||||
$quantity[$key] ? $quantity[$key] : 1,
|
|
||||||
$this->role_map[lang($role[$key])] ? $this->role_map[lang($role[$key])] : $role[$key]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
$missing[] = $name;
|
|
||||||
}
|
|
||||||
if(count($missing) > 0)
|
|
||||||
{
|
|
||||||
$this->warnings[$import_csv->get_current_position()] = $record->title . ' ' . lang('participants') . ': ' .
|
|
||||||
lang('Contact not found!') . '<br />'.implode(", ",$missing);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
$record->participants = $p_participants;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if($record->recurrence)
|
if($record->recurrence)
|
||||||
@ -243,6 +179,90 @@ class calendar_import_csv extends importexport_basic_import_csv {
|
|||||||
return $success;
|
return $success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse participants field into calendar resources
|
||||||
|
*
|
||||||
|
* @param string $participants
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
protected function parse_participants($record, &$warnings)
|
||||||
|
{
|
||||||
|
// Importing participants in human friendly format:
|
||||||
|
// Name (quantity)? (status) Role[, Name (quantity)? (status) Role]+
|
||||||
|
$statuses = implode('|', array_keys($this->status_map));
|
||||||
|
//echo ('/((?<name>.+?)(?: \((?<quantity>[\d]+)\))? \((?<status>'.$statuses.')\)(?: (?<role>[^ ,]+))?)(?:, )?/');
|
||||||
|
preg_match_all('/((?<name>.+?)(?: \((?<quantity>[\d]+)\))? \((?<status>'.$statuses.')\)(?: (?<role>[^ ,]+))?)(?:, )?/i',$record->participants,$participants);
|
||||||
|
$p_participants = array();
|
||||||
|
$missing = array();
|
||||||
|
|
||||||
|
list($lines, $p, $names, $quantity, $status, $role) = $participants;
|
||||||
|
foreach($names as $key => $name) {
|
||||||
|
//echo (__METHOD__ ." Name: $name Quantity: {$quantity[$key]} Status: {$status[$key]} Role: {$role[$key]}");
|
||||||
|
|
||||||
|
// Search for direct account name, then user in accounts first
|
||||||
|
$search = "\"$name\"";
|
||||||
|
$id = importexport_helper_functions::account_name2id($name);
|
||||||
|
|
||||||
|
// If not found, or not an exact match to a user (account_name2id is pretty generous)
|
||||||
|
if(!$id || $names[$key] !== $this->bo->participant_name($id)) {
|
||||||
|
$contacts = ExecMethod2('addressbook.addressbook_bo.search', $search,array('contact_id','account_id'),'org_name,n_family,n_given,cat_id,contact_email','','%',false,'OR',array(0,1));
|
||||||
|
if($contacts) $id = $contacts[0]['account_id'] ? $contacts[0]['account_id'] : 'c'.$contacts[0]['contact_id'];
|
||||||
|
}
|
||||||
|
if(!$id)
|
||||||
|
{
|
||||||
|
// Use calendar's registered resources to find participant
|
||||||
|
foreach($this->bo->resources as $resource)
|
||||||
|
{
|
||||||
|
// Can't search for email
|
||||||
|
if($resource['app'] == 'email') continue;
|
||||||
|
// Special resource search, since it does special stuff in link_query
|
||||||
|
if($resource['app'] == 'resources')
|
||||||
|
{
|
||||||
|
if(!$this->resource_so)
|
||||||
|
{
|
||||||
|
$this->resource_so = new resources_so();
|
||||||
|
}
|
||||||
|
$result = $this->resource_so->search($search,'res_id');
|
||||||
|
if(count($result) >= 1) {
|
||||||
|
$id = $resource['type'].$result[0]['res_id'];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Search app via link query
|
||||||
|
$link_options = array();
|
||||||
|
$result = Link::query($resource['app'], $search, $link_options);
|
||||||
|
|
||||||
|
if($result)
|
||||||
|
{
|
||||||
|
$id = $resource['type'] . key($result);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if($id) {
|
||||||
|
$p_participants[$id] = calendar_so::combine_status(
|
||||||
|
$this->status_map[lang($status[$key])] ? $this->status_map[lang($status[$key])] : $status[$key][0],
|
||||||
|
$quantity[$key] ? $quantity[$key] : 1,
|
||||||
|
$this->role_map[lang($role[$key])] ? $this->role_map[lang($role[$key])] : $role[$key]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$missing[] = $name;
|
||||||
|
}
|
||||||
|
if(count($missing) > 0)
|
||||||
|
{
|
||||||
|
$warnings = $record->title . ' ' . lang('participants') . ': ' .
|
||||||
|
lang('Contact not found!') . '<br />'.implode(", ",$missing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $p_participants;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search for matching records, based on the the given condition
|
* Search for matching records, based on the the given condition
|
||||||
*
|
*
|
||||||
@ -328,7 +348,7 @@ class calendar_import_csv extends importexport_basic_import_csv {
|
|||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
$messages = null;
|
$messages = null;
|
||||||
$result = $this->bo->update( $_data,
|
$result = $this->bo->update( $_data,
|
||||||
!$this->definition->plugin_options['skip_conflicts'],
|
!$this->definition->plugin_options['skip_conflicts'],
|
||||||
true, $this->is_admin, true, $messages,
|
true, $this->is_admin, true, $messages,
|
||||||
$this->definition->plugin_options['no_notification']
|
$this->definition->plugin_options['no_notification']
|
||||||
@ -357,10 +377,10 @@ class calendar_import_csv extends importexport_basic_import_csv {
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a warning message about conflicting events
|
* Add a warning message about conflicting events
|
||||||
*
|
*
|
||||||
* @param int $record_num Current record index
|
* @param int $record_num Current record index
|
||||||
* @param Array $conflicts List of found conflicting events
|
* @param Array $conflicts List of found conflicting events
|
||||||
*/
|
*/
|
||||||
|
161
calendar/tests/ImportParticipantsTest.php
Normal file
161
calendar/tests/ImportParticipantsTest.php
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for importing participants
|
||||||
|
*
|
||||||
|
* @link http://www.egroupware.org
|
||||||
|
* @author Nathan Gray
|
||||||
|
* @package calendar
|
||||||
|
* @copyright (c) 2017 Nathan Gray
|
||||||
|
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace EGroupware\calendar;
|
||||||
|
|
||||||
|
require_once realpath(__DIR__.'/../../api/tests/AppTest.php'); // Application test base
|
||||||
|
|
||||||
|
use Egroupware\Api;
|
||||||
|
|
||||||
|
class ImportParticipantsTest extends \EGroupware\Api\AppTest
|
||||||
|
{
|
||||||
|
|
||||||
|
// Import object
|
||||||
|
private $import = null;
|
||||||
|
|
||||||
|
// Method under test with modified access
|
||||||
|
private $parse_method = null;
|
||||||
|
|
||||||
|
public static function setUpBeforeClass()
|
||||||
|
{
|
||||||
|
parent::setUpBeforeClass();
|
||||||
|
|
||||||
|
}
|
||||||
|
public static function tearDownAfterClass()
|
||||||
|
{
|
||||||
|
parent::tearDownAfterClass();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setUp()
|
||||||
|
{
|
||||||
|
$this->bo = new \calendar_bo();
|
||||||
|
|
||||||
|
$this->import = new \calendar_import_csv();
|
||||||
|
$this->import->bo = $this->bo;
|
||||||
|
$this->import->role_map = array_flip($this->bo->roles);
|
||||||
|
$this->import->status_map = array_flip($this->bo->verbose_status);
|
||||||
|
|
||||||
|
// Make parse_participants method accessable
|
||||||
|
$class = new \ReflectionClass($this->import);
|
||||||
|
$this->parse_method = $class->getMethod('parse_participants');
|
||||||
|
$this->parse_method->setAccessible(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function tearDown()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that various participant strings are correctly parsed and matched
|
||||||
|
*
|
||||||
|
* @param array $expected IDs expected
|
||||||
|
* @param string $test_string String to be parsed
|
||||||
|
* @param boolean $warn Expect a warning
|
||||||
|
*
|
||||||
|
* @dataProvider participantProvider
|
||||||
|
*/
|
||||||
|
public function testUsers($expected, $test_string, $warn)
|
||||||
|
{
|
||||||
|
|
||||||
|
$warning = '';
|
||||||
|
$record = new ParticipantRecord($test_string);
|
||||||
|
|
||||||
|
// Parse the string
|
||||||
|
$parsed = $this->parse_method->invokeArgs($this->import, array($record, &$warning));
|
||||||
|
|
||||||
|
// Get numeric ID for this system
|
||||||
|
foreach ($expected as $id => $status)
|
||||||
|
{
|
||||||
|
$_id = $id;
|
||||||
|
unset($expected[$id]);
|
||||||
|
$id = $GLOBALS['egw']->accounts->name2id($_id);
|
||||||
|
$expected[$id] = $status;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
$this->assertEquals($expected, $parsed);
|
||||||
|
|
||||||
|
if($warn)
|
||||||
|
{
|
||||||
|
$this->assertNotEmpty($warning, 'Did not get a warning');
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$this->assertEmpty($warning, 'Got a warning');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function participantProvider()
|
||||||
|
{
|
||||||
|
return array(
|
||||||
|
// Expected resource IDs, string to be parsed
|
||||||
|
|
||||||
|
|
||||||
|
array(array(), '', false),
|
||||||
|
|
||||||
|
// No such user, but it looks OK - should warn about not found user
|
||||||
|
array(array(), 'Not found (No Response)', true),
|
||||||
|
array(array(), 'Not Found (4) (No Response) Chair', true),
|
||||||
|
array(array(), 'Delta (de), Dan (No Response) None', true),
|
||||||
|
|
||||||
|
// Statuses
|
||||||
|
array(array('demo' => 'A'), 'demo (Accepted)', false),
|
||||||
|
array(array('demo' => 'R'), 'demo (Rejected)', false),
|
||||||
|
array(array('demo' => 'T'), 'demo (Tentative)', false),
|
||||||
|
array(array('demo' => 'U'), 'demo (No Response)', false),
|
||||||
|
array(array('demo' => 'D'), 'demo (Delegated)', false),
|
||||||
|
|
||||||
|
// Status with quantity
|
||||||
|
array(array('demo' => 'A4'), 'demo (4) (Accepted)', false),
|
||||||
|
array(array('demo' => 'R4'), 'demo (4) (Rejected)', false),
|
||||||
|
array(array('demo' => 'T4'), 'demo (4) (Tentative)', false),
|
||||||
|
array(array('demo' => 'U4'), 'demo (4) (No Response)', false),
|
||||||
|
array(array('demo' => 'D4'), 'demo (4) (Delegated)', false),
|
||||||
|
array(array('demo' => 'A'), 'demo (Accepted) Requested', false),
|
||||||
|
|
||||||
|
// Roles
|
||||||
|
array(array('demo' => 'ACHAIR'), 'demo (Accepted) Chair', false),
|
||||||
|
array(array('demo' => 'AOPT-PARTICIPANT'), 'demo (Accepted) Optional', false),
|
||||||
|
array(array('demo' => 'ANON-PARTICIPANT'), 'demo (Accepted) None', false),
|
||||||
|
array(array('demo' => 'AUnknown'), 'demo (Accepted) Unknown', false),
|
||||||
|
|
||||||
|
// Quantity, status & role
|
||||||
|
array(array('demo' => 'A2CHAIR'), 'demo (2) (Accepted) Chair', false),
|
||||||
|
array(array('demo' => 'A2Invalid'), 'demo (2) (Accepted) Invalid', false),
|
||||||
|
|
||||||
|
// Multiples
|
||||||
|
array(array('demo' => 'A'), 'demo (Accepted), Found, Not (No Response)', true),
|
||||||
|
array(array('demo' => 'A'), 'Guest, Demo (Accepted), Found (why), Not (No Response)', true),
|
||||||
|
|
||||||
|
// Invalid - unparsable
|
||||||
|
array(array(), 'Totally invalid', false),
|
||||||
|
array(array(), 'demo (Invalid)', false),
|
||||||
|
array(array(), 'demo (4) (Acepted)', false),
|
||||||
|
array(array(), 'demo (Five) (No Response)', true),
|
||||||
|
|
||||||
|
// TOOD: These will need matching resources created to try to match on
|
||||||
|
/*
|
||||||
|
array(array('de' => 'A'), 'Delta (de), Dan (No Response) None', false),
|
||||||
|
array(array('de' => 'A'), 'Delta (de), Dan (3) (No Response) None', false)
|
||||||
|
*/
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ParticipantRecord {
|
||||||
|
public $participants = '';
|
||||||
|
public function __construct($p)
|
||||||
|
{
|
||||||
|
$this->participants = $p;
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user