[CalendarServer-changes] [10316] CalendarServer/trunk/twistedcaldav
source_changes at macosforge.org
source_changes at macosforge.org
Tue Jan 22 17:44:10 PST 2013
Revision: 10316
http://trac.calendarserver.org//changeset/10316
Author: cdaboo at apple.com
Date: 2013-01-22 17:44:10 -0800 (Tue, 22 Jan 2013)
Log Message:
-----------
Handle case where a cancel occurs when a recurrence pattern/dtstart is changed. This also requires relaxing
the invalid overridden instance behavior to not consider cancelled components as invalid.
Modified Paths:
--------------
CalendarServer/trunk/twistedcaldav/instance.py
CalendarServer/trunk/twistedcaldav/scheduling/implicit.py
Modified: CalendarServer/trunk/twistedcaldav/instance.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/instance.py 2013-01-23 01:42:33 UTC (rev 10315)
+++ CalendarServer/trunk/twistedcaldav/instance.py 2013-01-23 01:44:10 UTC (rev 10316)
@@ -27,41 +27,50 @@
from pycalendar.timezone import PyCalendarTimezone
class TooManyInstancesError(Exception):
+
def __init__(self):
Exception.__init__(self)
self.max_allowed = config.MaxAllowedInstances
+
def __repr__(self):
return "<%s max:%s>" % (self.__class__.__name__, self.max_allowed)
+
+
class InvalidOverriddenInstanceError(Exception):
+
def __init__(self, rid):
Exception.__init__(self)
self.rid = rid
+
def __repr__(self):
return "<%s invalid:%s>" % (self.__class__.__name__, self.rid)
+
+
class Instance(object):
-
- def __init__(self, component, start = None, end = None, rid = None, overridden = False, future = False):
+
+ def __init__(self, component, start=None, end=None, rid=None, overridden=False, future=False):
self.component = component
self.start = component.getStartDateUTC() if start is None else start
self.end = component.getEndDateUTC() if end is None else end
self.rid = self.start if rid is None else rid
self.overridden = overridden
self.future = future
-
+
+
def getAlarmTriggers(self):
"""
Get the set of alarm triggers for this instance.
@return: a set containing the UTC datetime's of each trigger in each alarm
"""
triggers = set()
-
+
for alarm in [x for x in self.component.subcomponents() if x.name() == "VALARM"]:
- (trigger, related, repeat, duration) = alarm.getTriggerDetails()
-
+ (trigger, related, repeat, duration) = alarm.getTriggerDetails()
+
# Handle relative vs absolute triggers
if isinstance(trigger, PyCalendarDateTime):
# Absolute trigger
@@ -70,36 +79,42 @@
# Relative trigger
start = (self.start if related else self.end) + trigger
triggers.add(start)
-
+
# Handle repeats
if repeat > 0:
tstart = start.duplicate()
- for _ignore in xrange(1, repeat+1):
+ for _ignore in xrange(1, repeat + 1):
tstart += duration
triggers.add(tstart)
-
+
return triggers
-
+
+
def isMasterInstance(self):
return not self.overridden and self.start == self.component.getStartDateUTC()
+
+
class InstanceList(object):
-
+
def __init__(self, ignoreInvalidInstances=False, normalizeFunction=normalizeForIndex):
self.instances = {}
self.limit = None
self.lowerLimit = None
self.ignoreInvalidInstances = ignoreInvalidInstances
self.normalizeFunction = normalizeFunction
-
+
+
def __iter__(self):
# Return keys in sorted order via iterator
for i in sorted(self.instances.keys()):
yield i
+
def __getitem__(self, key):
return self.instances[key]
+
def expandTimeRanges(self, componentSet, limit, lowerLimit=None):
"""
Expand the set of recurrence instances up to the specified date limit.
@@ -111,7 +126,7 @@
and type, forming a proper recurring set.
@param limit: L{PyCalendarDateTime} value representing the end of the expansion.
"""
-
+
# Look at each component type
got_master = False
overrides = []
@@ -142,7 +157,7 @@
# AVAILABLE components are just like VEVENT components
self._addMasterEventComponent(component, lowerLimit, limit)
got_master = True
-
+
for component in overrides:
if component.name() == "VEVENT":
self._addOverrideEventComponent(component, lowerLimit, limit, got_master)
@@ -155,6 +170,7 @@
# AVAILABLE components are just like VEVENT components
self._addOverrideEventComponent(component, lowerLimit, limit, got_master)
+
def addInstance(self, instance):
"""
Add the supplied instance to the map.
@@ -162,11 +178,12 @@
"""
self.instances[str(instance.rid)] = instance
-
+
# Check for too many instances
if config.MaxAllowedInstances and len(self.instances) > config.MaxAllowedInstances:
raise TooManyInstancesError()
+
def _getMasterEventDetails(self, component):
"""
Logic here comes from RFC4791 Section 9.9
@@ -189,9 +206,10 @@
end = start + duration
else:
duration = differenceDateTime(start, end)
-
+
return (rulestart, start, end, duration,)
+
def _addMasterEventComponent(self, component, lowerLimit, upperlimit):
"""
Add the specified master VEVENT Component to the instance list, expanding it
@@ -199,7 +217,7 @@
@param component: the Component to expand
@param limit: the end L{PyCalendarDateTime} for expansion
"""
-
+
details = self._getMasterEventDetails(component)
if details is None:
return
@@ -207,16 +225,17 @@
self._addMasterComponent(component, lowerLimit, upperlimit, rulestart, start, end, duration)
+
def _addOverrideEventComponent(self, component, lowerLimit, upperlimit, got_master):
"""
- Add the specified overridden VEVENT Component to the instance list, replacing
+ Add the specified overridden VEVENT Component to the instance list, replacing
the one generated by the master component.
@param component: the overridden Component.
@param got_master: whether a master component has already been expanded.
"""
-
+
#TODO: This does not take into account THISANDPRIOR - only THISANDFUTURE
-
+
details = self._getMasterEventDetails(component)
if details is None:
return
@@ -224,6 +243,7 @@
self._addOverrideComponent(component, lowerLimit, upperlimit, start, end, got_master)
+
def _getMasterToDoDetails(self, component):
"""
Logic here comes from RFC4791 Section 9.9
@@ -243,12 +263,12 @@
end = dtdue
else:
end = dtstart
-
+
# DUE case
elif dtdue is not None:
rulestart = component.propertyValue("DUE")
start = end = dtdue
-
+
# Fall back to COMPLETED or CREATED - cannot be recurring
else:
rulestart = None
@@ -269,6 +289,7 @@
return (rulestart, start, end, duration,)
+
def _addMasterToDoComponent(self, component, lowerLimit, upperlimit):
"""
Add the specified master VTODO Component to the instance list, expanding it
@@ -283,16 +304,17 @@
self._addMasterComponent(component, lowerLimit, upperlimit, rulestart, start, end, duration)
+
def _addOverrideToDoComponent(self, component, lowerLimit, upperlimit, got_master):
"""
- Add the specified overridden VTODO Component to the instance list, replacing
+ Add the specified overridden VTODO Component to the instance list, replacing
the one generated by the master component.
@param component: the overridden Component.
@param got_master: whether a master component has already been expanded.
"""
-
+
#TODO: This does not take into account THISANDPRIOR - only THISANDFUTURE
-
+
details = self._getMasterToDoDetails(component)
if details is None:
return
@@ -300,8 +322,9 @@
self._addOverrideComponent(component, lowerLimit, upperlimit, start, end, got_master)
+
def _addMasterComponent(self, component, lowerLimit, upperlimit, rulestart, start, end, duration):
-
+
rrules = component.getRecurrenceSet()
if rrules is not None and rulestart is not None:
# Do recurrence set expansion
@@ -310,7 +333,7 @@
# than the master DTSTART, and if we exclude those, the associated
# overridden instances will cause an InvalidOverriddenInstance.
limited = rrules.expand(rulestart,
- PyCalendarPeriod(PyCalendarDateTime(1900,1,1), upperlimit), expanded)
+ PyCalendarPeriod(PyCalendarDateTime(1900, 1, 1), upperlimit), expanded)
for startDate in expanded:
startDate = self.normalizeFunction(startDate)
endDate = startDate + duration
@@ -331,13 +354,16 @@
self.lowerLimit = lowerLimit
else:
self.limit = upperlimit
-
+
+ self.master_cancelled = component.propertyValue("STATUS") == "CANCELLED"
+
+
def _addOverrideComponent(self, component, lowerLimit, upperlimit, start, end, got_master):
# Get the recurrence override info
rid = component.getRecurrenceIDUTC()
range = component.getRange()
-
+
# Now add this instance, effectively overriding the one with the matching R-ID
start = self.normalizeFunction(start)
end = self.normalizeFunction(end)
@@ -350,47 +376,52 @@
return
# Make sure override RECURRENCE-ID is a valid instance of the master
+ cancelled = component.propertyValue("STATUS") == "CANCELLED"
if got_master:
if str(rid) not in self.instances and rid < upperlimit and (lowerLimit is None or rid >= lowerLimit):
- if self.ignoreInvalidInstances:
+ if self.master_cancelled or cancelled:
+ # Ignore invalid overrides when either the master or override is cancelled
+ pass
+ elif self.ignoreInvalidInstances:
return
else:
raise InvalidOverriddenInstanceError(str(rid))
-
+
self.addInstance(Instance(component, start, end, rid, True, range))
-
+
# Handle THISANDFUTURE if present
if range:
# Iterate over all the instances after this one, replacing those
# with a version based on this override component
-
+
# We need to account for a time shift in the overridden component by
# applying that shift to the future instances as well
timeShift = (start != rid)
if timeShift:
offsetTime = start - rid
newDuration = end - start
-
+
# First get sorted instance keys greater than the current components R-ID
for key in sorted(x for x in self.instances.keys() if x > str(rid)):
oldinstance = self.instances[key]
-
+
# Do not override instance that is already overridden
if oldinstance.overridden:
continue
-
+
# Determine the start/end of the new instance
originalStart = oldinstance.rid
start = oldinstance.start
end = oldinstance.end
-
+
if timeShift:
start += offsetTime
end = start + newDuration
-
+
# Now replacing existing entry with the new one
self.addInstance(Instance(component, start, end, originalStart, False, False))
+
def _addFreeBusyComponent(self, component, lowerLimit, upperlimit):
"""
Add the specified master VFREEBUSY Component to the instance list, expanding it
@@ -402,7 +433,7 @@
start = component.getStartDateUTC()
end = component.getEndDateUTC()
if end is None and start is not None:
- raise ValueError("VFREEBUSY component must have both DTSTART and DTEND: %r" % (component, ))
+ raise ValueError("VFREEBUSY component must have both DTSTART and DTEND: %r" % (component,))
# If the free busy is beyond the end of the range we want, ignore it
if start is not None and start >= upperlimit:
@@ -410,7 +441,7 @@
# If the free busy is before the start of the range we want, ignore it
if lowerLimit is not None and end is not None and end < lowerLimit:
- return
+ return
# Now look at each FREEBUSY property
for fb in component.properties("FREEBUSY"):
@@ -425,6 +456,7 @@
end = self.normalizeFunction(period.getEnd())
self.addInstance(Instance(component, start, end))
+
def _addAvailabilityComponent(self, component, lowerLimit, upperlimit):
"""
Add the specified master VAVAILABILITY Component to the instance list, expanding it
@@ -447,7 +479,7 @@
end = component.getEndDateUTC()
if lowerLimit is not None and end is not None and end < lowerLimit:
# If the availability is before the start of the range we want, ignore it
- return
+ return
if end is None:
end = PyCalendarDateTime(2100, 1, 1, 0, 0, 0, tzid=PyCalendarTimezone(utc=True))
end = self.normalizeFunction(end)
Modified: CalendarServer/trunk/twistedcaldav/scheduling/implicit.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/implicit.py 2013-01-23 01:42:33 UTC (rev 10315)
+++ CalendarServer/trunk/twistedcaldav/scheduling/implicit.py 2013-01-23 01:44:10 UTC (rev 10316)
@@ -613,6 +613,8 @@
# Check for removed attendees
if not recurrence_reschedule:
self.findRemovedAttendees()
+ else:
+ self.findRemovedAttendeesOnRecurrenceChange()
# 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.
@@ -736,6 +738,10 @@
"""
Look for attendees that have been removed from any instances. Save those off
as users that need to be sent a cancel.
+
+ This method does not handle a full recurrence change (one where the RRULE pattern
+ changes or the associated DTSTART changes). For the full change we will have another
+ method to handle that.
"""
# Several possibilities for when CANCELs need to be sent:
@@ -795,15 +801,39 @@
# as the set of attendees in the override may be different from the master set, but the override
# will have been accounted for by the previous attendee/instance logic.
if exdate not in removedInstances:
- self.cancelledAttendees.add((attendee, exdate))
+ self.cancelledAttendees.add((attendee, exdate,))
# For overridden instances added, check whether any attendees were removed from the master
for attendee, _ignore in master_attendees:
for rid in addedInstances:
if (attendee, rid) not in mappedNew and rid not in oldexdates:
- self.cancelledAttendees.add((attendee, rid))
+ self.cancelledAttendees.add((attendee, rid,))
+ def findRemovedAttendeesOnRecurrenceChange(self):
+ """
+ Look for attendees that have been removed during a change to the overall recurrence.
+
+ This is a special case to try and minimize the number of cancels sent to just those
+ attendees actually removed. The basic policy is this:
+
+ 1) If an attendee is present in the master component of the new event, they never
+ receive a CANCEL as they will always receive a REQUEST with the entire new event
+ data. i.e., they will see an event "replacement" rather than a cancel+new request.
+
+ 2) For all attendees in the old event, not in the new master, send a cancel of
+ the master or each override they appear in. That happens even if they appear in an
+ override in the new calendar, since in all likelihood there is no guaranteed exact
+ mapping between old and new instances.
+ """
+
+ self.cancelledAttendees = set()
+ new_master_attendees = set([attendee for attendee, _ignore in self.calendar.masterComponent().getAttendeesByInstance(onlyScheduleAgentServer=True)])
+ for attendee, rid in self.oldAttendeesByInstance:
+ if attendee not in new_master_attendees:
+ self.cancelledAttendees.add((attendee, rid,))
+
+
def coerceAttendeesPartstatOnCreate(self):
"""
Make sure any attendees handled by the server start off with PARTSTAT=NEEDS-ACTION as
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20130122/a0905912/attachment-0001.html>
More information about the calendarserver-changes
mailing list