[CalendarServer-changes] [3672] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Sun Feb 15 17:17:42 PST 2009


Revision: 3672
          http://trac.macosforge.org/projects/calendarserver/changeset/3672
Author:   cdaboo at apple.com
Date:     2009-02-15 17:17:37 -0800 (Sun, 15 Feb 2009)
Log Message:
-----------
Use STATUS:CANCELLED for attendee events that get cancelled.

Modified Paths:
--------------
    CalendarServer/trunk/run
    CalendarServer/trunk/twistedcaldav/ical.py
    CalendarServer/trunk/twistedcaldav/scheduling/icaldiff.py
    CalendarServer/trunk/twistedcaldav/scheduling/implicit.py
    CalendarServer/trunk/twistedcaldav/scheduling/itip.py
    CalendarServer/trunk/twistedcaldav/scheduling/processing.py

Modified: CalendarServer/trunk/run
===================================================================
--- CalendarServer/trunk/run	2009-02-15 19:10:03 UTC (rev 3671)
+++ CalendarServer/trunk/run	2009-02-16 01:17:37 UTC (rev 3672)
@@ -692,7 +692,7 @@
 
 caldavtester="${top}/CalDAVTester";
 
-svn_get "CalDAVTester" "${caldavtester}" "${svn_uri_base}/CalDAVTester/trunk" 3653;
+svn_get "CalDAVTester" "${caldavtester}" "${svn_uri_base}/CalDAVTester/trunk" 3671;
 
 #
 # PyFlakes

Modified: CalendarServer/trunk/twistedcaldav/ical.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/ical.py	2009-02-15 19:10:03 UTC (rev 3671)
+++ CalendarServer/trunk/twistedcaldav/ical.py	2009-02-16 01:17:37 UTC (rev 3672)
@@ -1013,11 +1013,18 @@
         """
         @raise ValueError: if the given calendar data is not valid.
         """
-        if self.name() != "VCALENDAR": raise ValueError("Not a calendar")
-        if not self.resourceType(): raise ValueError("Unknown resource type")
+        if self.name() != "VCALENDAR":
+            log.debug("Not a calendar: %s" % (self,))
+            raise ValueError("Not a calendar")
+        if not self.resourceType():
+            log.debug("Unknown resource type: %s" % (self,))
+            raise ValueError("Unknown resource type")
 
         version = self.propertyValue("VERSION")
