Monotone-Parent: 14fb6c252ef1563d917c59f289ac3d06479a7eab
Monotone-Revision: d085b8d79d3eca94ece4f311bf3d652d7f20bb00 Monotone-Author: wsourdeau@inverse.ca Monotone-Date: 2009-08-10T20:59:49 Monotone-Branch: ca.inverse.sogomaint-2.0.2
parent
89b67f8434
commit
ecfd8f026e
|
@ -6,6 +6,13 @@
|
||||||
|
|
||||||
2009-08-10 Wolfgang Sourdeau <wsourdeau@inverse.ca>
|
2009-08-10 Wolfgang Sourdeau <wsourdeau@inverse.ca>
|
||||||
|
|
||||||
|
* SoObjects/Appointments/SOGoAppointmentObject.m (-PUTAction):
|
||||||
|
modified to handle the case where the active user has the "respond
|
||||||
|
to" permission, in which case we grab the copy of the iCalendar
|
||||||
|
which is already in the DB and modify the partstats thereof based
|
||||||
|
on the entries found in the uploaded iCalendar. The latter is
|
||||||
|
discarded silently to avoid any further change.
|
||||||
|
|
||||||
* SoObjects/Appointments/iCalEntityObject+SOGo.m
|
* SoObjects/Appointments/iCalEntityObject+SOGo.m
|
||||||
(-userAsParticipant): new method that returns the "PARTICIPANT"
|
(-userAsParticipant): new method that returns the "PARTICIPANT"
|
||||||
entity of the SOGoUser passed as parameter, if found.
|
entity of the SOGoUser passed as parameter, if found.
|
||||||
|
|
|
@ -32,6 +32,7 @@
|
||||||
#import <NGCards/iCalEvent.h>
|
#import <NGCards/iCalEvent.h>
|
||||||
#import <NGCards/iCalEventChanges.h>
|
#import <NGCards/iCalEventChanges.h>
|
||||||
#import <NGCards/iCalPerson.h>
|
#import <NGCards/iCalPerson.h>
|
||||||
|
#import <NGCards/NSCalendarDate+NGCards.h>
|
||||||
#import <SaxObjC/XMLNamespaces.h>
|
#import <SaxObjC/XMLNamespaces.h>
|
||||||
|
|
||||||
#import <SoObjects/SOGo/iCalEntityObject+Utilities.h>
|
#import <SoObjects/SOGo/iCalEntityObject+Utilities.h>
|
||||||
|
@ -1245,52 +1246,129 @@
|
||||||
return @"IPM.Appointment";
|
return @"IPM.Appointment";
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
- (NSDictionary *) _partStatsFromCalendar: (iCalCalendar *) calendar
|
||||||
// If we see "X-SOGo: NoGroupsDecomposition" in the HTTP headers, we
|
{
|
||||||
// simply invoke super's PUTAction.
|
NSMutableDictionary *partStats;
|
||||||
//
|
NSArray *allEvents;
|
||||||
- (id) PUTAction: (WOContext *) _ctx
|
int count, max;
|
||||||
|
iCalEvent *currentEvent;
|
||||||
|
iCalPerson *ownerParticipant;
|
||||||
|
NSString *key;
|
||||||
|
SOGoUser *ownerUser;
|
||||||
|
|
||||||
|
ownerUser = [SOGoUser userWithLogin: owner roles: nil];
|
||||||
|
|
||||||
|
allEvents = [calendar events];
|
||||||
|
max = [allEvents count];
|
||||||
|
partStats = [NSMutableDictionary dictionaryWithCapacity: max];
|
||||||
|
|
||||||
|
for (count = 0; count < max; count++)
|
||||||
|
{
|
||||||
|
currentEvent = [allEvents objectAtIndex: count];
|
||||||
|
ownerParticipant = [currentEvent userAsParticipant: ownerUser];
|
||||||
|
if (ownerParticipant)
|
||||||
|
{
|
||||||
|
if (count == 0)
|
||||||
|
key = @"master";
|
||||||
|
else
|
||||||
|
key = [[currentEvent recurrenceId] iCalFormattedDateTimeString];
|
||||||
|
[partStats setObject: ownerParticipant forKey: key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return partStats;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void) _setupResponseCalendarInRequest: (WORequest *) rq
|
||||||
|
{
|
||||||
|
iCalCalendar *calendar, *putCalendar;
|
||||||
|
NSData *newContent;
|
||||||
|
NSArray *keys;
|
||||||
|
NSDictionary *partStats, *newPartStats;
|
||||||
|
NSString *partStat, *key;
|
||||||
|
int count, max;
|
||||||
|
|
||||||
|
calendar = [self calendar: NO secure: NO];
|
||||||
|
partStats = [self _partStatsFromCalendar: calendar];
|
||||||
|
keys = [partStats allKeys];
|
||||||
|
max = [keys count];
|
||||||
|
if (max > 0)
|
||||||
|
{
|
||||||
|
putCalendar = [iCalCalendar parseSingleFromSource: [rq contentAsString]];
|
||||||
|
newPartStats = [self _partStatsFromCalendar: putCalendar];
|
||||||
|
if ([keys isEqualToArray: [newPartStats allKeys]])
|
||||||
|
{
|
||||||
|
for (count = 0; count < max; count++)
|
||||||
|
{
|
||||||
|
key = [keys objectAtIndex: count];
|
||||||
|
partStat = [[newPartStats objectForKey: key] partStat];
|
||||||
|
[[partStats objectForKey: key] setPartStat: partStat];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newContent = [[calendar versitString]
|
||||||
|
dataUsingEncoding: [rq contentEncoding]];
|
||||||
|
[rq setContent: newContent];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void) _decomposeGroupsInRequest: (WORequest *) rq
|
||||||
{
|
{
|
||||||
iCalCalendar *calendar;
|
iCalCalendar *calendar;
|
||||||
NSArray *allEvents;
|
NSArray *allEvents;
|
||||||
iCalEvent *event;
|
iCalEvent *event;
|
||||||
WORequest *rq;
|
|
||||||
|
|
||||||
BOOL b;
|
|
||||||
int i;
|
int i;
|
||||||
|
BOOL modified;
|
||||||
|
|
||||||
rq = [_ctx request];
|
// If we decomposed at least one group, let's rewrite the content
|
||||||
|
// of the request. Otherwise, leave it as is in case this rewrite
|
||||||
|
// isn't totaly lossless.
|
||||||
|
calendar = [iCalCalendar parseSingleFromSource: [rq contentAsString]];
|
||||||
|
|
||||||
if ([[rq headersForKey: @"X-SOGo"] containsObject: @"NoGroupsDecomposition"])
|
// The algorithm is pretty straightforward:
|
||||||
return [super PUTAction: _ctx];
|
|
||||||
|
|
||||||
//NSLog(@"Content from request: %@", [rq contentAsString]);
|
|
||||||
|
|
||||||
// The algorithm is pretty straightforward:
|
|
||||||
//
|
//
|
||||||
// We get all events
|
// We get all events
|
||||||
// We get all attendees
|
// We get all attendees
|
||||||
// If some are groups, we decompose them
|
// If some are groups, we decompose them
|
||||||
// We regenerate the iCalendar string
|
// We regenerate the iCalendar string
|
||||||
//
|
//
|
||||||
calendar = [iCalCalendar parseSingleFromSource: [rq contentAsString]];
|
|
||||||
allEvents = [calendar events];
|
allEvents = [calendar events];
|
||||||
b = NO;
|
modified = NO;
|
||||||
|
|
||||||
for (i = 0; i < [allEvents count]; i++)
|
for (i = 0; i < [allEvents count]; i++)
|
||||||
{
|
{
|
||||||
event = [allEvents objectAtIndex: i];
|
event = [allEvents objectAtIndex: i];
|
||||||
if ([self expandGroupsInEvent: event])
|
modified |= [self expandGroupsInEvent: event];
|
||||||
b = YES;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//NSLog(@"Content from calendar:secure: %@", [calendar versitString]);
|
|
||||||
|
|
||||||
// If we decomposed at least one group, let's rewrite the content
|
// If we decomposed at least one group, let's rewrite the content
|
||||||
// of the request. Otherwise, leave it as is in case this rewrite
|
// of the request. Otherwise, leave it as is in case this rewrite
|
||||||
// isn't totaly lossless.
|
// isn't totaly lossless.
|
||||||
if (b)
|
if (modified)
|
||||||
[rq setContent: [[calendar versitString] dataUsingEncoding: [rq contentEncoding]]];
|
[rq setContent: [[calendar versitString] dataUsingEncoding: [rq contentEncoding]]];
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// If we see "X-SOGo: NoGroupsDecomposition" in the HTTP headers, we
|
||||||
|
// simply invoke super's PUTAction.
|
||||||
|
//
|
||||||
|
- (id) PUTAction: (WOContext *) _ctx
|
||||||
|
{
|
||||||
|
WORequest *rq;
|
||||||
|
NSArray *roles;
|
||||||
|
|
||||||
|
rq = [_ctx request];
|
||||||
|
|
||||||
|
roles = [[context activeUser] rolesForObject: self inContext: context];
|
||||||
|
if ([roles containsObject: @"ComponentResponder"]
|
||||||
|
&& ![roles containsObject: @"ComponentModifier"])
|
||||||
|
[self _setupResponseCalendarInRequest: rq];
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (![[rq headersForKey: @"X-SOGo"]
|
||||||
|
containsObject: @"NoGroupsDecomposition"])
|
||||||
|
[self _decomposeGroupsInRequest: rq];
|
||||||
|
}
|
||||||
|
|
||||||
return [super PUTAction: _ctx];
|
return [super PUTAction: _ctx];
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,7 +41,7 @@
|
||||||
"ModifyComponent" = ( "Owner", "ComponentModifier", "ObjectEditor" );
|
"ModifyComponent" = ( "Owner", "ComponentModifier", "ObjectEditor" );
|
||||||
"RespondToComponent" = ( "Owner", "ComponentModifier", "ComponentResponder" );
|
"RespondToComponent" = ( "Owner", "ComponentModifier", "ComponentResponder" );
|
||||||
"Access Object" = ( "Owner", "Organizer", "ObjectCreator", "ObjectEraser", "Participant", "ComponentModifier", "ComponentResponder", "ComponentViewer", "ComponentDAndTViewer" );
|
"Access Object" = ( "Owner", "Organizer", "ObjectCreator", "ObjectEraser", "Participant", "ComponentModifier", "ComponentResponder", "ComponentViewer", "ComponentDAndTViewer" );
|
||||||
"Change Images and Files" = ( "Owner", "Organizer", "ComponentModifier", "ObjectEditor" );
|
"Change Images and Files" = ( "Owner", "Organizer", "ComponentModifier", "ComponentResponder", "ObjectEditor" );
|
||||||
"Access Contents Information" = ( "Owner", "Organizer", "Participant", "ComponentModifier", "ComponentResponder", "ComponentViewer", "ComponentDAndTViewer" );
|
"Access Contents Information" = ( "Owner", "Organizer", "Participant", "ComponentModifier", "ComponentResponder", "ComponentViewer", "ComponentDAndTViewer" );
|
||||||
"WebDAV Access" = ( "Owner", "Organizer", "ObjectCreator", "ObjectEraser", "Participant", "ComponentModifier", "ComponentResponder", "ComponentViewer", "ComponentDAndTViewer" );
|
"WebDAV Access" = ( "Owner", "Organizer", "ObjectCreator", "ObjectEraser", "Participant", "ComponentModifier", "ComponentResponder", "ComponentViewer", "ComponentDAndTViewer" );
|
||||||
};
|
};
|
||||||
|
@ -61,7 +61,7 @@
|
||||||
"ModifyComponent" = ( "Owner", "ComponentModifier" );
|
"ModifyComponent" = ( "Owner", "ComponentModifier" );
|
||||||
"RespondToComponent" = ( "Participant", "ComponentModifier", "ComponentResponder" );
|
"RespondToComponent" = ( "Participant", "ComponentModifier", "ComponentResponder" );
|
||||||
"Access Object" = ( "Owner", "Organizer", "Participant", "ComponentModifier", "ComponentResponder", "ComponentViewer", "ComponentDAndTViewer" );
|
"Access Object" = ( "Owner", "Organizer", "Participant", "ComponentModifier", "ComponentResponder", "ComponentViewer", "ComponentDAndTViewer" );
|
||||||
"Change Images and Files" = ( "Owner", "Organizer", "ComponentModifier" );
|
"Change Images and Files" = ( "Owner", "Organizer", "ComponentModifier", "ComponentResponder" );
|
||||||
"Access Contents Information" = ( "Owner", "Organizer", "Participant", "ComponentModifier", "ComponentResponder", "ComponentViewer", "ComponentDAndTViewer" );
|
"Access Contents Information" = ( "Owner", "Organizer", "Participant", "ComponentModifier", "ComponentResponder", "ComponentViewer", "ComponentDAndTViewer" );
|
||||||
"WebDAV Access" = ( "Owner", "Organizer", "Participant", "ComponentModifier", "ComponentResponder", "ComponentViewer", "ComponentDAndTViewer" );
|
"WebDAV Access" = ( "Owner", "Organizer", "Participant", "ComponentModifier", "ComponentResponder", "ComponentViewer", "ComponentDAndTViewer" );
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,14 +9,31 @@ import xml.xpath
|
||||||
import time
|
import time
|
||||||
|
|
||||||
# TODO:
|
# TODO:
|
||||||
# - cal: we don't test "respond" yet
|
# - cal: complete test for "modify": "respond to" causes a 204 but no actual
|
||||||
|
# modification should occur
|
||||||
# - ab: testcase for addressbook-query, webdav-sync (no "calendar-data"
|
# - ab: testcase for addressbook-query, webdav-sync (no "calendar-data"
|
||||||
# equivalent)
|
# equivalent)
|
||||||
# ? cal: testcase for "calendar-query"
|
# - cal: testcase for "calendar-query"
|
||||||
# - test rights validity:
|
# - test rights validity:
|
||||||
# - send invalid rights to SOGo and expect failures
|
# - send invalid rights to SOGo and expect failures
|
||||||
# - refetch the set of rights and make sure it matches what was set
|
# - refetch the set of rights and make sure it matches what was set
|
||||||
# originally
|
# originally
|
||||||
|
# - test "current-user-acl-set"
|
||||||
|
|
||||||
|
def fetchUserEmail(login):
|
||||||
|
client = webdavlib.WebDAVClient(hostname, port,
|
||||||
|
username, password)
|
||||||
|
resource = '/SOGo/dav/%s/' % login
|
||||||
|
propfind = webdavlib.WebDAVPROPFIND(resource,
|
||||||
|
["{urn:ietf:params:xml:ns:caldav}calendar-user-address-set"],
|
||||||
|
0)
|
||||||
|
client.execute(propfind)
|
||||||
|
xpath_context = xml.xpath.CreateContext(propfind.response["document"])
|
||||||
|
xpath_context.setNamespaces({ "D": "DAV:",
|
||||||
|
"C": "urn:ietf:params:xml:ns:caldav" })
|
||||||
|
nodes = xml.xpath.Evaluate('/D:multistatus/D:response/D:propstat/D:prop/C:calendar-user-address-set/D:href', None, xpath_context)
|
||||||
|
|
||||||
|
return nodes[0].childNodes[0].nodeValue
|
||||||
|
|
||||||
class DAVAclTest(unittest.TestCase):
|
class DAVAclTest(unittest.TestCase):
|
||||||
resource = None
|
resource = None
|
||||||
|
@ -102,15 +119,18 @@ DTEND:20090805T140000Z
|
||||||
CLASS:%(class)s
|
CLASS:%(class)s
|
||||||
DESCRIPTION:%(class)s description
|
DESCRIPTION:%(class)s description
|
||||||
LOCATION:location
|
LOCATION:location
|
||||||
CREATED:20090805T100000Z
|
%(organizer_line)s%(attendee_line)sCREATED:20090805T100000Z
|
||||||
DTSTAMP:20090805T100000Z
|
DTSTAMP:20090805T100000Z
|
||||||
END:VEVENT
|
END:VEVENT
|
||||||
END:VCALENDAR"""
|
END:VCALENDAR"""
|
||||||
|
|
||||||
class DAVCalendarAclTest(DAVAclTest):
|
class DAVCalendarAclTest(DAVAclTest):
|
||||||
resource = '/SOGo/dav/%s/Calendar/test-dav-acl/' % username
|
resource = '/SOGo/dav/%s/Calendar/test-dav-acl/' % username
|
||||||
|
user_email = None
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
if self.user_email is None:
|
||||||
|
self.user_email = fetchUserEmail(username)
|
||||||
DAVAclTest.setUp(self)
|
DAVAclTest.setUp(self)
|
||||||
self.classToICSClass = { "pu": "PUBLIC",
|
self.classToICSClass = { "pu": "PUBLIC",
|
||||||
"pr": "PRIVATE",
|
"pr": "PRIVATE",
|
||||||
|
@ -139,15 +159,32 @@ class DAVCalendarAclTest(DAVAclTest):
|
||||||
"""'create', 'delete', 'view d&t' PUBLIC, 'modify' PRIVATE"""
|
"""'create', 'delete', 'view d&t' PUBLIC, 'modify' PRIVATE"""
|
||||||
self._testRights({ "c": True, "d": True, "pu": "d", "pr": "m" })
|
self._testRights({ "c": True, "d": True, "pu": "d", "pr": "m" })
|
||||||
|
|
||||||
|
def testCreateRespondToPublic(self):
|
||||||
|
"""'create', 'respond to' PUBLIC"""
|
||||||
|
self._testRights({ "c": True, "pu": "r" })
|
||||||
|
|
||||||
def testNothing(self):
|
def testNothing(self):
|
||||||
"""no right given"""
|
"""no right given"""
|
||||||
self._testRights({})
|
self._testRights({})
|
||||||
|
|
||||||
def _putEvent(self, client, filename,
|
def _putEvent(self, client, filename,
|
||||||
event_class = "PUBLIC", exp_status = 201):
|
event_class = "PUBLIC",
|
||||||
|
exp_status = 201,
|
||||||
|
organizer = None, attendee = None,
|
||||||
|
partstat = "NEEDS-ACTION"):
|
||||||
url = "%s%s" % (self.resource, filename)
|
url = "%s%s" % (self.resource, filename)
|
||||||
|
if organizer is not None:
|
||||||
|
organizer_line = "ORGANIZER:%s\n" % organizer
|
||||||
|
else:
|
||||||
|
organizer_line = ""
|
||||||
|
if attendee is not None:
|
||||||
|
attendee_line = "ATTENDEE;PARTSTAT=%s:%s\n" % (partstat, attendee)
|
||||||
|
else:
|
||||||
|
attendee_line = ""
|
||||||
event = event_template % { "class": event_class,
|
event = event_template % { "class": event_class,
|
||||||
"filename": filename }
|
"filename": filename,
|
||||||
|
"organizer_line": organizer_line,
|
||||||
|
"attendee_line": attendee_line }
|
||||||
put = webdavlib.HTTPPUT(url, event, "text/calendar; charset=utf-8")
|
put = webdavlib.HTTPPUT(url, event, "text/calendar; charset=utf-8")
|
||||||
client.execute(put)
|
client.execute(put)
|
||||||
self.assertEquals(put.response["status"], exp_status,
|
self.assertEquals(put.response["status"], exp_status,
|
||||||
|
@ -219,19 +256,24 @@ class DAVCalendarAclTest(DAVAclTest):
|
||||||
right = None
|
right = None
|
||||||
|
|
||||||
event = self._getEvent(event_class)
|
event = self._getEvent(event_class)
|
||||||
self._checkEventRight("GET", event, event_class, right)
|
self._checkViewEventRight("GET", event, event_class, right)
|
||||||
event = self._propfindEvent(event_class)
|
event = self._propfindEvent(event_class)
|
||||||
self._checkEventRight("PROPFIND", event, event_class, right)
|
self._checkViewEventRight("PROPFIND", event, event_class, right)
|
||||||
event = self._multigetEvent(event_class)
|
event = self._multigetEvent(event_class)
|
||||||
self._checkEventRight("multiget", event, event_class, right)
|
self._checkViewEventRight("multiget", event, event_class, right)
|
||||||
event = self._webdavSyncEvent(event_class)
|
event = self._webdavSyncEvent(event_class)
|
||||||
self._checkEventRight("webdav-sync", event, event_class, right)
|
self._checkViewEventRight("webdav-sync", event, event_class, right)
|
||||||
|
|
||||||
self._testModify(event_class, right)
|
self._testModify(event_class, right)
|
||||||
|
self._testRespondTo(event_class, right)
|
||||||
|
|
||||||
def _getEvent(self, event_class):
|
def _getEvent(self, event_class, is_invitation = False):
|
||||||
icsClass = self.classToICSClass[event_class]
|
icsClass = self.classToICSClass[event_class]
|
||||||
url = "%s%s.ics" % (self.resource, icsClass.lower())
|
if is_invitation:
|
||||||
|
filename = "invitation-%s" % icsClass.lower()
|
||||||
|
else:
|
||||||
|
filename = "%s" % icsClass.lower()
|
||||||
|
url = "%s%s.ics" % (self.resource, filename)
|
||||||
get = webdavlib.HTTPGET(url)
|
get = webdavlib.HTTPGET(url)
|
||||||
self.subscriber_client.execute(get)
|
self.subscriber_client.execute(get)
|
||||||
|
|
||||||
|
@ -315,7 +357,7 @@ class DAVCalendarAclTest(DAVAclTest):
|
||||||
|
|
||||||
return event
|
return event
|
||||||
|
|
||||||
def _checkEventRight(self, operation, event, event_class, right):
|
def _checkViewEventRight(self, operation, event, event_class, right):
|
||||||
if right is None:
|
if right is None:
|
||||||
self.assertEquals(event, None,
|
self.assertEquals(event, None,
|
||||||
"None right expecting event invisibility for"
|
"None right expecting event invisibility for"
|
||||||
|
@ -327,8 +369,10 @@ class DAVCalendarAclTest(DAVAclTest):
|
||||||
if right == "v" or right == "r" or right == "m":
|
if right == "v" or right == "r" or right == "m":
|
||||||
icsClass = self.classToICSClass[event_class]
|
icsClass = self.classToICSClass[event_class]
|
||||||
complete_event = (event_template % { "class": icsClass,
|
complete_event = (event_template % { "class": icsClass,
|
||||||
"filename": "%s.ics" % icsClass.lower() })
|
"filename": "%s.ics" % icsClass.lower(),
|
||||||
self.assertTrue(event == complete_event,
|
"organizer_line": "",
|
||||||
|
"attendee_line": ""})
|
||||||
|
self.assertTrue(event.strip() == complete_event.strip(),
|
||||||
"Right '%s' should return complete event"
|
"Right '%s' should return complete event"
|
||||||
" during operation '%s'"
|
" during operation '%s'"
|
||||||
% (right, operation))
|
% (right, operation))
|
||||||
|
@ -367,7 +411,7 @@ class DAVCalendarAclTest(DAVAclTest):
|
||||||
% key)
|
% key)
|
||||||
|
|
||||||
def _testModify(self, event_class, right):
|
def _testModify(self, event_class, right):
|
||||||
if right == "m":
|
if right == "m" or right == "r":
|
||||||
exp_code = 204
|
exp_code = 204
|
||||||
else:
|
else:
|
||||||
exp_code = 403
|
exp_code = 403
|
||||||
|
@ -376,6 +420,42 @@ class DAVCalendarAclTest(DAVAclTest):
|
||||||
self._putEvent(self.subscriber_client, filename, icsClass,
|
self._putEvent(self.subscriber_client, filename, icsClass,
|
||||||
exp_code)
|
exp_code)
|
||||||
|
|
||||||
|
def _testRespondTo(self, event_class, right):
|
||||||
|
icsClass = self.classToICSClass[event_class]
|
||||||
|
filename = "invitation-%s.ics" % icsClass.lower()
|
||||||
|
self._putEvent(self.client, filename, icsClass,
|
||||||
|
201,
|
||||||
|
"mailto:nobody@somewhere.com", self.user_email,
|
||||||
|
"NEEDS-ACTION")
|
||||||
|
|
||||||
|
if right == "m" or right == "r":
|
||||||
|
exp_code = 204
|
||||||
|
else:
|
||||||
|
exp_code = 403
|
||||||
|
|
||||||
|
# here we only do 'passive' validation: if a user has a "respond to"
|
||||||
|
# right, only the attendee entry will me modified. The change of
|
||||||
|
# organizer must thus be silently ignored below.
|
||||||
|
self._putEvent(self.subscriber_client, filename, icsClass,
|
||||||
|
exp_code, "mailto:someone@nowhere.com", self.user_email,
|
||||||
|
"ACCEPTED")
|
||||||
|
if exp_code == 204:
|
||||||
|
att_line = "ATTENDEE;PARTSTAT=ACCEPTED:%s\n" % self.user_email
|
||||||
|
if right == "r":
|
||||||
|
exp_event = event_template % {"class": icsClass,
|
||||||
|
"filename": filename,
|
||||||
|
"organizer_line": "ORGANIZER:mailto:nobody@somewhere.com\n",
|
||||||
|
"attendee_line": att_line}
|
||||||
|
else:
|
||||||
|
exp_event = event_template % {"class": icsClass,
|
||||||
|
"filename": filename,
|
||||||
|
"organizer_line": "ORGANIZER:mailto:someone@nowhere.com\n",
|
||||||
|
"attendee_line": att_line}
|
||||||
|
event = self._getEvent(event_class, True).replace("\r", "")
|
||||||
|
self.assertEquals(exp_event.strip(), event.strip(),
|
||||||
|
"'respond to' event does not match:\nreceived:\n"
|
||||||
|
"%s\nexpected:\n%s" % (event, exp_event))
|
||||||
|
|
||||||
# Addressbook:
|
# Addressbook:
|
||||||
# short rights notation: { "c": create,
|
# short rights notation: { "c": create,
|
||||||
# "d": delete,
|
# "d": delete,
|
||||||
|
|
Loading…
Reference in New Issue