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