-        if version != "2.0": raise ValueError("Not a version 2.0 iCalendar (version=%s)" % (version,))
+        if version != "2.0":
+            msg = "Not a version 2.0 iCalendar (version=%s)" % (version,)
+            log.debug(msg)
+            raise ValueError(msg)
 
     def validateForCalDAV(self):
         """
@@ -1028,7 +1035,9 @@
 
         # Disallowed in CalDAV-Access-08, section 4.1
         if self.hasProperty("METHOD"):
-            raise ValueError("METHOD property is not allowed in CalDAV iCalendar data")
+            msg = "METHOD property is not allowed in CalDAV iCalendar data"
+            log.debug(msg)
+            raise ValueError(msg)
 
         self.validateComponentsForCalDAV(False)
 
@@ -1054,7 +1063,9 @@
         for subcomponent in self.subcomponents():
             # Disallowed in CalDAV-Access-08, section 4.1
             if not method and subcomponent.hasProperty("METHOD"):
-                raise ValueError("METHOD property is not allowed in CalDAV iCalendar data")
+                msg = "METHOD property is not allowed in CalDAV iCalendar data"
+                log.debug(msg)
+                raise ValueError(msg)
         
             if subcomponent.name() == "VTIMEZONE":
                 timezones.add(subcomponent.propertyValue("TZID"))
@@ -1063,29 +1074,36 @@
                     ctype = subcomponent.name()
                 else:
                     if ctype != subcomponent.name():
-                        raise ValueError("Calendar resources may not contain more than one type of calendar " +
-                                         "component (%s and %s found)" % (ctype, subcomponent.name()))
+                        msg = "Calendar resources may not contain more than one type of calendar component (%s and %s found)" % (ctype, subcomponent.name())
+                        log.debug(msg)
+                        raise ValueError(msg)
         
                 if ctype not in allowedComponents:
-                    raise ValueError("Component type: %s not allowed" % (ctype,))
+                    msg = "Component type: %s not allowed" % (ctype,)
+                    log.debug(msg)
+                    raise ValueError(msg)
                     
                 uid = subcomponent.propertyValue("UID")
                 if uid is None:
-                    raise ValueError("All components must have UIDs")
+                    msg = "All components must have UIDs"
+                    log.debug(msg)
+                    raise ValueError(msg)
                 rid = subcomponent.getRecurrenceIDUTC()
                 
                 # Verify that UIDs are the same
                 if component_id is None:
                     component_id = uid
                 elif component_id != uid:
-                        raise ValueError("Calendar resources may not contain components with different UIDs " +
-                                         "(%s and %s found)" % (component_id, subcomponent.propertyValue("UID")))
+                    msg = "Calendar resources may not contain components with different UIDs (%s and %s found)" % (component_id, subcomponent.propertyValue("UID"))
+                    log.debug(msg)
+                    raise ValueError(msg)
 
                 # Verify that there is only one master component
                 if rid is None:
                     if got_master:
-                        raise ValueError("Calendar resources may not contain components with the same UIDs and no Recurrence-IDs " +
-                                         "(%s and %s found)" % (component_id, subcomponent.propertyValue("UID")))
+                        msg = "Calendar resources may not contain components with the same UIDs and no Recurrence-IDs (%s and %s found)" % (component_id, subcomponent.propertyValue("UID"))
+                        log.debug(msg)
+                        raise ValueError(msg)
                     else:
                         got_master = True
                         master_recurring = subcomponent.hasProperty("RRULE") or subcomponent.hasProperty("RDATE")
@@ -1094,13 +1112,15 @@
                             
                 # Check that if an override is present then the master is recurring
                 if got_override and got_master and not master_recurring:
-                    raise ValueError("Calendar resources must have a recurring master component if there is an overridden one " +
-                             "(%s)" % (subcomponent.propertyValue("UID"),))
+                    msg = "Calendar resources must have a recurring master component if there is an overridden one (%s)" % (subcomponent.propertyValue("UID"),)
+                    log.debug(msg)
+                    raise ValueError(msg)
                 
                 # Check for duplicate RECURRENCE-IDs        
                 if rid in component_rids:
-                    raise ValueError("Calendar resources may not contain components with the same Recurrence-IDs " +
-                                     "(%s)" % (rid,))
+                    msg = "Calendar resources may not contain components with the same Recurrence-IDs (%s)" % (rid,)
+                    log.debug(msg)
+                    raise ValueError(msg)
                 else:
                     component_rids.add(rid)
 
@@ -1111,7 +1131,9 @@
         #
         for timezone_ref in timezone_refs:
             if timezone_ref not in timezones:
-                raise ValueError("Timezone ID %s is referenced but not defined" % (timezone_ref,))
+                msg = "Timezone ID %s is referenced but not defined: %s" % (timezone_ref, self,)
+                log.debug(msg)
+                raise ValueError(msg)
         
         #
         # FIXME:
@@ -1120,8 +1142,7 @@
         #
         for timezone in timezones:
             if timezone not in timezone_refs:
-                #raise ValueError(
-                log.msg(
+                log.debug(
                     "Timezone %s is not referenced by any non-timezone component" % (timezone,)
                 )
 

Modified: CalendarServer/trunk/twistedcaldav/scheduling/icaldiff.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/icaldiff.py	2009-02-15 19:10:03 UTC (rev 3671)
+++ CalendarServer/trunk/twistedcaldav/scheduling/icaldiff.py	2009-02-16 01:17:37 UTC (rev 3672)
@@ -14,6 +14,7 @@
 # limitations under the License.
 ##
 
+from twistedcaldav.dateops import normalizeToUTC
 from twistedcaldav.ical import Component, Property
 from twistedcaldav.log import Logger
 from twistedcaldav.scheduling.cuaddress import normalizeCUAddr
@@ -112,10 +113,14 @@
             # Find matching component in new calendar
             new_component = self.calendar2.overriddenComponent(rid)
             if new_component is None:
+                # 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
-                if True:
+                new_component = self.calendar2.deriveInstance(rid)
+                if new_component:
                     # Derive a new instance from the new calendar and transfer attendee status
-                    new_component = self.calendar2.deriveInstance(rid)
                     self.calendar2.addComponent(new_component)
                     self._tryComponentMerge(old_component, new_component, ignore_attendee, is_organizer)
                 else:
@@ -137,6 +142,10 @@
             # Find matching component in old calendar
             old_component = self.calendar1.overriddenComponent(rid)
             if old_component is None:
+                # 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.calendar1.deriveInstance(rid)
                 if old_component:
@@ -242,14 +251,14 @@
         self.calendar2 = duplicateAndNormalize(self.calendar2)
 
         if self.calendar1 == self.calendar2:
-            return True, True, ()
+            return True, True
 
         # Need to look at each component and do special comparisons
         
         # Make sure the same VCALENDAR properties match
         if not self._checkVCALENDARProperties():
             self._logDiffError("attendeeDiff: VCALENDAR properties do not match")
-            return False, False, ()
+            return False, False
         
         # Make sure the same VTIMEZONE components appear
         tzidRemapping = False
@@ -271,7 +280,7 @@
                     self._logDiffError("attendeeDiff: VTIMEZONEs re-mapped")
                 except ValueError, e:
                     self._logDiffError("attendeeDiff: VTIMEZONE re-mapping failed: %s" % (str(e),))
-                    return False, False, ()
+                    return False, False
 
         return result
     
@@ -414,6 +423,7 @@
         # The following statement is required to force vobject to serialize the
         # calendar data and in the process add any missing VTIMEZONEs as needed.
         _ignore = str(self.originalCalendar2)
+        log.debug(_ignore)
         
     def _remapTZIDsOnComponent(self, oldComponent, newComponent):
         """
