diff --git a/calendar/inc/class.calendar_import_csv.inc.php b/calendar/inc/class.calendar_import_csv.inc.php index f460dfc48b..699e08fa18 100644 --- a/calendar/inc/class.calendar_import_csv.inc.php +++ b/calendar/inc/class.calendar_import_csv.inc.php @@ -97,7 +97,7 @@ class calendar_import_csv extends importexport_basic_import_csv { { $record->owner = $options['owner']; } - + // Handle errors in length or start/end date if($record->start > $record->end) { @@ -107,76 +107,12 @@ class calendar_import_csv extends importexport_basic_import_csv { // Parse particpants if ($record->participants && !is_array($record->participants)) { - // Importing participants in human friendly format: - // Name (quantity)? (status) Role[, Name (quantity)? (status) Role]+ - preg_match_all('/(([^(]+?)(?: \(([\d]+)\))? \(([^,)]+)\)(?: ([^ ,]+))?)(?:, )?/',$record->participants,$participants); - $p_participants = array(); - $missing = array(); - 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!') . '
'.implode(", ",$missing); - } + $warning = ''; + $record->participants = $this->parse_participants($record, $warning); + if($warning) + { + $this->warnings[$import_csv->get_current_position()] = $warning; } - $record->participants = $p_participants; } if($record->recurrence) @@ -243,6 +179,90 @@ class calendar_import_csv extends importexport_basic_import_csv { 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 ('/((?.+?)(?: \((?[\d]+)\))? \((?'.$statuses.')\)(?: (?[^ ,]+))?)(?:, )?/'); + preg_match_all('/((?.+?)(?: \((?[\d]+)\))? \((?'.$statuses.')\)(?: (?[^ ,]+))?)(?:, )?/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!') . '
'.implode(", ",$missing); + } + } + return $p_participants; + } + /** * 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; } else { $messages = null; - $result = $this->bo->update( $_data, + $result = $this->bo->update( $_data, !$this->definition->plugin_options['skip_conflicts'], true, $this->is_admin, true, $messages, $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 - * + * * @param int $record_num Current record index * @param Array $conflicts List of found conflicting events */ diff --git a/calendar/tests/ImportParticipantsTest.php b/calendar/tests/ImportParticipantsTest.php new file mode 100644 index 0000000000..cc35cb3d61 --- /dev/null +++ b/calendar/tests/ImportParticipantsTest.php @@ -0,0 +1,161 @@ +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; + } +} \ No newline at end of file