From d54fef79b8722d680984d1c83566b706e9af55c7 Mon Sep 17 00:00:00 2001 From: Wolfgang Sourdeau Date: Thu, 27 Aug 2009 16:20:41 +0000 Subject: [PATCH] Monotone-Parent: 413f1a1eef0a131464297caa0b801dbd10e14b8d Monotone-Revision: b2238fb6fffbf3d555c8ef5fd7436135fbbdfacb Monotone-Author: wsourdeau@inverse.ca Monotone-Date: 2009-08-27T16:20:41 Monotone-Branch: ca.inverse.sogo --- ChangeLog | 16 + .../Appointments/SOGoAppointmentObject.h | 7 +- .../Appointments/SOGoAppointmentObject.m | 198 +++++---- .../Appointments/SOGoCalendarComponent.h | 2 +- .../Appointments/SOGoComponentOccurence.h | 4 +- .../Appointments/SOGoComponentOccurence.m | 5 +- Tests/config.py.in | 3 + Tests/test-caldav-scheduling.py | 416 ++++++++++++++++++ UI/Scheduler/UIxAppointmentEditor.m | 6 +- 9 files changed, 575 insertions(+), 82 deletions(-) create mode 100755 Tests/test-caldav-scheduling.py diff --git a/ChangeLog b/ChangeLog index 1cdafa2ac..54be19d5f 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,21 @@ 2009-08-27 Wolfgang Sourdeau + * Tests/test-caldav-scheduling.py: new set of tests for CalDAV + scheduling (iTIP-over-DAV) operations. Implemented 9 scenarios for + invitation delegation. + + * SoObjects/Appointments/SOGoAppointmentObject.m + (_updateAttendee:withDelegate:ownerUser:forEventUID:withRecurrenceId:): + added "withDelegate:" parameter in order to be able to add or + remove the delegate corresponding to the attendee delegation. We + also adjust the "delegated-to:" and "delegated-from:" in the + corresponding attendee element from the event copy. + (-postCalDAVReplyTo:from:): deduce the delegate from the matching + attendee and pass it as parameter to subsequent method calls. + (-takeAttendeeStatus:withDelegate:from:withRecurrenceId:) + (changeParticipationStatus:withDelegate:forRecurrenceId:): take a + new "withDelegate:" parameter. + * Tests/webdavlib.py (HTTPQuery.__init__): the content-type is no longer passed as parameter and should be directly set by the client as an attribute. diff --git a/SoObjects/Appointments/SOGoAppointmentObject.h b/SoObjects/Appointments/SOGoAppointmentObject.h index ac5630d35..49c496246 100644 --- a/SoObjects/Appointments/SOGoAppointmentObject.h +++ b/SoObjects/Appointments/SOGoAppointmentObject.h @@ -47,11 +47,14 @@ @interface SOGoAppointmentObject : SOGoCalendarComponent -- (NSException *) changeParticipationStatus: (NSString *) _status; -- (NSException *) changeParticipationStatus: (NSString *) _status +- (NSException *) changeParticipationStatus: (NSString *) status + withDelegate: (iCalPerson *) delegate; +- (NSException *) changeParticipationStatus: (NSString *) status + withDelegate: (iCalPerson *) delegate forRecurrenceId: (NSCalendarDate *) _recurrenceId; - (void) takeAttendeeStatus: (iCalPerson *) attendee + withDelegate: (iCalPerson *) delegate from: (SOGoUser *) originator withRecurrenceId: (NSCalendarDate*) recurrenceId; diff --git a/SoObjects/Appointments/SOGoAppointmentObject.m b/SoObjects/Appointments/SOGoAppointmentObject.m index 7801812ca..247880946 100644 --- a/SoObjects/Appointments/SOGoAppointmentObject.m +++ b/SoObjects/Appointments/SOGoAppointmentObject.m @@ -581,11 +581,13 @@ // participation state has changed. // - uid is the actual UID of the user for whom we must // update the calendar event (with the participation change) +// - delegate is the delegated attendee if any // // This method is called multiple times, in order to update the // status of the attendee in calendars for the particular event UID. // - (NSException *) _updateAttendee: (iCalPerson *) attendee + withDelegate: (iCalPerson *) delegate ownerUser: (SOGoUser *) theOwnerUser forEventUID: (NSString *) eventUID withRecurrenceId: (NSCalendarDate *) recurrenceId @@ -596,9 +598,10 @@ SOGoAppointmentObject *eventObject; iCalCalendar *calendar; iCalEntityObject *event; - iCalPerson *otherAttendee; - NSString *iCalString, *recurrenceTime; + iCalPerson *otherAttendee, *otherDelegate; + NSString *iCalString, *recurrenceTime, *delegateEmail; NSException *error; + BOOL addDelegate, removeDelegate; error = nil; @@ -623,15 +626,52 @@ event = [eventObject newOccurenceWithID: recurrenceTime]; } - if ([[event sequence] compare: sequence] - == NSOrderedSame) + if ([[event sequence] compare: sequence] == NSOrderedSame) { SOGoUser *currentUser; currentUser = [context activeUser]; otherAttendee = [event findParticipant: theOwnerUser]; + + delegateEmail = [otherAttendee delegatedTo]; + if ([delegateEmail length]) + delegateEmail = [delegateEmail substringFromIndex: 7]; + if ([delegateEmail length]) + otherDelegate = [event findParticipantWithEmail: delegateEmail]; + else + otherDelegate = NO; + + /* we handle the addition/deletion of delegated users */ + addDelegate = NO; + removeDelegate = NO; + if (delegate) + { + if (otherDelegate) + { + if (![delegate hasSameEmailAddress: otherDelegate]) + { + removeDelegate = YES; + addDelegate = YES; + } + } + else + addDelegate = YES; + } + else + { + if (otherDelegate) + removeDelegate = YES; + } + + if (removeDelegate) + [event removeFromAttendees: otherDelegate]; + if (addDelegate) + [event addToAttendees: delegate]; + [otherAttendee setPartStat: [attendee partStat]]; - + [otherAttendee setDelegatedTo: [attendee delegatedTo]]; + [otherAttendee setDelegatedFrom: [attendee delegatedFrom]]; + // If one has accepted / declined an invitation on behalf of // the attendee, we add the user to the SENT-BY attribute. if (b && ![[currentUser login] isEqualToString: [theOwnerUser login]]) @@ -663,12 +703,13 @@ // -// This method is invoked only from the SOGo Web interface. +// This method is invoked from the SOGo Web interface. // // - theOwnerUser is owner of the calendar where the attendee // participation state has changed. // - (NSException *) _handleAttendee: (iCalPerson *) attendee + withDelegate: (iCalPerson *) delegate ownerUser: (SOGoUser *) theOwnerUser statusChange: (NSString *) newStatus inEvent: (iCalEvent *) event @@ -730,11 +771,12 @@ if (organizerUID) // Update the attendee in organizer's calendar. ex = [self _updateAttendee: attendee - ownerUser: theOwnerUser - forEventUID: [event uid] - withRecurrenceId: [event recurrenceId] - withSequence: [event sequence] - forUID: organizerUID + withDelegate: delegate + ownerUser: theOwnerUser + forEventUID: [event uid] + withRecurrenceId: [event recurrenceId] + withSequence: [event sequence] + forUID: organizerUID shouldAddSentBy: YES]; } @@ -748,26 +790,19 @@ int i; attendees = [event attendees]; - for (i = 0; i < [attendees count]; i++) { att = [attendees objectAtIndex: i]; - - if (att == attendee) continue; - - uid = [[LDAPUserManager sharedUserManager] - getUIDForEmail: [att rfc822Email]]; - - if (uid) - { - [self _updateAttendee: attendee - ownerUser: theOwnerUser - forEventUID: [event uid] - withRecurrenceId: [event recurrenceId] - withSequence: [event sequence] - forUID: uid - shouldAddSentBy: YES]; - } + uid = [att uid]; + if (uid && att != attendee) + [self _updateAttendee: attendee + withDelegate: delegate + ownerUser: theOwnerUser + forEventUID: [event uid] + withRecurrenceId: [event recurrenceId] + withSequence: [event sequence] + forUID: uid + shouldAddSentBy: YES]; } } @@ -1021,6 +1056,7 @@ // be propagated to the organizer and the other attendees. // - (void) takeAttendeeStatus: (iCalPerson *) attendee + withDelegate: (iCalPerson *) delegate from: (SOGoUser *) ownerUser withRecurrenceId: (NSCalendarDate*) recurrenceId { @@ -1042,60 +1078,56 @@ // If no occurence found, create one event = (iCalEvent*)[self newOccurenceWithID: recurrenceTime]; } - + // Find attendee within event localAttendee = [event findParticipantWithEmail: [attendee rfc822Email]]; if (localAttendee) { // Update the attendee's status +#warning this code should probably not exist, as a REPLY POST will be followed \ + by a PUT [localAttendee setPartStat: [attendee partStat]]; + [localAttendee setDelegatedTo: [attendee delegatedTo]]; + [localAttendee setDelegatedFrom: [attendee delegatedFrom]]; [self saveComponent: event]; - + NSArray *attendees; iCalPerson *att; NSString *uid; int i; - - // We update the copy of the organizer, only - // if it's a local user. + + /* We update the copy of the organizer, only if it's a local user. */ #warning add a check for only local users uid = [[event organizer] uid]; if (uid) [self _updateAttendee: attendee - ownerUser: ownerUser - forEventUID: [event uid] - withRecurrenceId: [event recurrenceId] - withSequence: [event sequence] - forUID: uid + withDelegate: delegate + ownerUser: ownerUser + forEventUID: [event uid] + withRecurrenceId: [event recurrenceId] + withSequence: [event sequence] + forUID: uid shouldAddSentBy: NO]; - - attendees = [event attendees]; + attendees = [event attendees]; for (i = 0; i < [attendees count]; i++) { att = [attendees objectAtIndex: i]; - - if (att == attendee) continue; - - uid = [[LDAPUserManager sharedUserManager] - getUIDForEmail: [att rfc822Email]]; - - if (uid) - { - // We skip the update that correspond to the owner - // since the CalDAV client will already have updated - // the actual event. - if ([ownerUser hasEmail: [att rfc822Email]]) - continue; - - [self _updateAttendee: attendee - ownerUser: ownerUser - forEventUID: [event uid] - withRecurrenceId: [event recurrenceId] - withSequence: [event sequence] - forUID: uid - shouldAddSentBy: NO]; - } + uid = [att uid]; + if (uid + && !(att == attendee || att == delegate + /* We skip the update that correspond to the owner since + the CalDAV client will already have updated the actual + event. */ + || [ownerUser hasEmail: [att rfc822Email]])) + [self _updateAttendee: attendee + withDelegate: delegate + ownerUser: ownerUser + forEventUID: [event uid] + withRecurrenceId: [event recurrenceId] + withSequence: [event sequence] + forUID: uid + shouldAddSentBy: NO]; } } else @@ -1107,9 +1139,9 @@ { NSMutableArray *elements; NSEnumerator *recipientsEnum; - NSString *recipient, *uid, *eventUID; + NSString *recipient, *uid, *eventUID, *delegateEmail; iCalEvent *event; - iCalPerson *attendee, *person; + iCalPerson *attendee, *person, *delegate; SOGoAppointmentObject *recipientEvent; SOGoUser *ownerUser; @@ -1121,6 +1153,16 @@ attendee = [event findParticipant: ownerUser]; eventUID = [event uid]; + delegate = nil; + delegateEmail = [attendee delegatedTo]; + if ([delegateEmail length]) + { + delegateEmail = [delegateEmail substringFromIndex: 7]; + if ([delegateEmail length]) + delegate + = [event findParticipantWithEmail: delegateEmail]; + } + recipientsEnum = [recipients objectEnumerator]; while ((recipient = [recipientsEnum nextObject])) if ([[recipient lowercaseString] hasPrefix: @"mailto:"]) @@ -1134,9 +1176,10 @@ if ([recipientEvent isNew]) [recipientEvent saveComponent: event]; else - [recipientEvent takeAttendeeStatus: attendee - from: ownerUser - withRecurrenceId: [event recurrenceId]]; + [recipientEvent takeAttendeeStatus: attendee + withDelegate: delegate + from: ownerUser + withRecurrenceId: [event recurrenceId]]; } // Send reply to recipient/organizer @@ -1154,12 +1197,15 @@ // // This method is invoked only from the SOGo Web interface. // -- (NSException *) changeParticipationStatus: (NSString *) _status +- (NSException *) changeParticipationStatus: (NSString *) status + withDelegate: (iCalPerson *) delegate { - return [self changeParticipationStatus: _status forRecurrenceId: nil]; + return [self changeParticipationStatus: status withDelegate: delegate + forRecurrenceId: nil]; } - (NSException *) changeParticipationStatus: (NSString *) _status + withDelegate: (iCalPerson *) delegate forRecurrenceId: (NSCalendarDate *) _recurrenceId { iCalCalendar *calendar; @@ -1193,19 +1239,20 @@ } if (event) { - // owerUser will actually be the owner of the calendar + // ownerUser will actually be the owner of the calendar // where the participation change on the event has // actually occured. The particpation change will of // course be on the attendee that is the owner of the // calendar where the participation change has occured. ownerUser = [SOGoUser userWithLogin: owner]; - + attendee = [event findParticipant: ownerUser]; if (attendee) ex = [self _handleAttendee: attendee - ownerUser: ownerUser - statusChange: _status - inEvent: event]; + withDelegate: delegate + ownerUser: ownerUser + statusChange: _status + inEvent: event]; else ex = [NSException exceptionWithHTTPStatus: 404 // Not Found reason: @"user does not participate in this " @@ -1268,7 +1315,8 @@ else if ([occurence userIsParticipant: ownerUser]) // The current user deletes the occurence; let the organizer know that // the user has declined this occurence. - [self changeParticipationStatus: @"DECLINED" forRecurrenceId: recurrenceId]; + [self changeParticipationStatus: @"DECLINED" withDelegate: nil + forRecurrenceId: recurrenceId]; } } diff --git a/SoObjects/Appointments/SOGoCalendarComponent.h b/SoObjects/Appointments/SOGoCalendarComponent.h index 76101fda2..7086cfdf2 100644 --- a/SoObjects/Appointments/SOGoCalendarComponent.h +++ b/SoObjects/Appointments/SOGoCalendarComponent.h @@ -70,7 +70,7 @@ from: (SOGoUser *) from to: (iCalPerson *) recipient; - (void) sendResponseToOrganizer: (iCalRepeatableEntityObject *) newComponent - from: (SOGoUser *) owner; + from: (SOGoUser *) owner; - (void) sendReceiptEmailUsingTemplateNamed: (NSString *) template forObject: (iCalRepeatableEntityObject *) object diff --git a/SoObjects/Appointments/SOGoComponentOccurence.h b/SoObjects/Appointments/SOGoComponentOccurence.h index a09a48ea5..2990fcf7c 100644 --- a/SoObjects/Appointments/SOGoComponentOccurence.h +++ b/SoObjects/Appointments/SOGoComponentOccurence.h @@ -28,6 +28,7 @@ @class NSException; @class iCalCalendar; +@class iCalPerson; @class iCalRepeatableEntityObject; @class SOGoCalendarComponent; @@ -55,7 +56,8 @@ - (void) setMasterComponent: (iCalRepeatableEntityObject *) newMaster; - (void) setIsNew: (BOOL) newIsNew; -- (NSException *) changeParticipationStatus: (NSString *) newPartStat; +- (NSException *) changeParticipationStatus: (NSString *) newPartStat + withDelegate: (iCalPerson *) delegate; @end diff --git a/SoObjects/Appointments/SOGoComponentOccurence.m b/SoObjects/Appointments/SOGoComponentOccurence.m index eb93b0331..50ebd1289 100644 --- a/SoObjects/Appointments/SOGoComponentOccurence.m +++ b/SoObjects/Appointments/SOGoComponentOccurence.m @@ -194,12 +194,15 @@ #warning most of SOGoCalendarComponent and SOGoComponentOccurence share the same external interface... \ they should be siblings or SOGoComponentOccurence the parent class of SOGoCalendarComponent... - (NSException *) changeParticipationStatus: (NSString *) newStatus + withDelegate: (iCalPerson *) delegate { NSCalendarDate *date; date = [component recurrenceId]; - return [container changeParticipationStatus: newStatus forRecurrenceId: date]; + return [container changeParticipationStatus: newStatus + withDelegate: delegate + forRecurrenceId: date]; } @end diff --git a/Tests/config.py.in b/Tests/config.py.in index b6207d526..f87d4a546 100644 --- a/Tests/config.py.in +++ b/Tests/config.py.in @@ -5,3 +5,6 @@ password = "mypass" subscriber_username = "otheruser" subscriber_password = "otherpass" + +attendee1 = "user@domain.com" +attendee1_delegate = "otheruser@domain.com" diff --git a/Tests/test-caldav-scheduling.py b/Tests/test-caldav-scheduling.py new file mode 100755 index 000000000..2ab0a6d82 --- /dev/null +++ b/Tests/test-caldav-scheduling.py @@ -0,0 +1,416 @@ +#!/usr/bin/python + +# setup: username must be super-user or have read-access to PUBLIC events in +# both attendee and delegate's personal calendar + +from config import hostname, port, username, password, attendee1, attendee1_delegate + +import datetime +import sys +import time +import unittest +import vobject +import vobject.base +import vobject.icalendar +import webdavlib +import StringIO + +def fetchUserInfo(login): + client = webdavlib.WebDAVClient(hostname, port, username, password) + resource = "/SOGo/dav/%s/" % login + propfind = webdavlib.WebDAVPROPFIND(resource, + ["displayname", + "{urn:ietf:params:xml:ns:caldav}calendar-user-address-set"], + 0) + propfind.xpath_namespace = { "D": "DAV:", + "C": "urn:ietf:params:xml:ns:caldav" } + client.execute(propfind) + assert(propfind.response["status"] == 207) + name_nodes = propfind.xpath_evaluate('/D:multistatus/D:response/D:propstat/D:prop/D:displayname', + None) + email_nodes = propfind.xpath_evaluate('/D:multistatus/D:response/D:propstat/D:prop/C:calendar-user-address-set/D:href', + None) + + return (name_nodes[0].childNodes[0].nodeValue, email_nodes[0].childNodes[0].nodeValue) + +class CalDAVITIPDelegationTest(unittest.TestCase): + def setUp(self): + self.client = webdavlib.WebDAVClient(hostname, port, + username, password) + (self.user_name, self.user_email) = fetchUserInfo(username) + (self.attendee1_name, self.attendee1_email) = fetchUserInfo(attendee1) + (self.attendee1_delegate_name, self.attendee1_delegate_email) = fetchUserInfo(attendee1_delegate) + + self.user_calendar = "/SOGo/dav/%s/Calendar/personal/" % username + self.attendee1_calendar = "/SOGo/dav/%s/Calendar/personal/" % attendee1 + self.attendee1_delegate_calendar = "/SOGo/dav/%s/Calendar/personal/" % attendee1_delegate + + def _newEvent(self): + newCal = vobject.iCalendar() + vevent = newCal.add('vevent') + vevent.add('summary').value = "test event" + vevent.add('transp').value = "OPAQUE" + + now = datetime.datetime.now() + startdate = vevent.add('dtstart') + startdate.value = now + enddate = vevent.add('dtend') + enddate.value = now + datetime.timedelta(0, 3600) + vevent.add('uid').value = "test-delegation" + vevent.add('dtstamp').value = now + vevent.add('last-modified').value = now + vevent.add('created').value = now + + vevent.add('sequence').value = "0" + + return newCal + + def tearDown(self): + self._deleteEvent(self.client, + "%stest-delegation.ics" % self.user_calendar, None) + self._deleteEvent(self.client, + "%stest-delegation.ics" % self.attendee1_calendar, None) + self._deleteEvent(self.client, + "%stest-delegation.ics" % self.attendee1_delegate_calendar, + None) + + def _putEvent(self, client, filename, event, exp_status = 201): + put = webdavlib.HTTPPUT(filename, event.serialize()) + put.content_type = "text/calendar; charset=utf-8" + client.execute(put) + if exp_status is not None: + self.assertEquals(put.response["status"], exp_status) + + def _postEvent(self, client, outbox, event, originator, recipients, + exp_status = 200): + post = webdavlib.CalDAVPOST(outbox, event.serialize(), + originator, recipients) + client.execute(post) + if exp_status is not None: + self.assertEquals(post.response["status"], exp_status) + + def _getEvent(self, client, filename, exp_status = 200): + get = webdavlib.HTTPGET(filename) + client.execute(get) + + if exp_status is not None: + self.assertEquals(get.response["status"], exp_status) + + if get.response["headers"]["content-type"].startswith("text/calendar"): + stream = StringIO.StringIO(get.response["body"]) + event = vobject.base.readComponents(stream).next() + else: + event = None + + return event + + def _deleteEvent(self, client, filename, exp_status = 204): + delete = webdavlib.WebDAVDELETE(filename) + client.execute(delete) + if exp_status is not None: + self.assertEquals(delete.response["status"], exp_status) + + def _eventAttendees(self, event): + attendees = {} + + event_component = event.vevent + for child in event_component.getChildren(): + if child.name == "ATTENDEE": + try: + delegated_to = child.delegated_to_param + except: + delegated_to = "(none)" + try: + delegated_from = child.delegated_from_param + except: + delegated_from = "(none)" + attendees[child.value] = ("%s/%s/%s" + % (child.partstat_param, + delegated_to, + delegated_from)) + + return attendees + + def _compareAttendees(self, compared_event, event): + compared_attendees = self._eventAttendees(compared_event) + compared_emails = compared_attendees.keys() + self.assertTrue(len(compared_emails) > 0, + "no attendee found") + compared_emails.sort() + + attendees = self._eventAttendees(event) + emails = attendees.keys() + emails.sort() + + self.assertEquals(len(compared_emails), len(emails), + "number of attendees is not equal" + + " (actual: %d, exp: %d)" + % (len(compared_emails), len(emails))) + + for email in emails: + self.assertEquals(compared_attendees[email], + attendees[email], + "partstat for attendee '%s' does not match" + " (actual: '%s', expected: '%s')" + % (email, + compared_attendees[email], attendees[email])) + + def testInvitationDelegation(self): + """ invitation delegation """ + + # the invitation must not exist + self._deleteEvent(self.client, + "%stest-delegation.ics" % self.user_calendar, None) + self._deleteEvent(self.client, + "%stest-delegation.ics" % self.attendee1_calendar, None) + self._deleteEvent(self.client, + "%stest-delegation.ics" % self.attendee1_delegate_calendar, + None) + + # 1. org -> attendee => org: 1, attendee: 1 (pst=N-A), delegate: 0 + + invitation = self._newEvent() + invitation.add("method").value = "REQUEST" + organizer = invitation.vevent.add('organizer') + organizer.cn_param = self.user_name + organizer.value = self.user_email + attendee = invitation.vevent.add('attendee') + attendee.cn_param = self.attendee1_name + attendee.rsvp_param = "TRUE" + attendee.partstat_param = "NEEDS-ACTION" + attendee.value = self.attendee1_email + + self._postEvent(self.client, self.user_calendar, invitation, + self.user_email, [self.attendee1_email]) + del invitation.method + self._putEvent(self.client, + "%stest-delegation.ics" % self.user_calendar, + invitation) + + att_inv = self._getEvent(self.client, + "%stest-delegation.ics" + % self.attendee1_calendar) + self._compareAttendees(att_inv, invitation) + + # 2. attendee delegates to delegate + # => org: 1 (updated), attendee: 1 (updated,pst=D), + # delegate: 1 (new,pst=N-A) + + invitation.add("method").value = "REQUEST" + attendee1 = invitation.vevent.attendee + attendee1.partstat_param = "DELEGATED" + attendee1.delegated_to_param = self.attendee1_delegate_email + delegate = invitation.vevent.add('attendee') + delegate.delegated_from_param = self.attendee1_email + delegate.cn_param = self.attendee1_delegate_name + delegate.rsvp_param = "TRUE" + delegate.partstat_param = "NEEDS-ACTION" + delegate.value = self.attendee1_delegate_email + + self._postEvent(self.client, + self.attendee1_calendar, invitation, + self.attendee1_email, [self.attendee1_delegate_email]) + invitation.method.value = "REPLY" + self._postEvent(self.client, + self.attendee1_calendar, invitation, + self.attendee1_email, [self.user_email]) + del invitation.method + self._putEvent(self.client, + "%stest-delegation.ics" % self.attendee1_calendar, + invitation, 204) + + del_inv = self._getEvent(self.client, + "%stest-delegation.ics" + % self.attendee1_delegate_calendar) + self._compareAttendees(del_inv, invitation) + org_inv = self._getEvent(self.client, + "%stest-delegation.ics" % self.user_calendar) + self._compareAttendees(org_inv, invitation) + + # 3. delegate accepts + # => org: 1 (updated), attendee: 1 (updated,pst=D), + # delegate: 1 (accepted,pst=A) + + invitation.add("method").value = "REPLY" + delegate.partstat_param = "ACCEPTED" + self._postEvent(self.client, + self.attendee1_delegate_calendar, invitation, + self.attendee1_delegate_email, [self.user_email, self.attendee1_email]) + del invitation.method + self._putEvent(self.client, + "%stest-delegation.ics" % self.attendee1_delegate_calendar, + invitation, 204) + + org_inv = self._getEvent(self.client, + "%stest-delegation.ics" % self.user_calendar) + self._compareAttendees(org_inv, invitation) + att_inv = self._getEvent(self.client, + "%stest-delegation.ics" % self.attendee1_calendar) + self._compareAttendees(att_inv, invitation) + + # 4. attendee accepts + # => org: 1 (updated), attendee: 1 (updated,pst=A), + # delegate: 0 (cancelled, deleted) + + cancellation = vobject.iCalendar() + cancellation.copy(invitation) + cancellation.add("method").value = "CANCEL" + cancellation.vevent.sequence.value = "1" + self._postEvent(self.client, + self.attendee1_calendar, cancellation, + self.attendee1_email, [self.attendee1_delegate_email]) + + attendee1 = invitation.vevent.attendee + attendee1.partstat_param = "ACCEPTED" + del attendee1.delegated_to_param + invitation.add("method").value = "REPLY" + invitation.vevent.remove(delegate) + self._postEvent(self.client, + self.attendee1_calendar, invitation, + self.attendee1_email, [self.user_email]) + + del invitation.method + self._putEvent(self.client, + "%stest-delegation.ics" % self.attendee1_calendar, + invitation, 204) + + org_inv = self._getEvent(self.client, + "%stest-delegation.ics" % self.user_calendar) + self._compareAttendees(org_inv, invitation) + + del_inv = self._getEvent(self.client, + "%stest-delegation.ics" % self.attendee1_delegate_calendar, 404) + + # 5. org updates inv. + # => org: 1 (updated), attendee: 1 (updated), delegate: 0 + + invitation.add("method").value = "REQUEST" + invitation.vevent.summary.value = "Updated invitation" + invitation.vevent.sequence.value = "1" + attendee.partstat_param = "NEEDS-ACTION" + now = datetime.datetime.now() + invitation.vevent.last_modified.value = now + invitation.vevent.dtstamp.value = now + + self._postEvent(self.client, self.user_calendar, invitation, + self.user_email, [self.attendee1_email]) + + del invitation.method + self._putEvent(self.client, + "%stest-delegation.ics" % self.user_calendar, + invitation, 204) + + att_inv = self._getEvent(self.client, + "%stest-delegation.ics" % self.attendee1_calendar) + self._compareAttendees(att_inv, invitation) + + # 6. attendee delegates to delegate + # => org: 1 (updated), attendee: 1 (updated), delegate: 1 (new) + + invitation.add("method").value = "REQUEST" + attendee1.partstat_param = "DELEGATED" + attendee1.delegated_to_param = self.attendee1_delegate_email + + delegate = invitation.vevent.add('attendee') + delegate.delegated_from_param = self.attendee1_email + delegate.cn_param = self.attendee1_delegate_name + delegate.rsvp_param = "TRUE" + delegate.partstat_param = "NEEDS-ACTION" + delegate.value = self.attendee1_delegate_email + + self._postEvent(self.client, + self.attendee1_calendar, invitation, + self.attendee1_email, [self.attendee1_delegate_email]) + invitation.method.value = "REPLY" + self._postEvent(self.client, + self.attendee1_calendar, invitation, + self.attendee1_email, [self.user_email]) + del invitation.method + self._putEvent(self.client, + "%stest-delegation.ics" % self.attendee1_calendar, + invitation, 204) + + org_inv = self._getEvent(self.client, + "%stest-delegation.ics" % self.user_calendar) + self._compareAttendees(org_inv, invitation) + del_inv = self._getEvent(self.client, + "%stest-delegation.ics" + % self.attendee1_delegate_calendar) + self._compareAttendees(del_inv, invitation) + + # 7. delegate accepts + # => org: 1 (updated), attendee: 1 (updated), delegate: 1 (accepted) + + invitation.add("method").value = "REPLY" + delegate.partstat_param = "ACCEPTED" + self._postEvent(self.client, + self.attendee1_delegate_calendar, invitation, + self.attendee1_delegate_email, [self.user_email, + self.attendee1_email]) + del invitation.method + self._putEvent(self.client, + "%stest-delegation.ics" % self.attendee1_delegate_calendar, + invitation, 204) + + org_inv = self._getEvent(self.client, + "%stest-delegation.ics" % self.user_calendar) + self._compareAttendees(org_inv, invitation) + att_inv = self._getEvent(self.client, + "%stest-delegation.ics" % self.attendee1_calendar) + self._compareAttendees(att_inv, invitation) + + # 8. org updates inv. + # => org: 1 (updated), attendee: 1 (updated,partstat unchanged), + # delegate: 1 (updated,partstat reset) + + invitation.add("method").value = "REQUEST" + now = datetime.datetime.now() + invitation.vevent.last_modified.value = now + invitation.vevent.dtstamp.value = now + invitation.vevent.summary.value = "Updated invitation (again)" + invitation.vevent.sequence.value = "2" + delegate.partstat_param = "NEEDS-ACTION" + + self._postEvent(self.client, self.user_calendar, invitation, + self.user_email, [self.attendee1_email, self.attendee1_delegate_email]) + + del invitation.method + self._putEvent(self.client, + "%stest-delegation.ics" % self.user_calendar, + invitation, 204) + + att_inv = self._getEvent(self.client, + "%stest-delegation.ics" % self.attendee1_calendar) + self._compareAttendees(att_inv, invitation) + del_inv = self._getEvent(self.client, + "%stest-delegation.ics" % self.attendee1_calendar) + self._compareAttendees(del_inv, invitation) + + # 9. org cancels invitation + # => org: 1 (updated), attendee: 0 (cancelled, deleted), + # delegate: 0 (cancelled, deleted) + + invitation.add("method").value = "CANCEL" + now = datetime.datetime.now() + invitation.vevent.last_modified.value = now + invitation.vevent.dtstamp.value = now + invitation.vevent.summary.value = "Cancelled invitation (again)" + invitation.vevent.sequence.value = "3" + + self._postEvent(self.client, self.user_calendar, invitation, + self.user_email, [self.attendee1_email, self.attendee1_delegate_email]) + + del invitation.method + invitation.vevent.remove(attendee) + invitation.vevent.remove(delegate) + self._putEvent(self.client, + "%stest-delegation.ics" % self.user_calendar, + invitation, 204) + + att_inv = self._getEvent(self.client, + "%stest-delegation.ics" % self.attendee1_calendar, 404) + del_inv = self._getEvent(self.client, + "%stest-delegation.ics" % self.attendee1_calendar, 404) + +if __name__ == "__main__": + unittest.main() diff --git a/UI/Scheduler/UIxAppointmentEditor.m b/UI/Scheduler/UIxAppointmentEditor.m index f807c3ce9..3758dda5b 100644 --- a/UI/Scheduler/UIxAppointmentEditor.m +++ b/UI/Scheduler/UIxAppointmentEditor.m @@ -516,14 +516,16 @@ - (id) acceptAction { - [[self clientObject] changeParticipationStatus: @"ACCEPTED"]; + [[self clientObject] changeParticipationStatus: @"ACCEPTED" + withDelegate: nil]; return self; } - (id) declineAction { - [[self clientObject] changeParticipationStatus: @"DECLINED"]; + [[self clientObject] changeParticipationStatus: @"DECLINED" + withDelegate: nil]; return self; }