@@ -483,6 +493,8 @@
         # First get uid/rid map of components
         def mapComponents(calendar):
             map = {}
+            cancelledRids = set()
+            master = None
             for component in calendar.subcomponents():
                 if component.name() == "VTIMEZONE":
                     continue
@@ -490,63 +502,80 @@
                 uid = component.propertyValue("UID")
                 rid = component.getRecurrenceIDUTC()
                 map[(name, uid, rid,)] = component
-            return map
+                if component.propertyValue("STATUS") == "CANCELLED" and rid is not None:
+                    cancelledRids.add(rid)
+                if rid is None:
+                    master = component
+            
+            # Normalize each master by adding any STATUS:CANCELLED components as EXDATEs
+            exdates = set()
+            if master:
+                for rid in sorted(cancelledRids):
+                    master.addProperty(Property("EXDATE", [rid,]))
+                
+                # Get all EXDATEs in UTC
+                for exdate in master.properties("EXDATE"):
+                    exdates.update([normalizeToUTC(value) for value in exdate.value()])
+               
+            return exdates, map
         
-        map1 = mapComponents(self.calendar1)
+        exdates1, map1 = mapComponents(self.calendar1)
         set1 = set(map1.keys())
-        map2 = mapComponents(self.calendar2)
+        exdates2, map2 = mapComponents(self.calendar2)
         set2 = set(map2.keys())
 
-        # Ugly case: if an Attendee has a STATUS:CANCELLED meeting and the ORGANIZER does not,
-        # we may need to remove an EXDATE for the cancelled instance from the ORGANIZER's
-        # master instance to ensure that matches
-        cancelled_rids = []
-        master2 = self.calendar2.masterComponent()
-        for key in set2 - set1:
-            component2 = map2[key]
-            if component2.propertyValue("STATUS") == "CANCELLED":
-                rid = component2.getRecurrenceIDUTC()
-                cancelled_rids.append(rid)
-                if master2:
-                    master2.addProperty(Property("EXDATE", [rid,]))
-        
-        # All the components in calendar1 must be in calendar2
+        # All the components in calendar1 must be in calendar2 unless they are CANCELLED
         result = set1 - set2
