[CalendarServer-changes] [9925] CalendarServer/trunk/twistedcaldav/scheduling
source_changes at macosforge.org
source_changes at macosforge.org
Thu Oct 11 08:53:12 PDT 2012
Revision: 9925
http://trac.calendarserver.org//changeset/9925
Author: cdaboo at apple.com
Date: 2012-10-11 08:53:12 -0700 (Thu, 11 Oct 2012)
Log Message:
-----------
Fix for the case when one cancelled instance is replaced by another by an attendee. Whitespace clean-up.
Modified Paths:
--------------
CalendarServer/trunk/twistedcaldav/scheduling/icaldiff.py
CalendarServer/trunk/twistedcaldav/scheduling/implicit.py
CalendarServer/trunk/twistedcaldav/scheduling/itip.py
Modified: CalendarServer/trunk/twistedcaldav/scheduling/icaldiff.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/icaldiff.py 2012-10-11 15:28:35 UTC (rev 9924)
+++ CalendarServer/trunk/twistedcaldav/scheduling/icaldiff.py 2012-10-11 15:53:12 UTC (rev 9925)
@@ -36,26 +36,27 @@
log = Logger()
class iCalDiff(object):
-
+
def __init__(self, oldcalendar, newcalendar, smart_merge):
"""
-
+
@param oldcalendar:
@type oldcalendar:
@param newcalendar:
@type newcalendar:
"""
-
+
self.oldcalendar = oldcalendar
self.newcalendar = newcalendar
self.smart_merge = smart_merge
-
+
+
def organizerDiff(self):
"""
Diff the two calendars looking for changes that should trigger implicit scheduling if
changed by an organizer. Basically any change except for anything related to a VALARM.
"""
-
+
# If smart merge is needed we have to do this before trying the diff
if self.smart_merge:
log.debug("organizerDiff: doing smart Organizer diff/merge")
@@ -76,7 +77,7 @@
calendar.removePropertyParameters("ATTENDEE", ("RSVP", "SCHEDULE-STATUS", "SCHEDULE-FORCE-SEND",))
calendar.normalizeAll()
return calendar
-
+
# Normalize components for comparison
oldcalendar_norm = duplicateAndNormalize(self.oldcalendar)
newcalendar_norm = duplicateAndNormalize(self.newcalendar)
@@ -84,6 +85,7 @@
result = oldcalendar_norm == newcalendar_norm
return result
+
def _organizerMerge(self):
"""
Merge changes to ATTENDEE properties in oldcalendar into newcalendar.
@@ -91,20 +93,21 @@
organizer = normalizeCUAddr(self.newcalendar.masterComponent().propertyValue("ORGANIZER"))
self._doSmartMerge(organizer, True)
+
def _doSmartMerge(self, ignore_attendee, is_organizer):
"""
Merge changes to ATTENDEE properties in oldcalendar into newcalendar.
"""
-
+
old_master = self.oldcalendar.masterComponent()
new_master = self.newcalendar.masterComponent()
-
+
# Do master merge first
self._tryComponentMerge(old_master, new_master, ignore_attendee, is_organizer)
# New check the matching components
for old_component in self.oldcalendar.subcomponents():
-
+
# Make sure we have an appropriate component
if old_component.name() == "VTIMEZONE":
continue
@@ -118,7 +121,7 @@
# If the old component was cancelled ignore when an attendee
if not is_organizer and old_component.propertyValue("STATUS") == "CANCELLED":
continue
-
+
# Determine whether the instance is still valid in the new calendar
new_component = self.newcalendar.deriveInstance(rid)
if new_component:
@@ -133,7 +136,7 @@
# Check the new instances not in the old calendar
for new_component in self.newcalendar.subcomponents():
-
+
# Make sure we have an appropriate component
if new_component.name() == "VTIMEZONE":
continue
@@ -147,7 +150,7 @@
# If the new component is cancelled ignore when an attendee
if not is_organizer and new_component.propertyValue("STATUS") == "CANCELLED":
continue
-
+
# Try to derive a new instance in the client and transfer attendee status
old_component = self.oldcalendar.deriveInstance(rid)
if old_component:
@@ -156,11 +159,13 @@
else:
# Ignore as we have no state for the new instance
pass
-
+
+
def _tryComponentMerge(self, old_comp, new_comp, ignore_attendee_value, is_organizer):
if not is_organizer or not self._organizerChangePreventsMerge(old_comp, new_comp):
self._transferAttendees(old_comp, new_comp, ignore_attendee_value)
+
def _organizerChangePreventsMerge(self, old_comp, new_comp):
"""
Check whether a change from an Organizer needs a re-schedule which means that any
@@ -175,7 +180,7 @@
"""
props_to_test = ("DTSTART", "DTEND", "DURATION", "RRULE", "RDATE", "EXDATE", "RECURRENCE-ID",)
-
+
for prop in props_to_test:
# Change => no merge
old_props = set(old_comp.properties(prop))
@@ -184,7 +189,8 @@
return True
return False
-
+
+
def _transferAttendees(self, old_comp, new_comp, ignore_attendee_value):
"""
Transfer Attendee PARTSTAT from old component to new component.
@@ -206,7 +212,7 @@
old_attendees[value] = attendee
for new_attendee in new_comp.properties("ATTENDEE"):
-
+
# Whenever SCHEDULE-FORCE-SEND is explicitly set by the Organizer we assume the Organizer
# is deliberately overwriting PARTSTAT
if new_attendee.parameterValue("SCHEDULE-FORCE-SEND", "") == "REQUEST":
@@ -219,7 +225,8 @@
self._transferParameter(old_attendee, new_attendee, "PARTSTAT")
self._transferParameter(old_attendee, new_attendee, "RSVP")
self._transferParameter(old_attendee, new_attendee, "SCHEDULE-STATUS")
-
+
+
def _transferParameter(self, old_property, new_property, parameter):
paramvalue = old_property.parameterValue(parameter)
if paramvalue is None:
@@ -230,21 +237,22 @@
else:
new_property.setParameter(parameter, paramvalue)
+
def attendeeMerge(self, attendee):
"""
Merge the ATTENDEE specific changes with the organizer's view of the attendee's event.
This will remove any attempt by the attendee to change things like the time or location.
-
+
@param attendee: the value of the ATTENDEE property corresponding to the attendee making the change
@type attendee: C{str}
-
+
@return: C{tuple} of:
C{bool} - change is allowed
C{bool} - iTIP reply needs to be sent
C{list} - list of RECURRENCE-IDs changed
L{Component} - new calendar object to store
"""
-
+
self.attendee = normalizeCUAddr(attendee)
returnCalendar = self.oldcalendar.duplicate()
@@ -252,7 +260,7 @@
changeCausesReply = False
changedRids = []
-
+
# First get uid/rid map of components
def mapComponents(calendar):
map = {}
@@ -269,7 +277,7 @@
cancelledRids.add(rid)
if rid is None:
master = component
-
+
# Normalize each master by adding any STATUS:CANCELLED components as EXDATEs
exdates = None
if master:
@@ -277,9 +285,9 @@
exdates = set()
for exdate in master.properties("EXDATE"):
exdates.update([value.getValue().duplicate().adjustToUTC() for value in exdate.value()])
-
+
return exdates, map, master
-
+
exdatesold, mapold, masterold = mapComponents(self.oldcalendar)
setold = set(mapold.keys())
exdatesnew, mapnew, masternew = mapComponents(self.newcalendar)
@@ -303,18 +311,18 @@
return False, False, (), None
else:
componentold = self.oldcalendar.overriddenComponent(masternewStart)
-
+
# Take the recurrence ID from component1 and fix map2/set2
keynew = (masternew.name(), masternew.propertyValue("UID"), None)
componentnew = mapnew[keynew]
del mapnew[keynew]
-
+
ridold = componentold.getRecurrenceIDUTC()
newkeynew = (masternew.name(), masternew.propertyValue("UID"), ridold)
mapnew[newkeynew] = componentnew
setnew.remove(keynew)
setnew.add(newkeynew)
-
+
# All the components in oldcalendar must be in newcalendar unless they are CANCELLED
for key in setold - setnew:
_ignore_name, _ignore_uid, rid = key
@@ -327,7 +335,7 @@
if self._attendeeDecline(overridden):
changeCausesReply = True
changedRids.append(rid.getText() if rid else "")
-
+
# When a master component is present we keep the missing override in place but mark it as hidden.
# When no master is present we remove the override,
if exdatesnew is not None:
@@ -338,7 +346,7 @@
# We used to generate a 403 here - but instead we now ignore this error and let the server data
# override the client
self._logDiffError("attendeeMerge: Missing uncancelled component from first calendar: %s" % (key,))
- else:
+ else:
if exdatesnew is not None and rid not in exdatesnew:
# We used to generate a 403 here - but instead we now ignore this error and let the server data
# override the client
@@ -350,11 +358,11 @@
if returnMaster:
# Use the original R-ID value so we preserve the timezone
original_rid = component.propertyValue("RECURRENCE-ID")
- returnMaster.addProperty(Property("EXDATE", [original_rid,]))
-
+ returnMaster.addProperty(Property("EXDATE", [original_rid, ]))
+
# Derive a new component in the new calendar for each new one in setnew
for key in setnew - setold:
-
+
# First check if the attendee's copy is cancelled and properly EXDATE'd
# and skip it if so.
_ignore_name, _ignore_uid, rid = key
@@ -387,7 +395,7 @@
returnCalendar.addComponent(newOverride)
# So now returnCalendar has all the same components as set2. Check changes and do transfers.
-
+
# Make sure the same VCALENDAR properties match
if not self._checkVCALENDARProperties(returnCalendar, self.newcalendar):
# We used to generate a 403 here - but instead we now ignore this error and let the server data
@@ -402,7 +410,7 @@
_ignore_name, _ignore_uid, rid = key
serverData = returnCalendar.overriddenComponent(rid)
clientData = mapnew[key]
-
+
allowed, reply = self._transferAttendeeData(serverData, clientData, declines)
if not allowed:
# We used to generate a 403 here - but instead we now ignore this error and let the server data
@@ -422,7 +430,7 @@
if self._attendeeDecline(overridden):
changeCausesReply = True
changedRids.append(decline.getText() if decline else "")
-
+
# When a master component is present we keep the missing override in place but mark it as hidden.
# When no master is present we remove the override,
if exdatesnew is not None:
@@ -434,43 +442,45 @@
return True, changeCausesReply, changedRids, returnCalendar
+
def _checkVCALENDARProperties(self, serverData, clientData):
self._transferProperty("X-CALENDARSERVER-ACCESS", serverData, clientData)
# Get property differences in the VCALENDAR objects
propdiff = set(serverData.properties()) ^ set(clientData.properties())
-
+
# Ignore certain properties
ignored = ("PRODID", "CALSCALE",)
propdiff = set([prop for prop in propdiff if prop.name() not in ignored])
-
+
result = len(propdiff) == 0
if not result:
log.debug("VCALENDAR properties differ: %s" % (propdiff,))
return result
+
def _transferAttendeeData(self, serverComponent, clientComponent, declines):
-
+
# We are skipping this check now - instead we let the server data override the broken client data
# First check validity of date-time related properties and get removed components which are declines
self._checkInvalidChanges(serverComponent, clientComponent, declines)
-
+
# Now look for items to transfer from one to the other.
# We care about the ATTENDEE's PARTSTAT, TRANSP, VALARMS, X-APPLE-NEEDS-REPLY,
# DTSTAMP, LAST-MODIFIED, COMPLETED, and ATTACH's referring to a dropbox
-
+
replyNeeded = False
# ATTENDEE/PARTSTAT/RSVP
serverAttendee = serverComponent.getAttendeeProperty((self.attendee,))
clientAttendee = clientComponent.getAttendeeProperty((self.attendee,))
-
+
# Possible case where one ATTENDEE prop is missing - this happens with a "fake" master sometimes
if serverAttendee is None or clientAttendee is None:
log.err("ATTENDEE for user making an attendee change is missing: %s" % (self.attendee,))
return False, False
-
+
if serverAttendee.parameterValue("PARTSTAT", "NEEDS-ACTION") != clientAttendee.parameterValue("PARTSTAT", "NEEDS-ACTION"):
serverAttendee.setParameter("PARTSTAT", clientAttendee.parameterValue("PARTSTAT", "NEEDS-ACTION"))
replyNeeded = True
@@ -490,7 +500,7 @@
self._transferProperty("LAST-MODIFIED", serverComponent, clientComponent)
self._transferProperty("X-APPLE-NEEDS-REPLY", serverComponent, clientComponent)
self._transferProperty("COMPLETED", serverComponent, clientComponent)
-
+
# Dropbox - this now never returns false
self._transferDropBoxData(serverComponent, clientComponent)
@@ -499,14 +509,15 @@
for comp in clientComponent.subcomponents():
if comp.name() == "VALARM":
serverComponent.addComponent(comp)
-
+
return True, replyNeeded
-
+
+
def _transferDropBoxData(self, serverComponent, clientComponent):
-
+
serverDropbox = serverComponent.propertyValue("X-APPLE-DROPBOX")
clientDropbox = clientComponent.propertyValue("X-APPLE-DROPBOX")
-
+
# Handle four cases
if not clientDropbox:
return True
@@ -527,7 +538,7 @@
dataValue = attachment.value()
if dataValue.find(serverDropbox) != -1:
serverComponent.removeProperty(attachment)
-
+
# Copy new ATTACH's to server
for attachment in tuple(clientComponent.properties("ATTACH")):
valueType = attachment.parameterValue("VALUE")
@@ -535,16 +546,17 @@
dataValue = attachment.value()
if dataValue.find(serverDropbox) != -1:
serverComponent.addProperty(attachment)
-
+
return True
-
+
+
def _checkInvalidChanges(self, serverComponent, clientComponent, declines):
-
+
# Properties we care about: DTSTART, DTEND, DURATION, RRULE, RDATE, EXDATE
-
+
serverProps = self._getNormalizedDateTimeProperties(serverComponent)
clientProps = self._getNormalizedDateTimeProperties(clientComponent)
-
+
# Need to special case EXDATEs as an Attendee can effectively DECLINE by adding an EXDATE
if serverProps[:-1] != clientProps[:-1]:
invalidChanges = []
@@ -562,30 +574,31 @@
return True
else:
return True
-
+
+
def _getNormalizedDateTimeProperties(self, component):
-
+
# Basic time properties
if component.name() in ("VEVENT", "VJOURNAL",):
dtstart = component.getProperty("DTSTART")
dtend = component.getProperty("DTEND")
duration = component.getProperty("DURATION")
-
+
timeRange = PyCalendarPeriod(
- start = dtstart.value() if dtstart is not None else None,
- end = dtend.value() if dtend is not None else None,
- duration = duration.value() if duration is not None else None,
+ start=dtstart.value() if dtstart is not None else None,
+ end=dtend.value() if dtend is not None else None,
+ duration=duration.value() if duration is not None else None,
)
newdue = None
-
+
elif component.name() == "VTODO":
dtstart = component.getProperty("DTSTART")
duration = component.getProperty("DURATION")
-
+
if dtstart or duration:
timeRange = PyCalendarPeriod(
- start = dtstart.value() if dtstart is not None else None,
- duration = duration.value() if duration is not None else None,
+ start=dtstart.value() if dtstart is not None else None,
+ duration=duration.value() if duration is not None else None,
)
else:
timeRange = PyCalendarPeriod()
@@ -593,7 +606,7 @@
newdue = component.getProperty("DUE")
if newdue is not None:
newdue = newdue.value().duplicate().adjustToUTC()
-
+
# Recurrence rules - we need to normalize the order of the value parts
newrrules = set()
rrules = component.properties("RRULE")
@@ -602,7 +615,7 @@
indexedTokens.update([valuePart.split("=") for valuePart in rrule.value().getText().split(";")])
sortedValue = ";".join(["%s=%s" % (key, value,) for key, value in sorted(indexedTokens.iteritems(), key=lambda x:x[0])])
newrrules.add(sortedValue)
-
+
# RDATEs
newrdates = set()
rdates = component.properties("RDATE")
@@ -611,7 +624,7 @@
if isinstance(value, PyCalendarDateTime):
value = value.duplicate().adjustToUTC()
newrdates.add(value)
-
+
# EXDATEs
newexdates = set()
exdates = component.properties("EXDATE")
@@ -620,6 +633,7 @@
return timeRange.getStart(), timeRange.getEnd(), newdue, newrrules, newrdates, newexdates
+
def _transferProperty(self, propName, serverComponent, clientComponent):
changed = False
@@ -640,7 +654,7 @@
@param component:
@type component:
-
+
@return: C{bool} indicating whether the PARTSTAT value was in fact changed
"""
attendee = component.getAttendeeProperty((self.attendee,))
@@ -649,7 +663,7 @@
if attendee is None:
log.err("ATTENDEE for user making an attendee change is missing: %s" % (self.attendee,))
return False
-
+
partstatChanged = attendee.parameterValue("PARTSTAT", "NEEDS-ACTION") != "DECLINED"
attendee.setParameter("PARTSTAT", "DECLINED")
prop = component.getProperty("X-APPLE-NEEDS-REPLY")
@@ -657,6 +671,7 @@
component.removeProperty(prop)
return partstatChanged
+
def whatIsDifferent(self):
"""
Compare the two calendar objects in their entirety and return a list of properties
@@ -674,7 +689,7 @@
rid = component.getRecurrenceIDUTC()
map[(name, uid, rid,)] = component
return map
-
+
rids = {}
oldmap = mapComponents(self.oldcalendar)
@@ -687,7 +702,7 @@
component1 = oldmap[key]
component2 = newmap[key]
self._diffComponents(component1, component2, rids)
-
+
# Now verify that each additional component in oldset matches a derived component in newset
for key in oldset - newset:
oldcomponent = oldmap[key]
@@ -695,7 +710,7 @@
if newcomponent is None:
continue
self._diffComponents(oldcomponent, newcomponent, rids)
-
+
# Now verify that each additional component in oldset matches a derived component in newset
for key in newset - oldset:
oldcomponent = self.oldcalendar.deriveInstance(key[2])
@@ -703,9 +718,10 @@
continue
newcomponent = newmap[key]
self._diffComponents(oldcomponent, newcomponent, rids)
-
+
return rids
+
def _componentDuplicateAndNormalize(self, comp):
comp = comp.duplicate()
comp.normalizePropertyValueLists("EXDATE")
@@ -717,14 +733,15 @@
iTipGenerator.prepareSchedulingMessage(comp, reply=True)
return comp
+
def _diffComponents(self, comp1, comp2, rids):
-
+
assert isinstance(comp1, Component) and isinstance(comp2, Component)
-
+
if comp1.name() != comp2.name():
log.debug("Component names are different: '%s' and '%s'" % (comp1.name(), comp2.name()))
return
-
+
# Duplicate then normalize for comparison
comp1 = self._componentDuplicateAndNormalize(comp1)
comp2 = self._componentDuplicateAndNormalize(comp2)
@@ -732,7 +749,7 @@
# Diff all the properties
propdiff = set(comp1.properties()) ^ set(comp2.properties())
addedChanges = False
-
+
propsChanged = {}
for prop in propdiff:
if prop.name() in (
@@ -755,11 +772,12 @@
if "_TZID" in propsChanged[prop.name()]:
propsChanged[prop.name()].remove("_TZID")
propsChanged[prop.name()].add("TZID")
-
+
if addedChanges:
rid = comp1.getRecurrenceIDUTC()
rids[rid.getText() if rid is not None else ""] = propsChanged
+
def _logDiffError(self, title):
strcal1 = str(self.oldcalendar)
@@ -770,7 +788,7 @@
fromfile='Existing Calendar Object',
tofile='New Calendar Object',
))
-
+
logstr = """%s
------ Existing Calendar Data ------
Modified: CalendarServer/trunk/twistedcaldav/scheduling/implicit.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/implicit.py 2012-10-11 15:28:35 UTC (rev 9924)
+++ CalendarServer/trunk/twistedcaldav/scheduling/implicit.py 2012-10-11 15:53:12 UTC (rev 9925)
@@ -26,8 +26,8 @@
from twistedcaldav.directory.principal import DirectoryCalendarPrincipalResource
from twistedcaldav.ical import Property
from twistedcaldav.scheduling import addressmapping
-from twistedcaldav.scheduling.cuaddress import InvalidCalendarUser,\
- LocalCalendarUser, PartitionedCalendarUser, OtherServerCalendarUser,\
+from twistedcaldav.scheduling.cuaddress import InvalidCalendarUser, \
+ LocalCalendarUser, PartitionedCalendarUser, OtherServerCalendarUser, \
normalizeCUAddr
from twistedcaldav.scheduling.icaldiff import iCalDiff
from twistedcaldav.scheduling.itip import iTipGenerator, iTIPRequestStatus
@@ -48,19 +48,20 @@
#
class ImplicitScheduler(object):
-
+
# Return Status codes
- STATUS_OK = 0
+ STATUS_OK = 0
STATUS_ORPHANED_CANCELLED_EVENT = 1
- STATUS_ORPHANED_EVENT = 2
+ STATUS_ORPHANED_EVENT = 2
def __init__(self):
-
+
self.return_status = ImplicitScheduler.STATUS_OK
+
@inlineCallbacks
def testImplicitSchedulingPUT(self, request, resource, resource_uri, calendar, internal_request=False):
-
+
self.request = request
self.resource = resource
self.calendar = calendar
@@ -78,12 +79,12 @@
resource.isScheduleObject = None
is_scheduling_object = (yield self.checkSchedulingObjectResource(resource))
existing_type = "schedule" if is_scheduling_object else "calendar"
-
+
if existing_type == "calendar":
self.action = "create" if new_type == "schedule" else "none"
else:
self.action = "modify" if new_type == "schedule" else "remove"
-
+
# Cannot create new resource with existing UID
if not existing_resource or self.action == "create":
yield self.hasCalendarResourceUIDSomewhereElse(resource, resource_uri, new_type)
@@ -95,7 +96,7 @@
self.return_calendar = calendar
self.calendar = (yield resource.iCalendarForUser(request))
yield self.checkImplicitState()
-
+
# Once we have collected sufficient information from the calendar data, check validity of organizer and attendees
self.checkValidOrganizer()
@@ -119,13 +120,13 @@
(caldav_namespace, "organizer-allowed"),
"Organizer cannot schedule without a master component.",
))
-
returnValue((self.action != "none", new_type == "schedule",))
+
@inlineCallbacks
def testImplicitSchedulingMOVE(self, request, srcresource, srccal, src_uri, destresource, destcal, dest_uri, calendar, internal_request=False):
-
+
self.request = request
self.resource = destresource
self.calendar = calendar
@@ -158,9 +159,10 @@
returnValue((self.action != "none", new_type == "schedule",))
+
@inlineCallbacks
def testImplicitSchedulingCOPY(self, request, srcresource, srccal, src_uri, destresource, destcal, dest_uri, calendar, internal_request=False):
-
+
self.request = request
self.resource = destresource
self.calendar = calendar
@@ -191,9 +193,10 @@
returnValue((self.action != "none", src_is_implicit,))
+
@inlineCallbacks
def testImplicitSchedulingDELETE(self, request, resource, calendar, internal_request=False):
-
+
self.request = request
self.resource = resource
self.calendar = calendar
@@ -207,11 +210,12 @@
returnValue((self.action != "none", False,))
+
def checkValidOrganizer(self):
"""
Make sure the ORGANIZER is allowed to do certain scheduling operations.
"""
-
+
# Check to see whether the organizer principal is enabled for scheduling. If not, do not allow them
# to create new scheduling resources.
if self.action == "create":
@@ -223,9 +227,10 @@
"Organizer cannot schedule",
))
+
@inlineCallbacks
def checkSchedulingObjectResource(self, resource):
-
+
if resource and resource.exists():
implicit = resource.isScheduleObject
if implicit is not None:
@@ -238,12 +243,13 @@
except ValueError:
# We have different ORGANIZERs in the same iCalendar object - this is an error
returnValue(False)
-
+
# Any ORGANIZER => a scheduling object resource
returnValue(organizer is not None)
returnValue(False)
-
+
+
@inlineCallbacks
def checkImplicitState(self):
# Get some useful information from the calendar
@@ -265,6 +271,7 @@
returnValue(self.state is not None)
+
@inlineCallbacks
def doImplicitScheduling(self, do_smart_merge=False):
"""
@@ -275,7 +282,7 @@
@return: a new calendar object modified with scheduling information,
or C{None} if nothing happened or C{int} if some other state occurs
"""
-
+
# Setup some parameters
self.do_smart_merge = do_smart_merge
self.except_attendees = ()
@@ -296,6 +303,7 @@
else:
returnValue(self.return_calendar if hasattr(self, "return_calendar") else self.calendar)
+
@inlineCallbacks
def refreshAllAttendeesExceptSome(self, request, resource, except_attendees=(), only_attendees=None):
"""
@@ -315,7 +323,6 @@
self.changed_rids = None
self.reinvites = None
-
# Get some useful information from the calendar
yield self.extractCalendarData()
self.organizerPrincipal = self.resource.principalForCalendarUserAddress(self.organizer)
@@ -324,14 +331,14 @@
# Originator is the organizer in this case
self.originatorPrincipal = self.organizerPrincipal
self.originator = self.organizer
-
+
# We want to suppress chatty iMIP messages when other attendees reply
self.request.suppressRefresh = False
for attendee in self.calendar.getAllAttendeeProperties():
if attendee.parameterValue("PARTSTAT", "NEEDS-ACTION").upper() == "NEEDS-ACTION":
self.request.suppressRefresh = True
-
+
if hasattr(self.request, "doing_attendee_refresh"):
self.request.doing_attendee_refresh += 1
else:
@@ -348,9 +355,10 @@
self.request.extendedLogItems = {}
self.request.extendedLogItems["itip.refreshes"] = refreshCount
+
@inlineCallbacks
def sendAttendeeReply(self, request, resource, calendar, attendee):
-
+
self.request = request
self.resource = resource
self.calendar = calendar
@@ -360,20 +368,21 @@
self.calendar_owner = None
self.internal_request = True
self.changed_rids = None
-
+
# Get some useful information from the calendar
- yield self.extractCalendarData()
+ yield self.extractCalendarData()
self.originator = self.attendee = attendee.principal.canonicalCalendarUserAddress()
self.attendeePrincipal = attendee.principal
-
+
result = (yield self.scheduleWithOrganizer())
returnValue(result)
+
@inlineCallbacks
def extractCalendarData(self):
-
+
# Get the originator who is the owner of the calendar resource being modified
self.originatorPrincipal = None
self.originator = ""
@@ -401,17 +410,18 @@
(caldav_namespace, "single-organizer"),
"Only one organizer allowed in scheduling object resource",
))
-
+
# Get the ATTENDEEs
self.attendeesByInstance = self.calendar.getAttendeesByInstance(True, onlyScheduleAgentServer=True)
self.instances = set(self.calendar.getComponentInstances())
self.attendees = set()
for attendee, _ignore in self.attendeesByInstance:
self.attendees.add(attendee)
-
+
# Some other useful things
self.uid = self.calendar.resourceUID()
-
+
+
@inlineCallbacks
def hasCalendarResourceUIDSomewhereElse(self, check_resource, check_uri, type):
"""
@@ -437,34 +447,36 @@
"Cannot duplicate scheduling object resource",
))
+
@inlineCallbacks
def isOrganizerScheduling(self):
"""
Test whether this is a scheduling operation by an organizer
"""
-
+
# First must have organizer property
if not self.organizer:
returnValue(False)
-
+
# Organizer must map to a valid principal
self.organizerPrincipal = self.resource.principalForCalendarUserAddress(self.organizer)
self.organizerAddress = (yield addressmapping.mapper.getCalendarUser(self.organizer, self.organizerPrincipal))
if not self.organizerPrincipal:
returnValue(False)
-
+
# Organizer must be the owner of the calendar resource
if str(self.calendar_owner) != self.organizerPrincipal.principalURL():
returnValue(False)
returnValue(True)
+
def isAttendeeScheduling(self):
-
+
# First must have organizer property
if not self.organizer:
return False
-
+
# Check to see whether any attendee is the owner
for attendee in self.attendees:
attendeePrincipal = self.resource.principalForCalendarUserAddress(attendee)
@@ -472,9 +484,10 @@
self.attendee = attendee
self.attendeePrincipal = attendeePrincipal
return True
-
+
return False
+
@inlineCallbacks
def doAccessControl(self, principal, is_organizer):
"""
@@ -486,21 +499,23 @@
@param is_organizer:
@type is_organizer:
"""
-
+
# Find outbox
outboxURL = principal.scheduleOutboxURL()
outbox = (yield self.request.locateResource(outboxURL))
yield outbox.authorize(self.request, (caldavxml.ScheduleSend(),))
+
def makeScheduler(self):
"""
Convenience method which we can override in unit tests to make testing easier.
"""
return CalDAVScheduler(self.request, self.resource)
+
@inlineCallbacks
def doImplicitOrganizer(self):
-
+
# Do access control
if not self.internal_request:
yield self.doAccessControl(self.organizerPrincipal, True)
@@ -510,7 +525,7 @@
self.cancelledAttendees = ()
self.reinvites = None
self.needs_action_rids = None
-
+
self.needs_sequence_change = False
# Check for a delete
@@ -521,7 +536,7 @@
# Cancel all attendees
self.cancelledAttendees = [(attendee, None) for attendee in self.attendees]
-
+
# CANCEL always bumps sequence
self.needs_sequence_change = True
@@ -533,7 +548,7 @@
self.oldAttendeesByInstance = self.oldcalendar.getAttendeesByInstance(True, onlyScheduleAgentServer=True)
self.oldInstances = set(self.oldcalendar.getComponentInstances())
self.coerceAttendeesPartstatOnModify()
-
+
# Don't allow any SEQUENCE to decrease
if self.oldcalendar:
self.calendar.sequenceInSync(self.oldcalendar)
@@ -550,14 +565,14 @@
returnValue(None)
else:
log.debug("Implicit - organizer '%s' is modifying UID: '%s'" % (self.organizer, self.uid))
-
+
for rid in self.needs_action_rids:
comp = self.calendar.overriddenComponent(rid)
-
+
for attendee in comp.getAllAttendeeProperties():
if attendee.hasParameter("PARTSTAT"):
cuaddr = attendee.value()
-
+
if cuaddr in self.organizerPrincipal.calendarUserAddresses():
# If the attendee is the organizer then do not update
# the PARTSTAT to NEEDS-ACTION.
@@ -569,7 +584,7 @@
# Check for removed attendees
if not recurrence_reschedule:
self.findRemovedAttendees()
-
+
# For now we always bump the sequence number on modifications because we cannot track DTSTAMP on
# the Attendee side. But we check the old and the new and only bump if the client did not already do it.
self.needs_sequence_change = self.calendar.needsiTIPSequenceChange(self.oldcalendar)
@@ -577,7 +592,7 @@
elif self.action == "create":
log.debug("Implicit - organizer '%s' is creating UID: '%s'" % (self.organizer, self.uid))
self.coerceAttendeesPartstatOnCreate()
-
+
# Always set RSVP=TRUE for any NEEDS-ACTION
for attendee in self.calendar.getAllAttendeeProperties():
if attendee.parameterValue("PARTSTAT", "NEEDS-ACTION").upper() == "NEEDS-ACTION":
@@ -587,7 +602,7 @@
self.calendar.bumpiTIPInfo(oldcalendar=self.oldcalendar, doSequence=True)
yield self.scheduleWithAttendees()
-
+
# Always clear SCHEDULE-FORCE-SEND from all attendees after scheduling
for attendee in self.calendar.getAllAttendeeProperties():
try:
@@ -595,8 +610,9 @@
except KeyError:
pass
+
def isOrganizerChangeInsignificant(self):
-
+
rids = None
date_changed_rids = None
reinvites = None
@@ -613,7 +629,7 @@
if "ORGANIZER" in props:
checkOrganizerValue = True
rids.add(rid)
-
+
if any([testprop in props for testprop in (
"DTSTART",
"DTEND",
@@ -631,7 +647,7 @@
recurrence_reschedule = True
elif "RRULE" in props:
-
+
# Need to see if the RRULE change is a simple truncation or expansion - i.e. a change to
# COUNT or UNTIL only. If so we don't need to treat this as a complete re-schedule.
@@ -643,23 +659,23 @@
oldrrule = oldrrule[0].value() if len(oldrrule) else None
newrrule = tuple(self.calendar.masterComponent().properties("RRULE"))
newrrule = newrrule[0].value() if len(newrrule) else None
-
+
if newrrule is not None and oldrrule is not None:
-
+
# Normalize the rrules by removing COUNT/UNTIL and then compare
oldrrule = oldrrule.duplicate()
newrrule = newrrule.duplicate()
-
+
oldrrule.setUseUntil(False)
oldrrule.setUntil(None)
oldrrule.setUseCount(False)
oldrrule.setCount(0)
-
+
newrrule.setUseUntil(False)
newrrule.setUntil(None)
newrrule.setUseCount(False)
newrrule.setCount(0)
-
+
# If they are equal we have a simple change - no overall reschedule
if newrrule == oldrrule:
recurrence_reschedule = False
@@ -685,13 +701,14 @@
pass
return no_change, rids, date_changed_rids, reinvites, recurrence_reschedule
-
+
+
def findRemovedAttendees(self):
"""
Look for attendees that have been removed from any instances. Save those off
as users that need to be sent a cancel.
"""
-
+
# Several possibilities for when CANCELs need to be sent:
#
# Remove ATTENDEE property
@@ -700,12 +717,12 @@
# Remove RDATE
# Truncate RRULE
# Change RRULE
-
+
# TODO: the later three will be ignored for now.
mappedOld = set(self.oldAttendeesByInstance)
mappedNew = set(self.attendeesByInstance)
-
+
# Get missing instances
removedInstances = self.oldInstances - self.instances
addedInstances = self.instances - self.oldInstances
@@ -722,18 +739,18 @@
# Now figure out the attendees that need to be sent CANCELs
self.cancelledAttendees = set()
-
+
for item in mappedOld:
if item not in mappedNew:
-
+
# Several possibilities:
#
# 1. removed from master component - always a CANCEL
# 2. removed from overridden component - always a CANCEL
# 3. removed overridden component - only CANCEL if not in master or exdate added
-
+
new_attendee, rid = item
-
+
# 1. & 2.
if rid is None or rid not in removedInstances:
self.cancelledAttendees.add(item)
@@ -757,6 +774,7 @@
if (attendee, rid) not in mappedNew and rid not in oldexdates:
self.cancelledAttendees.add((attendee, rid))
+
def coerceAttendeesPartstatOnCreate(self):
"""
Make sure any attendees handled by the server start off with PARTSTAT=NEEDS-ACTION as
@@ -768,21 +786,22 @@
continue
if attendee.parameterValue("SCHEDULE-AGENT", "SERVER").upper() == "SERVER" and attendee.hasParameter("PARTSTAT"):
attendee.setParameter("PARTSTAT", "NEEDS-ACTION")
-
+
+
def coerceAttendeesPartstatOnModify(self):
"""
Make sure that the organizer does not change attendees' PARTSTAT to anything
other than NEEDS-ACTION for those attendees handled by the server.
"""
-
+
# Get the set of Rids in each calendar
newRids = set(self.calendar.getComponentInstances())
oldRids = set(self.oldcalendar.getComponentInstances())
-
+
# Test/fix ones that are the same
for rid in (newRids & oldRids):
self.compareAttendeePartstats(self.oldcalendar.overriddenComponent(rid), self.calendar.overriddenComponent(rid))
-
+
# Test/fix ones added
for rid in (newRids - oldRids):
# Compare the new one to the old master
@@ -790,8 +809,8 @@
# For removals, we ignore ones that are no longer valid
valid_old_rids = self.calendar.validInstances(oldRids - newRids)
-
- # Test/fix ones removed
+
+ # Test/fix ones removed
for rid in valid_old_rids:
# Compare the old one to the new master
# Note it is hard to recover from this state so raise instead
@@ -800,16 +819,17 @@
self.calendar.overriddenComponent(None),
raiseOnMisMatch=True
)
-
+
+
def compareAttendeePartstats(self, old_component, new_component, raiseOnMisMatch=False):
"""
Compare two components, old and new, and make sure the Organizer has not changed the PARTSTATs
in the new one to anything other than NEEDS-ACTION. If there is a change, undo it.
"""
-
+
old_attendees = dict([(normalizeCUAddr(attendee.value()), attendee) for attendee in old_component.getAllAttendeeProperties()])
new_attendees = dict([(normalizeCUAddr(attendee.value()), attendee) for attendee in new_component.getAllAttendeeProperties()])
-
+
for cuaddr, newattendee in new_attendees.items():
# Don't adjust ORGANIZER's ATTENDEE
if newattendee.value() in self.organizerPrincipal.calendarUserAddresses():
@@ -828,12 +848,13 @@
else:
newattendee.setParameter("PARTSTAT", old_partstat)
+
@inlineCallbacks
def scheduleWithAttendees(self):
-
+
# First process cancelled attendees
total = (yield self.processCancels())
-
+
# Process regular requests next
if self.action in ("create", "modify",):
total += (yield self.processRequests())
@@ -842,9 +863,10 @@
self.request.extendedLogItems = {}
self.request.extendedLogItems["itip.requests"] = total
+
@inlineCallbacks
def processCancels(self):
-
+
# TODO: a better policy here is to aggregate by attendees with the same set of instances
# being cancelled, but for now we will do one scheduling message per attendee.
@@ -852,17 +874,17 @@
aggregated = {}
for attendee, rid in self.cancelledAttendees:
aggregated.setdefault(attendee, []).append(rid)
-
+
count = 0
for attendee, rids in aggregated.iteritems():
-
+
# Don't send message back to the ORGANIZER
if attendee in self.organizerPrincipal.calendarUserAddresses():
continue
# Generate an iTIP CANCEL message for this attendee, cancelling
# each instance or the whole
-
+
if None in rids:
# One big CANCEL will do
itipmsg = iTipGenerator.generateCancel(self.oldcalendar, (attendee,), None, self.action == "remove")
@@ -874,19 +896,20 @@
if itipmsg:
# This is a local CALDAV scheduling operation.
scheduler = self.makeScheduler()
-
+
# Do the PUT processing
log.info("Implicit CANCEL - organizer: '%s' to attendee: '%s', UID: '%s', RIDs: '%s'" % (self.organizer, attendee, self.uid, rids))
response = (yield scheduler.doSchedulingViaPUT(self.originator, (attendee,), itipmsg, internal_request=True))
self.handleSchedulingResponse(response, True)
-
+
count += 1
-
+
returnValue(count)
-
+
+
@inlineCallbacks
def processRequests(self):
-
+
# TODO: a better policy here is to aggregate by attendees with the same set of instances
# being requested, but for now we will do one scheduling message per attendee.
@@ -916,18 +939,19 @@
if itipmsg is not None:
# This is a local CALDAV scheduling operation.
scheduler = self.makeScheduler()
-
+
# Do the PUT processing
log.info("Implicit REQUEST - organizer: '%s' to attendee: '%s', UID: '%s'" % (self.organizer, attendee, self.uid,))
response = (yield scheduler.doSchedulingViaPUT(self.originator, (attendee,), itipmsg, internal_request=True))
self.handleSchedulingResponse(response, True)
-
+
count += 1
-
+
returnValue(count)
+
def handleSchedulingResponse(self, response, is_organizer):
-
+
# Map each recipient in the response to a status code
responses = {}
for item in response.responses:
@@ -935,7 +959,7 @@
recipient = str(item.children[0].children[0])
status = str(item.children[1])
responses[recipient] = status
-
+
# Now apply to each ATTENDEE/ORGANIZER in the original data
self.calendar.setParameterToValueForPropertyWithValue(
"SCHEDULE-STATUS",
@@ -943,6 +967,7 @@
"ATTENDEE" if is_organizer else "ORGANIZER",
recipient)
+
@inlineCallbacks
def doImplicitAttendee(self):
@@ -986,7 +1011,7 @@
))
else:
self.oldcalendar = None
-
+
# Get the ORGANIZER's current copy of the calendar object
yield self.getOrganizersCopy()
if self.organizer_calendar:
@@ -1008,7 +1033,7 @@
if not changeAllowed:
if self.calendar.hasPropertyValueInAllComponents(Property("STATUS", "CANCELLED")):
log.debug("Attendee '%s' is creating CANCELLED event for mismatched UID: '%s' - removing entire event" % (self.attendee, self.uid,))
- self.return_status = ImplicitScheduler.STATUS_ORPHANED_CANCELLED_EVENT
+ self.return_status = ImplicitScheduler.STATUS_ORPHANED_EVENT
returnValue(None)
else:
log.error("Attendee '%s' is not allowed to make an unauthorized change to an organized event: UID:%s" % (self.attendeePrincipal, self.uid,))
@@ -1018,6 +1043,13 @@
"Attendee changes are not allowed",
))
+ # Check that the return calendar actually has any components left - this can happen if a cancelled
+ # component is removed and replaced by another cancelled or invalid one
+ if self.calendar.mainType() is None:
+ log.debug("Attendee '%s' is replacing CANCELLED event: '%s' - removing entire event" % (self.attendee, self.uid,))
+ self.return_status = ImplicitScheduler.STATUS_ORPHANED_EVENT
+ returnValue(None)
+
if not doITipReply:
log.debug("Implicit - attendee '%s' is updating UID: '%s' but change is not significant" % (self.attendee, self.uid))
returnValue(None)
@@ -1078,6 +1110,7 @@
log.debug("Implicit - attendee '%s' is updating UID without server scheduling: '%s'" % (self.attendee, self.uid))
# Nothing else to do
+
@inlineCallbacks
def doImplicitMissingAttendee(self):
@@ -1089,7 +1122,7 @@
# We will allow the attendee to do anything in this case, but we will mark the organizer
# with an schedule-status error and schedule-agent none
log.debug("Missing attendee is allowed to update UID: '%s' with invalid organizer '%s'" % (self.uid, self.organizer))
-
+
# Make sure ORGANIZER is not changed if originally SCHEDULE-AGENT=SERVER
if self.resource.exists():
self.oldcalendar = (yield self.resource.iCalendarForUser(self.request))
@@ -1108,6 +1141,7 @@
self.calendar.setParameterToValueForPropertyWithValue("SCHEDULE-AGENT", "NONE", "ORGANIZER", None)
self.calendar.setParameterToValueForPropertyWithValue("SCHEDULE-STATUS", iTIPRequestStatus.NO_USER_SUPPORT_CODE, "ORGANIZER", None)
+
def checkOrganizerScheduleAgent(self):
is_server = self.calendar.getOrganizerScheduleAgent()
@@ -1122,19 +1156,20 @@
self.calendar.setParameterToValueForPropertyWithValue("SCHEDULE-AGENT", "NONE", "ORGANIZER", None)
self.calendar.setParameterToValueForPropertyWithValue("SCHEDULE-STATUS", iTIPRequestStatus.NO_USER_SUPPORT_CODE, "ORGANIZER", None)
is_server = False
-
+
return is_server
+
@inlineCallbacks
def getOrganizersCopy(self):
"""
Get the Organizer's copy of the event being processed.
-
+
NB it is possible that the Organizer is not hosted on this server
so the result here will be None. In that case we have to trust that
the attendee does the right thing about changing the details in the event.
"""
-
+
self.organizer_calendar = None
calendar_resource, _ignore_name, _ignore_collection, _ignore_uri = (yield getCalendarObjectForPrincipals(self.request, self.organizerPrincipal, self.uid))
if calendar_resource:
@@ -1143,14 +1178,15 @@
# For partitioning where the organizer is on a different node, we will assume that the attendee's copy
# of the event is up to date and "authoritative". So we pretend that is the organizer copy
self.organizer_calendar = self.oldcalendar
-
+
+
def isAttendeeChangeInsignificant(self):
"""
Check whether the change is significant (PARTSTAT) or allowed
(attendee can only change their property, alarms, TRANSP, and
instances. Raise an exception if it is not allowed.
"""
-
+
oldcalendar = self.oldcalendar
if oldcalendar is None:
oldcalendar = self.organizer_calendar
@@ -1158,28 +1194,31 @@
differ = iCalDiff(oldcalendar, self.calendar, self.do_smart_merge)
return differ.attendeeMerge(self.attendee)
+
def scheduleWithOrganizer(self, changedRids=None):
if not hasattr(self.request, "extendedLogItems"):
self.request.extendedLogItems = {}
self.request.extendedLogItems["itip.reply"] = "reply"
-
+
itipmsg = iTipGenerator.generateAttendeeReply(self.calendar, self.attendee, changedRids=changedRids)
# Send scheduling message
return self.sendToOrganizer("REPLY", itipmsg)
+
def scheduleCancelWithOrganizer(self):
if not hasattr(self.request, "extendedLogItems"):
self.request.extendedLogItems = {}
self.request.extendedLogItems["itip.reply"] = "cancel"
-
+
itipmsg = iTipGenerator.generateAttendeeReply(self.calendar, self.attendee, force_decline=True)
# Send scheduling message
return self.sendToOrganizer("CANCEL", itipmsg)
+
def sendToOrganizer(self, action, itipmsg):
# Send scheduling message
@@ -1190,7 +1229,7 @@
# Do the PUT processing
def _gotResponse(response):
self.handleSchedulingResponse(response, False)
-
+
log.info("Implicit %s - attendee: '%s' to organizer: '%s', UID: '%s'" % (action, self.attendee, self.organizer, self.uid,))
d = scheduler.doSchedulingViaPUT(self.originator, (self.organizer,), itipmsg, internal_request=True)
d.addCallback(_gotResponse)
Modified: CalendarServer/trunk/twistedcaldav/scheduling/itip.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/itip.py 2012-10-11 15:28:35 UTC (rev 9924)
+++ CalendarServer/trunk/twistedcaldav/scheduling/itip.py 2012-10-11 15:53:12 UTC (rev 9925)
@@ -30,7 +30,7 @@
from twext.python.log import Logger
from twistedcaldav.config import config
-from twistedcaldav.ical import Property, iCalendarProductID, Component,\
+from twistedcaldav.ical import Property, iCalendarProductID, Component, \
ignoredComponents
from pycalendar.datetime import PyCalendarDateTime
@@ -50,10 +50,10 @@
def processNewRequest(itip_message, recipient=None, creating=False):
"""
Process a METHOD=REQUEST for a brand new calendar object.
-
+
@param itip_message: the iTIP message calendar object to process.
@type itip_message:
-
+
@return: calendar object ready to save
"""
assert itip_message.propertyValue("METHOD") == "REQUEST", "iTIP message must have METHOD:REQUEST"
@@ -65,7 +65,7 @@
if recipient:
iTipProcessing.addTranspForNeedsAction(calendar.subcomponents(), recipient)
-
+
# Check for incoming DECLINED
if creating:
master = calendar.masterComponent()
@@ -81,23 +81,24 @@
calendar.removeComponent(component)
return calendar
-
+
+
@staticmethod
def processRequest(itip_message, calendar, recipient):
"""
Process a METHOD=REQUEST. We need to merge per-attendee properties such as TRANPS, COMPLETED etc
with the data coming from the organizer.
-
+
@param itip_message: the iTIP message calendar object to process.
@type itip_message:
@param calendar: the calendar object to apply the REQUEST to
@type calendar:
-
+
@return: a C{tuple} of:
calendar object ready to save, or C{None} (request should be ignored)
a C{set} of recurrences that changed, or C{None}
"""
-
+
# Check sequencing
if not iTipProcessing.sequenceComparison(itip_message, calendar):
# Ignore out of sequence message
@@ -126,10 +127,10 @@
organizer_schedule_status = None
if itip_message.masterComponent() is not None:
-
+
# Get a new calendar object first
new_calendar = iTipProcessing.processNewRequest(itip_message, recipient)
-
+
# Copy over master alarms, comments
master_component = new_calendar.masterComponent()
for alarm in master_valarms:
@@ -140,16 +141,16 @@
master_component.replaceProperty(transp)
for completed in completeds:
master_component.replaceProperty(completed)
- if organizer_schedule_status:
+ if organizer_schedule_status:
organizer = master_component.getProperty("ORGANIZER")
if organizer:
organizer.setParameter("SCHEDULE-STATUS", organizer_schedule_status)
-
+
# Now try to match recurrences in the new calendar
for component in tuple(new_calendar.subcomponents()):
if component.name() != "VTIMEZONE" and component.getRecurrenceIDUTC() is not None:
iTipProcessing.transferItems(calendar, master_valarms, private_comments, transps, completeds, organizer_schedule_status, component, recipient)
-
+
# Now try to match recurrences from the old calendar
for component in calendar.subcomponents():
if component.name() != "VTIMEZONE" and component.getRecurrenceIDUTC() is not None:
@@ -160,7 +161,7 @@
if new_component:
new_calendar.addComponent(new_component)
iTipProcessing.transferItems(calendar, master_valarms, private_comments, transps, completeds, organizer_schedule_status, new_component, recipient)
-
+
# Replace the entire object
return new_calendar, rids
@@ -185,24 +186,25 @@
# Write back the modified object
return calendar, rids
+
@staticmethod
def processCancel(itip_message, calendar, autoprocessing=False):
"""
Process a METHOD=CANCEL.
-
+
TODO: Yes, I am going to ignore RANGE= on RECURRENCE-ID for now...
-
+
@param itip_message: the iTIP message calendar object to process.
@type itip_message:
@param calendar: the calendar object to apply the CANCEL to
@type calendar:
-
+
@return: C{tuple} of:
C{bool} : C{True} if processed, C{False} if scheduling message should be ignored
C{bool} : C{True} if calendar object should be deleted, C{False} otherwise
C{set} : set of Recurrence-IDs for cancelled instances, or C{None} if all cancelled
"""
-
+
assert itip_message.propertyValue("METHOD") == "CANCEL", "iTIP message must have METHOD:CANCEL"
assert itip_message.resourceUID() == calendar.resourceUID(), "UIDs must be the same to process iTIP message"
@@ -235,32 +237,32 @@
for component in itip_message.subcomponents():
if component.name() == "VTIMEZONE":
continue
-
+
# Extract RECURRENCE-ID value from component
rid = component.getRecurrenceIDUTC()
rids.add(rid)
-
+
# Get the one that matches in the calendar
overridden = calendar.overriddenComponent(rid)
-
+
if overridden:
# We are cancelling an overridden component.
if autoprocessing:
# Exclude the cancelled instance
exdates.append(component.getRecurrenceIDUTC())
-
+
# Remove the existing component.
calendar.removeComponent(overridden)
else:
# Existing component is cancelled.
overridden.replaceProperty(Property("STATUS", "CANCELLED"))
newseq = component.propertyValue("SEQUENCE")
- overridden.replacePropertyInAllComponents(Property("SEQUENCE", newseq))
+ overridden.replaceProperty(Property("SEQUENCE", newseq))
elif calendar_master:
# We are trying to CANCEL a non-overridden instance.
-
+
if autoprocessing:
# Exclude the cancelled instance
exdates.append(component.getRecurrenceIDUTC())
@@ -284,26 +286,27 @@
return True, True, None
else:
return True, False, rids
-
+
+
@staticmethod
def processReply(itip_message, calendar):
"""
Process a METHOD=REPLY.
-
+
TODO: Yes, I am going to ignore RANGE= on RECURRENCE-ID for now...
TODO: We have no way to track SEQUENCE/DTSTAMP on a per-attendee basis to correctly serialize out-of-order
replies.
-
+
@param itip_message: the iTIP message calendar object to process.
@type itip_message:
@param calendar: the calendar object to apply the REPLY to
@type calendar:
-
+
@return: a C{tuple} of:
C{True} if processed, C{False} if scheduling message should be ignored
C{tuple} of change info
"""
-
+
assert itip_message.propertyValue("METHOD") == "REPLY", "iTIP message must have METHOD:REPLY"
assert itip_message.resourceUID() == calendar.resourceUID(), "UIDs must be the same to process iTIP message"
@@ -326,7 +329,7 @@
# Now do all overridden ones (sort by RECURRENCE-ID)
sortedComponents = []
for itip_component in itip_message.subcomponents():
-
+
# Make sure we have an appropriate component
if itip_component.name() == "VTIMEZONE":
continue
@@ -334,9 +337,9 @@
if rid is None:
continue
sortedComponents.append((rid, itip_component,))
-
- sortedComponents.sort(key=lambda x:x[0])
-
+
+ sortedComponents.sort(key=lambda x: x[0])
+
for rid, itip_component in sortedComponents:
# Find matching component in organizer's copy
match_component = calendar.overriddenComponent(rid)
@@ -366,6 +369,7 @@
log.error("ATTENDEE property in a REPLY must be the same in all components\n%s" % (str(itip_message),))
return False, None
+
@staticmethod
def updateAttendeeData(from_component, to_component):
"""
@@ -377,7 +381,7 @@
@param to_component:
@type to_component:
"""
-
+
# Track what changed
partstat_changed = False
private_comment_changed = False
@@ -397,7 +401,7 @@
attendee = attendees[0]
partstat = attendee.parameterValue("PARTSTAT", "NEEDS-ACTION")
-
+
# Now find matching ATTENDEE in to_component
existing_attendee = to_component.getAttendeeProperty((attendee.value(),))
if existing_attendee:
@@ -405,7 +409,7 @@
existing_attendee.setParameter("PARTSTAT", partstat)
existing_attendee.setParameter("SCHEDULE-STATUS", reqstatus)
partstat_changed = (oldpartstat != partstat)
-
+
# Always delete RSVP on PARTSTAT change
if partstat_changed:
try:
@@ -418,7 +422,7 @@
# Look for X-CALENDARSERVER-PRIVATE-COMMENT property in iTIP component (State 1 in spec)
attendee_comment = tuple(from_component.properties("X-CALENDARSERVER-PRIVATE-COMMENT"))
attendee_comment = attendee_comment[0] if len(attendee_comment) else None
-
+
# Look for matching X-CALENDARSERVER-ATTENDEE-COMMENT property in existing data (State 2 in spec)
private_comments = tuple(to_component.properties("X-CALENDARSERVER-ATTENDEE-COMMENT"))
for comment in private_comments:
@@ -431,64 +435,65 @@
else:
attendee_comment = None
private_comment = None
-
+
# Now do update logic
if attendee_comment is None and private_comment is None:
# Nothing to do
pass
-
+
elif attendee_comment is None and private_comment is not None:
# Remove all property parameters
private_comment.removeAllParameters()
-
+
# Add default parameters
private_comment.setParameter("X-CALENDARSERVER-ATTENDEE-REF", attendee.value())
private_comment.setParameter("X-CALENDARSERVER-DTSTAMP", PyCalendarDateTime.getNowUTC().getText())
-
+
# Set value empty
private_comment.setValue("")
-
+
private_comment_changed = True
-
+
elif attendee_comment is not None and private_comment is None:
-
+
# Add new property
private_comment = Property(
"X-CALENDARSERVER-ATTENDEE-COMMENT",
attendee_comment.value(),
- params = {
+ params={
"X-CALENDARSERVER-ATTENDEE-REF": attendee.value(),
- "X-CALENDARSERVER-DTSTAMP": PyCalendarDateTime.getNowUTC().getText(),
+ "X-CALENDARSERVER-DTSTAMP": PyCalendarDateTime.getNowUTC().getText(),
}
)
to_component.addProperty(private_comment)
-
+
private_comment_changed = True
-
+
else:
# Only change if different
if private_comment.value() != attendee_comment.value():
# Remove all property parameters
private_comment.removeAllParameters()
-
+
# Add default parameters
private_comment.setParameter("X-CALENDARSERVER-ATTENDEE-REF", attendee.value())
private_comment.setParameter("X-CALENDARSERVER-DTSTAMP", PyCalendarDateTime.getNowUTC().getText())
-
+
# Set new value
private_comment.setValue(attendee_comment.value())
-
+
private_comment_changed = True
return attendee.value(), partstat_changed, private_comment_changed
+
@staticmethod
def transferItems(from_calendar, master_valarms, private_comments, transps, completeds, organizer_schedule_status, to_component, recipient, remove_matched=False):
"""
Transfer properties from a calendar to a component by first trying to match the component in the original calendar and
use the properties from that, or use the values provided as arguments (which have been derived from the original calendar's
master component).
-
+
@return: C{True} if an EXDATE match occurred requiring the incoming component to be removed.
"""
@@ -505,7 +510,7 @@
organizer = matched.getProperty("ORGANIZER")
organizer_schedule_status = organizer.parameterValue("SCHEDULE-STATUS", None) if organizer else None
- if organizer_schedule_status:
+ if organizer_schedule_status:
organizer = to_component.getProperty("ORGANIZER")
if organizer:
organizer.setParameter("SCHEDULE-STATUS", organizer_schedule_status)
@@ -513,7 +518,7 @@
# Remove the old one
if remove_matched:
from_calendar.removeComponent(matched)
-
+
# Check for incoming DECLINED
attendee = to_component.getAttendeeProperty((recipient,))
if attendee and attendee.parameterValue("PARTSTAT", "NEEDS-ACTION") == "DECLINED":
@@ -526,20 +531,21 @@
attendee = to_component.getAttendeeProperty((recipient,))
if attendee and attendee.parameterValue("PARTSTAT", "NEEDS-ACTION") == "DECLINED":
return True
-
+
# It is a new override - copy any valarms on the existing master component
# into the new one.
[to_component.addComponent(alarm) for alarm in master_valarms]
[to_component.addProperty(comment) for comment in private_comments]
[to_component.replaceProperty(transp) for transp in transps]
[to_component.replaceProperty(completed) for completed in completeds]
- if organizer_schedule_status:
+ if organizer_schedule_status:
organizer = to_component.getProperty("ORGANIZER")
if organizer:
organizer.setParameter("SCHEDULE-STATUS", organizer_schedule_status)
-
+
return False
-
+
+
@staticmethod
def addTranspForNeedsAction(components, recipient):
# For each component where the ATTENDEE property of the recipient has PARTSTAT
@@ -551,20 +557,21 @@
if attendee and attendee.parameterValue("PARTSTAT", "NEEDS-ACTION") == "NEEDS-ACTION":
component.replaceProperty(Property("TRANSP", "TRANSPARENT"))
+
@staticmethod
def sequenceComparison(itip, calendar):
"""
Do appropriate itip message sequencing based by comparison with existing calendar data.
-
+
@return: C{True} if the itip message is new and should be processed, C{False}
if no processing is needed
@rtype: C{bool}
"""
-
+
# Master component comparison trumps all else
itip_master = itip.masterComponent()
cal_master = calendar.masterComponent()
-
+
# If master component exists, compare all in iTIP and update if any are new
if cal_master:
for itip_component in itip.subcomponents():
@@ -573,7 +580,7 @@
cal_component = calendar.overriddenComponent(itip_component.getRecurrenceIDUTC())
if cal_component is None:
cal_component = cal_master
-
+
# TODO: No DTSTAMP comparison because we do not track DTSTAMPs
# Treat components the same as meaning so an update - in theory no harm in doing that
if Component.compareComponentsForITIP(itip_component, cal_component, use_dtstamp=False) >= 0:
@@ -582,7 +589,7 @@
return False
elif itip_master:
-
+
# Do comparison of each appropriate component if any one is new, process the itip
for cal_component in calendar.subcomponents():
if cal_component.name() in ignoredComponents:
@@ -595,14 +602,14 @@
# Treat components the same as meaning so an update - in theory no harm in doing that
if Component.compareComponentsForITIP(itip_component, cal_component, use_dtstamp=False) >= 0:
return True
-
+
return False
-
+
else:
# Do comparison of each matching component if any one is new, process the entire itip.
# There is a race condition here, similar to REPLY, where we could reinstate an instance
# that has been removed. Not much we can do about it without additional tracking.
-
+
cal_rids = set()
for cal_component in calendar.subcomponents():
if cal_component.name() in ignoredComponents:
@@ -613,7 +620,7 @@
if itip_component.name() in ignoredComponents:
continue
itip_rids.add(itip_component.getRecurrenceIDUTC())
-
+
# Compare ones that match
for rid in cal_rids & itip_rids:
cal_component = calendar.overriddenComponent(rid)
@@ -623,24 +630,26 @@
# Treat components the same as meaning so an update - in theory no harm in doing that
if Component.compareComponentsForITIP(itip_component, cal_component, use_dtstamp=False) >= 0:
return True
-
+
# If there are others in one set and not the other - always process, else no process
return len(cal_rids ^ itip_rids) > 0
-
+
+
+
class iTipGenerator(object):
"""
This assumes that DTSTAMP and SEQUENCE are already at their new values in the original calendar
data passed in to each generateXXX() call.
"""
-
+
@staticmethod
def generateCancel(original, attendees, instances=None, full_cancel=False):
"""
This assumes that SEQUENCE is not already at its new value in the original calendar data. This
is because the component passed in is the one that originally contained the attendee that is
- being removed.
+ being removed.
"""
-
+
itip = Component("VCALENDAR")
itip.addProperty(Property("VERSION", "2.0"))
itip.addProperty(Property("PRODID", iCalendarProductID))
@@ -652,7 +661,7 @@
tzids = set()
added = False
for instance_rid in instances:
-
+
# Create a new component matching the type of the original
comp = Component(original.mainType())
@@ -664,7 +673,7 @@
instance = original.overriddenComponent(instance_rid)
if instance is None:
instance = original.deriveInstance(instance_rid)
-
+
# If the instance to be cancelled did not exist in the original, then
# do nothing
if instance is None:
@@ -679,11 +688,11 @@
comp.addProperty(instance.getOrganizerProperty())
if instance_rid:
comp.addProperty(Property("RECURRENCE-ID", instance_rid.duplicate().adjustToUTC()))
-
+
def addProperties(propname):
for icalproperty in instance.properties(propname):
comp.addProperty(icalproperty)
-
+
addProperties("SUMMARY")
addProperties("DTSTART")
addProperties("DTEND")
@@ -706,7 +715,7 @@
itip.addComponent(comp)
added = True
-
+
if added:
# Now include any referenced tzids
for comp in original.subcomponents():
@@ -714,37 +723,39 @@
tzid = comp.propertyValue("TZID")
if tzid in tzids:
itip.addComponent(comp)
-
+
# Strip out unwanted bits
iTipGenerator.prepareSchedulingMessage(itip)
-
+
return itip
else:
return None
+
@staticmethod
def generateAttendeeRequest(original, attendees, filter_rids):
"""
This assumes that SEQUENCE is already at its new value in the original calendar data.
"""
-
+
# Start with a copy of the original as we may have to modify bits of it
itip = original.duplicate()
itip.replaceProperty(Property("PRODID", iCalendarProductID))
itip.addProperty(Property("METHOD", "REQUEST"))
-
+
# Now filter out components that do not contain every attendee
itip.attendeesView(attendees, onlyScheduleAgentServer=True)
-
+
# Now filter out components except the ones specified
if itip.filterComponents(filter_rids):
# Strip out unwanted bits
iTipGenerator.prepareSchedulingMessage(itip)
return itip
-
+
else:
return None
+
@staticmethod
def generateAttendeeReply(original, attendee, changedRids=None, force_decline=False):
@@ -792,22 +803,23 @@
"LOCATION",
"DESCRIPTION",
))
-
+
# Now set each ATTENDEE's PARTSTAT to DECLINED
if force_decline:
attendeeProps = itip.getAttendeeProperties((attendee,))
assert attendeeProps, "Must have some matching ATTENDEEs"
for attendeeProp in attendeeProps:
attendeeProp.setParameter("PARTSTAT", "DECLINED")
-
+
# Add REQUEST-STATUS to each top-level component
- itip.addPropertyToAllComponents(Property("REQUEST-STATUS", ["2.0", "Success",]))
+ itip.addPropertyToAllComponents(Property("REQUEST-STATUS", ["2.0", "Success", ]))
# Strip out unwanted bits
iTipGenerator.prepareSchedulingMessage(itip, reply=True)
return itip
+
@staticmethod
def prepareSchedulingMessage(itip, reply=False):
"""
@@ -831,40 +843,42 @@
# Attendee properties that need to go to the Organizer
keep_properties = ("X-CALENDARSERVER-PRIVATE-COMMENT",)
itip.removeXProperties(keep_properties=keep_properties)
-
+
# Property Parameters
itip.removePropertyParameters("ATTENDEE", ("SCHEDULE-AGENT", "SCHEDULE-STATUS", "SCHEDULE-FORCE-SEND",))
itip.removePropertyParameters("ORGANIZER", ("SCHEDULE-AGENT", "SCHEDULE-STATUS", "SCHEDULE-FORCE-SEND",))
+
+
class iTIPRequestStatus(object):
"""
String constants for various iTIP status codes we use.
"""
-
- MESSAGE_PENDING_CODE = "1.0"
- MESSAGE_SENT_CODE = "1.1"
- MESSAGE_DELIVERED_CODE = "1.2"
- SUCCESS_CODE = "2.0"
+ MESSAGE_PENDING_CODE = "1.0"
+ MESSAGE_SENT_CODE = "1.1"
+ MESSAGE_DELIVERED_CODE = "1.2"
- INVALID_CALENDAR_USER_CODE = "3.7"
- NO_AUTHORITY_CODE = "3.8"
+ SUCCESS_CODE = "2.0"
- BAD_REQUEST_CODE = "5.0"
- SERVICE_UNAVAILABLE_CODE = "5.1"
- INVALID_SERVICE_CODE = "5.2"
- NO_USER_SUPPORT_CODE = "5.3"
+ INVALID_CALENDAR_USER_CODE = "3.7"
+ NO_AUTHORITY_CODE = "3.8"
- MESSAGE_PENDING = MESSAGE_PENDING_CODE + ";Scheduling message send is pending"
- MESSAGE_SENT = MESSAGE_SENT_CODE + ";Scheduling message has been sent"
- MESSAGE_DELIVERED = MESSAGE_DELIVERED_CODE + ";Scheduling message has been delivered"
-
- SUCCESS = SUCCESS_CODE + ";Success"
+ BAD_REQUEST_CODE = "5.0"
+ SERVICE_UNAVAILABLE_CODE = "5.1"
+ INVALID_SERVICE_CODE = "5.2"
+ NO_USER_SUPPORT_CODE = "5.3"
- INVALID_CALENDAR_USER = INVALID_CALENDAR_USER_CODE + ";Invalid Calendar User"
- NO_AUTHORITY = NO_AUTHORITY_CODE + ";No authority"
+ MESSAGE_PENDING = MESSAGE_PENDING_CODE + ";Scheduling message send is pending"
+ MESSAGE_SENT = MESSAGE_SENT_CODE + ";Scheduling message has been sent"
+ MESSAGE_DELIVERED = MESSAGE_DELIVERED_CODE + ";Scheduling message has been delivered"
- BAD_REQUEST = BAD_REQUEST_CODE + ";Service cannot handle request"
- SERVICE_UNAVAILABLE = SERVICE_UNAVAILABLE_CODE + ";Service unavailable"
- INVALID_SERVICE = INVALID_SERVICE_CODE + ";Invalid calendar service"
- NO_USER_SUPPORT = NO_USER_SUPPORT_CODE + ";No scheduling support for user"
+ SUCCESS = SUCCESS_CODE + ";Success"
+
+ INVALID_CALENDAR_USER = INVALID_CALENDAR_USER_CODE + ";Invalid Calendar User"
+ NO_AUTHORITY = NO_AUTHORITY_CODE + ";No authority"
+
+ BAD_REQUEST = BAD_REQUEST_CODE + ";Service cannot handle request"
+ SERVICE_UNAVAILABLE = SERVICE_UNAVAILABLE_CODE + ";Service unavailable"
+ INVALID_SERVICE = INVALID_SERVICE_CODE + ";Invalid calendar service"
+ NO_USER_SUPPORT = NO_USER_SUPPORT_CODE + ";No scheduling support for user"
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20121011/44b16fb1/attachment-0001.html>
More information about the calendarserver-changes
mailing list