[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