-        if result:
-            log.debug("Missing components from first calendar: %s" % (result,))
-            return False, False, ()
+        for key in result:
+            component = map1[key]
+            if component.propertyValue("STATUS") != "CANCELLED":
+                log.debug("Missing uncancelled component from first calendar: %s" % (key,))
+                return False, False
+            else: 
+                _ignore_name, _ignore_uid, rid = key
+                if rid not in exdates2:
+                    log.debug("Missing EXDATE for cancelled components from first calendar: %s" % (key,))
+                    return False, False
+                    
 
         # Now verify that each component in set1 matches what is in set2
         attendee_unchanged = True
         for key, value in map1.iteritems():
             component1 = value
-            component2 = map2[key]
+            component2 = map2.get(key)
+            if component2 is None:
+                continue
 
             nomismatch, no_attendee_change = self._testComponents(component1, component2)
             if not nomismatch:
-                return False, False, ()
+                return False, False
             attendee_unchanged &= no_attendee_change
         
         # Now verify that each additional component in set2 matches a derived component in set1
         for key in set2 - set1:
             
-            # First check if the attendee's copy is cancelled
+            # First check if the attendee's copy is cancelled and properly EXDATE'd
+            # and skip it if so.
             component2 = map2[key]
             if component2.propertyValue("STATUS") == "CANCELLED":
+                _ignore_name, _ignore_uid, rid = key
+                if rid not in exdates1:
+                    log.debug("Cancelled component not found in first calendar (or no EXDATE): %s" % (key,))
+                    return False, False
                 continue
 
             # Now derive the organizer's expected instance and compare
             component1 = self.calendar1.deriveInstance(key[2])
             if component1 is None:
                 log.debug("_compareComponents: Could not derive instance: %s" % (key[2],))
-                return False, False, ()
+                return False, False
             
             nomismatch, no_attendee_change = self._testComponents(component1, component2)
             if not nomismatch:
-                return False, False, ()
+                return False, False
             attendee_unchanged &= no_attendee_change
             
-        return True, attendee_unchanged, tuple(cancelled_rids)
+        return True, attendee_unchanged
 
     def _testComponents(self, comp1, comp2):
         

Modified: CalendarServer/trunk/twistedcaldav/scheduling/implicit.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/implicit.py	2009-02-15 19:10:03 UTC (rev 3671)
+++ CalendarServer/trunk/twistedcaldav/scheduling/implicit.py	2009-02-16 01:17:37 UTC (rev 3672)
@@ -648,8 +648,12 @@
             yield self.doAccessControl(self.attendeePrincipal, False)
 
         if self.action == "remove":
-            log.debug("Implicit - attendee '%s' is cancelling UID: '%s'" % (self.attendee, self.uid))
-            yield self.scheduleCancelWithOrganizer()
+            if self.calendar.hasPropertyValueInAllComponents(Property("STATUS", "CANCELLED")):
+                log.debug("Implicit - attendee '%s' is removing cancelled UID: '%s'" % (self.attendee, self.uid))
+                # Nothing else to do
+            else:
+                log.debug("Implicit - attendee '%s' is cancelling UID: '%s'" % (self.attendee, self.uid))
+                yield self.scheduleCancelWithOrganizer()
         
         else:
             # Get the ORGANIZER's current copy of the calendar object
@@ -662,7 +666,7 @@
                     self.oldcalendar = None
 
                 # Determine whether the current change is allowed
-                change_allowed, no_itip, cancelled_rids = self.isAttendeeChangeInsignificant()
+                change_allowed, no_itip = self.isAttendeeChangeInsignificant()
 
                 if not change_allowed:
                     if self.calendar.hasPropertyValueInAllComponents(Property("STATUS", "CANCELLED")):
@@ -673,21 +677,6 @@
                         log.error("Attendee '%s' is not allowed to make an unauthorized change to an organized event: UID:%s" % (self.attendeePrincipal, self.uid,))
                         raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-attendee-change")))
 
-                # Remove orphaned attendee cancelled events
-                if cancelled_rids:
-                    log.debug("Attendee '%s' is creating CANCELLED overridden instances for UID: '%s' - removing instances" % (self.attendee, self.uid,))
-                    master = self.calendar.masterComponent()
-                    for rid in cancelled_rids:
-                        self.calendar.removeComponent(self.calendar.overriddenComponent(rid))
-                        if master:
-                            master.addProperty(Property("EXDATE", [rid,]))
-                    
-                    # If no components left, make sure we delete the orphaned event
-                    if self.calendar.mainType() is None:
-                        log.debug("Attendee '%s' CANCELLED all instances of UID: '%s' - removing entire event" % (self.attendee, self.uid,))
-                        self.return_status = ImplicitScheduler.STATUS_ORPHANED_CANCELLED_EVENT
-                        returnValue(None)
-
                 if no_itip:
                     log.debug("Implicit - attendee '%s' is updating UID: '%s' but change is not significant" % (self.attendee, self.uid))
                     returnValue(None)
@@ -758,10 +747,8 @@
             oldcalendar = self.organizer_calendar
             oldcalendar.attendeesView((self.attendee,))
         differ = iCalDiff(oldcalendar, self.calendar, self.do_smart_merge)
-        change_allowed, no_itip, cancelled_rids = differ.attendeeDiff(self.attendee)
+        return differ.attendeeDiff(self.attendee)
 
-        return change_allowed, no_itip, cancelled_rids
-
     def scheduleWithOrganizer(self):
 
         itipmsg = iTipGenerator.generateAttendeeReply(self.calendar, self.attendee)

Modified: CalendarServer/trunk/twistedcaldav/scheduling/itip.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/itip.py	2009-02-15 19:10:03 UTC (rev 3671)
+++ CalendarServer/trunk/twistedcaldav/scheduling/itip.py	2009-02-16 01:17:37 UTC (rev 3672)
@@ -33,6 +33,7 @@
 import datetime
 
 from twistedcaldav.config import config
+from twistedcaldav.dateops import normalizeToUTC
 from twistedcaldav.log import Logger
 from twistedcaldav.ical import Property, iCalendarProductID, Component
 
@@ -118,6 +119,24 @@
                 if component.name() != "VTIMEZONE" and component.getRecurrenceIDUTC() is not None:
                     iTipProcessing.transferItems(calendar, master_valarms, private_comments, component)
             
+            # Now try to match recurrences
+            for component in calendar.subcomponents():
+                if component.name() != "VTIMEZONE" and component.getRecurrenceIDUTC() is not None:
+                    rid = component.getRecurrenceIDUTC()
+                    if new_calendar.overriddenComponent(rid) is None:
+                        new_component = new_calendar.deriveInstance(rid)
+                        new_calendar.addComponent(new_component)
+                        iTipProcessing.transferItems(calendar, master_valarms, private_comments, new_component)
+                        if component.propertyValue("STATUS") == "CANCELLED":
+                            new_component.replaceProperty(Property("STATUS", "CANCELLED"))
+                            for exdate in master_component.properties("EXDATE"):
+                                for value in exdate.value():
+                                    if value == rid:
+                                        exdate.value().remove(value)
+                                        if len(exdate.value()) == 0:
+                                            master_component.removeProperty(exdate)
+                                        break
+            
             # Replace the entire object
             return new_calendar, props_changed, rids
 
@@ -142,7 +161,7 @@
             return calendar, props_changed, rids
 
     @staticmethod
-    def processCancel(itip_message, calendar):
+    def processCancel(itip_message, calendar, autoprocessing=False):
         """
         Process a METHOD=CANCEL.
         
@@ -164,7 +183,13 @@
 
         # Check to see if this is a cancel of the entire event
         if itip_message.masterComponent() is not None:
-            return True, True, None
+            if autoprocessing:
+                # Delete the entire event off the auto-processed calendar
+                return True, True, None
+            else:
+                # Cancel every instance in the existing event
+                calendar.replacePropertyInAllComponents(Property("STATUS", "CANCELLED"))
+                return True, False, None
 
         # iTIP CANCEL can contain multiple components being cancelled in the RECURRENCE-ID case.
         # So we need to iterate over each iTIP component.
@@ -189,17 +214,28 @@
             if overridden:
                 # We are cancelling an overridden component.
 
-                # Exclude the cancelled instance
-                exdates.append(component.getRecurrenceIDUTC())
+                if autoprocessing:
+                    # Exclude the cancelled instance
+                    exdates.append(component.getRecurrenceIDUTC())
                 
-                # Remove the existing component.
-                calendar.removeComponent(overridden)
+                    # Remove the existing component.
+                    calendar.removeComponent(overridden)
+                else:
+                    # Existing component is cancelled.
+                    overridden.replaceProperty(Property("STATUS", "CANCELLED"))
+
             elif calendar_master:
                 # We are trying to CANCEL a non-overridden instance.
+                
+                if autoprocessing:
+                    # Exclude the cancelled instance
+                    exdates.append(component.getRecurrenceIDUTC())
+                else:
+                    # Derive a new component and cancel it.
+                    overridden = calendar.deriveInstance(rid)
+                    overridden.replaceProperty(Property("STATUS", "CANCELLED"))
+                    calendar.addComponent(overridden)
 
-                # Exclude the cancelled instance
-                exdates.append(component.getRecurrenceIDUTC())
-
         # If we have any EXDATEs lets add them to the existing calendar object.
         if exdates and calendar_master:
             calendar_master.addProperty(Property("EXDATE", exdates))
@@ -461,7 +497,7 @@
             comp.addProperty(Property("SEQUENCE", seq))
             comp.addProperty(instance.getOrganizerProperty())
             if instance_rid:
-                comp.addProperty(Property("RECURRENCE-ID", instance_rid))
+                comp.addProperty(Property("RECURRENCE-ID", normalizeToUTC(instance_rid)))
             
             def addProperties(propname):
                 for property in instance.properties(propname):

Modified: CalendarServer/trunk/twistedcaldav/scheduling/processing.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/processing.py	2009-02-15 19:10:03 UTC (rev 3671)
+++ CalendarServer/trunk/twistedcaldav/scheduling/processing.py	2009-02-16 01:17:37 UTC (rev 3672)
@@ -369,7 +369,7 @@
             autoprocessed = self.recipient.principal.autoSchedule()
 
             # Check to see if this is a cancel of the entire event
-            processed_message, delete_original, rids = iTipProcessing.processCancel(self.message, self.recipient_calendar)
+            processed_message, delete_original, rids = iTipProcessing.processCancel(self.message, self.recipient_calendar, autoprocessing=autoprocessed)
             if processed_message:
                 if delete_original:
                     
@@ -393,14 +393,14 @@
                     yield self.writeCalendarResource(self.recipient_calendar_collection_uri, self.recipient_calendar_collection, self.recipient_calendar_name, self.recipient_calendar)
 
                     # Build the schedule-changes XML element
+                    actions = (customxml.Cancel(),)
+                    if rids:
+                        actions += (customxml.Recurrences(
+                            *[customxml.RecurrenceID.fromString(rid) for rid in rids]
+                        ),)
                     changes = customxml.ScheduleChanges(
                         customxml.DTStamp(),
-                        customxml.Action(
-                            customxml.Cancel(),
-                            customxml.Recurrences(
-                                *[customxml.RecurrenceID.fromString(rid) for rid in rids]
-                            ),
-                        ),
+                        customxml.Action(*actions),
                     )
                     result = (True, autoprocessed, changes)
             else:
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20090215/62aa3927/attachment-0001.html>


More information about the calendarserver-changes mailing list