[CalendarServer-changes] [2831] CalendarServer/branches/users/cdaboo/implicit-2805/twistedcaldav

source_changes at macosforge.org source_changes at macosforge.org
Mon Aug 18 12:52:50 PDT 2008


Revision: 2831
          http://trac.macosforge.org/projects/calendarserver/changeset/2831
Author:   cdaboo at apple.com
Date:     2008-08-18 12:52:49 -0700 (Mon, 18 Aug 2008)
Log Message:
-----------
Handle almost all the auto-processing that we need to. Still need to do attendee request updates.

Modified Paths:
--------------
    CalendarServer/branches/users/cdaboo/implicit-2805/twistedcaldav/ical.py
    CalendarServer/branches/users/cdaboo/implicit-2805/twistedcaldav/itip.py
    CalendarServer/branches/users/cdaboo/implicit-2805/twistedcaldav/method/put_common.py
    CalendarServer/branches/users/cdaboo/implicit-2805/twistedcaldav/scheduling/implicit.py
    CalendarServer/branches/users/cdaboo/implicit-2805/twistedcaldav/scheduling/processing.py

Added Paths:
-----------
    CalendarServer/branches/users/cdaboo/implicit-2805/twistedcaldav/scheduling/itip.py
    CalendarServer/branches/users/cdaboo/implicit-2805/twistedcaldav/scheduling/test/test_implicit.py
    CalendarServer/branches/users/cdaboo/implicit-2805/twistedcaldav/scheduling/test/test_itip.py

Modified: CalendarServer/branches/users/cdaboo/implicit-2805/twistedcaldav/ical.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2805/twistedcaldav/ical.py	2008-08-18 19:51:37 UTC (rev 2830)
+++ CalendarServer/branches/users/cdaboo/implicit-2805/twistedcaldav/ical.py	2008-08-18 19:52:49 UTC (rev 2831)
@@ -34,6 +34,7 @@
 from twistedcaldav.dateops import compareDateTime, normalizeToUTC, timeRangesOverlap
 from twistedcaldav.instance import InstanceList
 from twistedcaldav.log import Logger
+from types import ListType
 from vobject import newFromBehavior, readComponents
 from vobject.base import Component as vComponent, ContentLine as vContentLine, ParseError as vParseError
 from vobject.icalendar import TimezoneComponent, dateTimeToString, deltaToOffset, getTransition, stringToDate, stringToDateTime, stringToDurations, utc
@@ -83,7 +84,11 @@
     def __str__ (self): return self._vobject.serialize()
     def __repr__(self): return "<%s: %r: %r>" % (self.__class__.__name__, self.name(), self.value())
 
-    def __hash__(self): return hash((self.name(), self.value()))
+    def __hash__(self):
+        if type(self.value()) is ListType:
+            return hash((self.name(), tuple(self.value())))
+        else:
+            return hash((self.name(), self.value()))
 
     def __ne__(self, other): return not self.__eq__(other)
     def __eq__(self, other):
@@ -763,6 +768,24 @@
         instances.expandTimeRanges(componentSet, limit)
         return instances
 
+    def getComponentInstances(self):
+        """
+        Get the R-ID value for each component.
+        
+        @return: a tuple of recurrence-ids
+        """
+        
+        # Extract appropriate sub-component if this is a VCALENDAR
+        if self.name() == "VCALENDAR":
+            result = ()
+            for component in self.subcomponents():
+                if component.name() != "VTIMEZONE":
+                    result += component.getComponentInstances()
+            return result
+        else:
+            rid = self.getRecurrenceIDUTC()
+            return (rid,)
+
     def deriveInstance(self, rid):
         """
         Derive an instance from the master component that has the provided RECURRENCE-ID, but

Modified: CalendarServer/branches/users/cdaboo/implicit-2805/twistedcaldav/itip.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2805/twistedcaldav/itip.py	2008-08-18 19:51:37 UTC (rev 2830)
+++ CalendarServer/branches/users/cdaboo/implicit-2805/twistedcaldav/itip.py	2008-08-18 19:52:49 UTC (rev 2831)
@@ -45,12 +45,10 @@
 from twistedcaldav import caldavxml
 from twistedcaldav.accounting import accountingEnabled, emitAccounting
 from twistedcaldav.log import Logger
-from twistedcaldav.ical import Property, iCalendarProductID, Component
+from twistedcaldav.ical import Property, iCalendarProductID
 from twistedcaldav.method import report_common
 from twistedcaldav.resource import isCalendarCollectionResource
 
-from vobject.icalendar import utc
-
 log = Logger()
 
 __version__ = "0.0"
@@ -899,99 +897,3 @@
             return -1
     
         return 0
-
-class iTipGenerator(object):
-    
-    @staticmethod
-    def generateCancel(original, attendees, instances=None):
-        
-        itip = Component("VCALENDAR")
-        itip.addProperty(Property("VERSION", "2.0"))
-        itip.addProperty(Property("PRODID", iCalendarProductID))
-        itip.addProperty(Property("METHOD", "CANCEL"))
-
-        if instances is None:
-            instances = (None,)
-
-        for instance_rid in instances:
-            
-            # Create a new component matching the type of the original
-            comp = Component(original.mainType())
-            itip.addComponent(comp)
-
-            # Use the master component when the instance is None
-            if not instance_rid:
-                instance = original.masterComponent()
-            else:
-                instance = original.overriddenComponent(instance_rid)
-                if instance is None:
-                    instance = original.masterComponent()
-            assert instance is not None
-
-            # Add some required properties extracted from the original
-            comp.addProperty(Property("DTSTAMP", datetime.datetime.now(tz=utc)))
-            comp.addProperty(Property("UID", instance.propertyValue("UID")))
-            seq = instance.propertyValue("SEQUENCE")
-            seq = str(int(seq) + 1) if seq else "1"
-            comp.addProperty(Property("SEQUENCE", seq))
-            comp.addProperty(instance.getOrganizerProperty())
-            if instance_rid:
-                comp.addProperty(Property("RECURRENCE-ID", instance_rid))
-            
-            # Extract the matching attendee property
-            for attendee in attendees:
-                attendeeProp = instance.getAttendeeProperty((attendee,))
-                assert attendeeProp is not None
-                comp.addProperty(attendeeProp)
-
-        return itip
-
-    @staticmethod
-    def generateAttendeeRequest(original, attendees):
-
-        # Start with a copy of the original as we may have to modify bits of it
-        itip = original.duplicate()
-        itip.addProperty(Property("METHOD", "REQUEST"))
-        
-        # Now filter out components that do not contain every attendee
-        itip.attendeesView(attendees)
-        
-        # No alarms
-        itip.removeAlarms()
-
-        return itip
-
-    @staticmethod
-    def generateAttendeeReply(original, attendee, force_decline=False):
-
-        # Start with a copy of the original as we may have to modify bits of it
-        itip = original.duplicate()
-        itip.addProperty(Property("METHOD", "REPLY"))
-        
-        # Remove all attendees except the one we want
-        itip.removeAllButOneAttendee(attendee)
-        
-        # No alarms
-        itip.removeAlarms()
-
-        # Remove all but essential properties
-        itip.removeUnwantedProperties((
-            "UID",
-            "RECURRENCE-ID",
-            "SEQUENCE",
-            "DTSTAMP",
-            "ORGANIZER",
-            "ATTENDEE",
-        ))
-        
-        # 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:
-                if "PARTSTAT" in attendeeProp.params():
-                    attendeeProp.params()["PARTSTAT"][0] = "DECLINED"
-                else:
-                    attendeeProp.params()["PARTSTAT"] = ["DECLINED"]
-        
-        return itip

Modified: CalendarServer/branches/users/cdaboo/implicit-2805/twistedcaldav/method/put_common.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2805/twistedcaldav/method/put_common.py	2008-08-18 19:51:37 UTC (rev 2830)
+++ CalendarServer/branches/users/cdaboo/implicit-2805/twistedcaldav/method/put_common.py	2008-08-18 19:52:49 UTC (rev 2831)
@@ -181,7 +181,8 @@
         source=None, source_uri=None, sourceparent=None, sourcecal=False, deletesource=False,
         destination=None, destination_uri=None, destinationparent=None, destinationcal=True,
         calendar=None,
-        isiTIP=False
+        isiTIP=False,
+        allowImplicitSchedule=True,
     ):
         """
         Function that does common PUT/COPY/MOVE behavior.
@@ -198,7 +199,8 @@
         @param sourceparent:      the L{CalDAVFile} for the source resource's parent collection, or None if source is None.
         @param destinationparent: the L{CalDAVFile} for the destination resource's parent collection.
         @param deletesource:      True if the source resource is to be deleted on successful completion, False otherwise.
-        @param isiTIP:            True if relaxed calendar data validation is to be done, False otherwise.
+        @param isiTIP:                True if relaxed calendar data validation is to be done, False otherwise.
+        @param allowImplicitSchedule: True if implicit scheduling should be attempted, False otherwise.
         """
         
         # Check that all arguments are valid
@@ -236,6 +238,7 @@
         self.calendardata = None
         self.deletesource = deletesource
         self.isiTIP = isiTIP
+        self.allowImplicitSchedule = allowImplicitSchedule
         
         self.rollback = None
         self.access = None
@@ -677,7 +680,7 @@
             yield self.checkQuota()
 
             # Do scheduling
-            if not self.isiTIP:
+            if not self.isiTIP and self.allowImplicitSchedule:
                 scheduler = ImplicitScheduler()
                 self.calendar = (yield scheduler.doImplicitScheduling(self.request, self.destination, self.calendar, False))
 

Modified: CalendarServer/branches/users/cdaboo/implicit-2805/twistedcaldav/scheduling/implicit.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2805/twistedcaldav/scheduling/implicit.py	2008-08-18 19:51:37 UTC (rev 2830)
+++ CalendarServer/branches/users/cdaboo/implicit-2805/twistedcaldav/scheduling/implicit.py	2008-08-18 19:52:49 UTC (rev 2831)
@@ -19,7 +19,7 @@
 from twisted.web2.dav.http import ErrorResponse
 from twisted.web2.http import HTTPError
 from twistedcaldav.caldavxml import caldav_namespace
-from twistedcaldav.itip import iTipGenerator
+from twistedcaldav.scheduling.itip import iTipGenerator
 from twistedcaldav.log import Logger
 from twistedcaldav.scheduling.scheduler import CalDAVScheduler
 from twistedcaldav.method import report_common
@@ -188,13 +188,68 @@
         as users that need to be sent a cancel.
         """
         
+        # Several possibilities for when CANCELs need to be sent:
+        #
+        # Remove ATTENDEE property
+        # Add EXDATE
+        # Remove overridden component
+        # Remove RDATE
+        # Truncate RRULE
+        # Change RRULE
+        
+        # TODO: the later three will be ignored for now.
+
         oldAttendeesByInstance = self.oldcalendar.getAttendeesByInstance()
         
         mappedOld = set(oldAttendeesByInstance)
         mappedNew = set(self.attendeesByInstance)
         
-        self.cancelledAttendees = mappedOld.difference(mappedNew)
+        # Get missing instances
+        oldInstances = set(self.oldcalendar.getComponentInstances())
+        newInstances = set(self.calendar.getComponentInstances())
+        removedInstances = oldInstances - newInstances
 
+        # Also look for new EXDATEs
+        oldexdates = set()
+        for property in self.oldcalendar.masterComponent().properties("EXDATE"):
+            oldexdates.update(property.value())
+        newexdates = set()
+        for property in self.calendar.masterComponent().properties("EXDATE"):
+            newexdates.update(property.value())
+
+        addedexdates = newexdates - oldexdates
+
+        # 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)
+                else:
+                    # 3.
+                    if (new_attendee, None) not in mappedNew or rid in addedexdates:
+                        self.cancelledAttendees.add(item)
+
+        master_attendees = self.oldcalendar.masterComponent().getAttendeesByInstance()
+        for attendee, _ignore in master_attendees:
+            for exdate in addedexdates:
+                # Don't remove the master attendee's when an EXDATE is added for a removed overridden component
+                # 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))
+
     @inlineCallbacks
     def scheduleWithAttendees(self):
         

Added: CalendarServer/branches/users/cdaboo/implicit-2805/twistedcaldav/scheduling/itip.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2805/twistedcaldav/scheduling/itip.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/implicit-2805/twistedcaldav/scheduling/itip.py	2008-08-18 19:52:49 UTC (rev 2831)
@@ -0,0 +1,329 @@
+##
+# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+iTIP (RFC2446) processing.
+"""
+
+#
+# This is currently used for handling auto-replies to schedule requests arriving
+# in an inbox. It is called in a delayed fashion via reactor.callLater.
+#
+# We assume that all the components/calendars we deal with have been determined
+# as being 'valid for CalDAV/iTIP', i.e. they contain UIDs, single component
+# types, etc.
+#
+# The logic for component matching needs a lot more work as it currently does not
+# know how to deal with overridden instances.
+#
+
+import datetime
+
+from twistedcaldav.log import Logger
+from twistedcaldav.ical import Property, iCalendarProductID, Component
+
+from vobject.icalendar import utc
+
+log = Logger()
+
+__version__ = "0.0"
+
+__all__ = [
+    "iTipProcessor",
+    "iTipGenerator",
+]
+
+class iTipProcessing(object):
+    
+    @staticmethod
+    def processNewRequest(itip_message):
+        """
+        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"
+
+        calendar = itip_message.duplicate()
+        method = calendar.getProperty("METHOD")
+        if method:
+            calendar.removeProperty(method)
+            
+        return calendar
+        
+    @staticmethod
+    def processRequest(itip_message, calendar):
+        """
+        Process a METHOD=REQUEST.
+        
+        @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: C{True} if request is valid, C{False} otherwise (request should be ignored)
+        """
+        pass
+
+    @staticmethod
+    def processCancel(itip_message, calendar):
+        """
+        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
+        """
+        
+        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"
+
+        # Check to see if this is a cancel of the entire event
+        if itip_message.masterComponent() is not None:
+            return True, True
+
+        # iTIP CANCEL can contain multiple components being cancelled in the RECURRENCE-ID case.
+        # So we need to iterate over each iTIP component.
+
+        # Get the existing calendar master object if it exists
+        calendar_master = calendar.masterComponent()
+        exdates = []
+
+        # Look at each component in the iTIP message
+        for component in itip_message.subcomponents():
+            if component.name() == "VTIMEZONE":
+                continue
+        
+            # Extract RECURRENCE-ID value from component
+            rid = component.getRecurrenceIDUTC()
+            
+            # Get the one that matches in the calendar
+            overridden = calendar.overriddenComponent(rid)
+            
+            if overridden:
+                # We are cancelling an overridden component.
+
+                # Exclude the cancelled instance
+                exdates.append(component.getRecurrenceIDUTC())
+                
+                # Remove the existing component.
+                calendar.removeComponent(overridden)
+            elif calendar_master:
+                # We are trying to CANCEL a non-overridden instance.
+
+                # 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))
+
+        # See if there are still components in the calendar - we might have deleted the last overridden instance
+        # in which case the calendar object is empty (except for VTIMEZONEs).
+        if calendar.mainType() is None:
+            # Delete the now empty calendar object
+            return True, True
+        else:
+            return True, False
+    
+    @staticmethod
+    def processReply(itip_message, calendar):
+        """
+        Process a METHOD=REPLY.
+        
+        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 REPLY to
+        @type calendar:
+        
+        @return: C{True} if processed, C{False} if scheduling message should be ignored
+        """
+        
+        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"
+
+        # Take each component in the reply and update the corresponding component
+        # in the organizer's copy (possibly generating new ones) so that the ATTENDEE
+        # PARTSTATs match up.
+
+        # Do the master first
+        old_master = calendar.masterComponent()
+        new_master = itip_message.masterComponent()
+        if new_master:
+            iTipProcessing.updateAttendeePartStat(new_master, old_master)
+
+        # Now do all overridden ones
+        for itip_component in itip_message.subcomponents():
+            
+            # Make sure we have an appropriate component
+            if itip_component.name() == "VTIMEZONE":
+                continue
+            rid = itip_component.getRecurrenceIDUTC()
+            if rid is None:
+                continue
+            
+            # Find matching component in organizer's copy
+            match_component = calendar.overriddenComponent(rid)
+            if match_component is None:
+                # Attendee is overriding an instance themselves - we need to create a derived one
+                # for the Organizer
+                match_component = calendar.deriveInstance(rid)
+                calendar.addComponent(match_component)
+
+            iTipProcessing.updateAttendeePartStat(itip_component, match_component)
+                
+        return True
+
+    @staticmethod
+    def updateAttendeePartStat(from_component, to_component):
+        """
+        Copy the PARTSTAT of the Attendee in the from_component to the matching ATTENDEE
+        in the to_component. Ignore if no match found.
+
+        @param from_component:
+        @type from_component:
+        @param to_component:
+        @type to_component:
+        """
+        
+        # Get attendee in from_component - there MUST be only one
+        attendees = tuple(from_component.properties("ATTENDEE"))
+        assert len(attendees) == 1, "There must be one and only one ATTENDEE property in a REPLY"
+        attendee = attendees[0]
+        partstat = attendee.params().get("PARTSTAT", ("NEEDS-ACTION",))[0]
+        
+        # Now find matching ATTENDEE in to_component
+        existing_attendee = to_component.getAttendeeProperty((attendee.value(),))
+        if existing_attendee:
+            existing_attendee.params().setdefault("PARTSTAT", [partstat])[0] = partstat
+
+class iTipGenerator(object):
+    
+    @staticmethod
+    def generateCancel(original, attendees, instances=None):
+        
+        itip = Component("VCALENDAR")
+        itip.addProperty(Property("VERSION", "2.0"))
+        itip.addProperty(Property("PRODID", iCalendarProductID))
+        itip.addProperty(Property("METHOD", "CANCEL"))
+
+        if instances is None:
+            instances = (None,)
+
+        tzids = set()
+        for instance_rid in instances:
+            
+            # Create a new component matching the type of the original
+            comp = Component(original.mainType())
+            itip.addComponent(comp)
+
+            # Use the master component when the instance is None
+            if not instance_rid:
+                instance = original.masterComponent()
+            else:
+                instance = original.overriddenComponent(instance_rid)
+                if instance is None:
+                    instance = original.masterComponent()
+            assert instance is not None
+
+            # Add some required properties extracted from the original
+            comp.addProperty(Property("DTSTAMP", datetime.datetime.now(tz=utc)))
+            comp.addProperty(Property("UID", instance.propertyValue("UID")))
+            seq = instance.propertyValue("SEQUENCE")
+            seq = str(int(seq) + 1) if seq else "1"
+            comp.addProperty(Property("SEQUENCE", seq))
+            comp.addProperty(instance.getOrganizerProperty())
+            if instance_rid:
+                comp.addProperty(Property("RECURRENCE-ID", instance_rid))
+            
+            # Extract the matching attendee property
+            for attendee in attendees:
+                attendeeProp = instance.getAttendeeProperty((attendee,))
+                assert attendeeProp is not None
+                comp.addProperty(attendeeProp)
+
+            tzids.update(comp.timezoneIDs())
+            
+        # Now include any referenced tzids
+        for comp in original.subcomponents():
+            if comp.name() == "VTIMEZONE":
+                tzid = comp.propertyValue("TZID")
+                if tzid in tzids:
+                    itip.addComponent(comp)
+
+        return itip
+
+    @staticmethod
+    def generateAttendeeRequest(original, attendees):
+
+        # Start with a copy of the original as we may have to modify bits of it
+        itip = original.duplicate()
+        itip.addProperty(Property("METHOD", "REQUEST"))
+        
+        # Now filter out components that do not contain every attendee
+        itip.attendeesView(attendees)
+        
+        # No alarms
+        itip.removeAlarms()
+
+        return itip
+
+    @staticmethod
+    def generateAttendeeReply(original, attendee, force_decline=False):
+
+        # Start with a copy of the original as we may have to modify bits of it
+        itip = original.duplicate()
+        itip.addProperty(Property("METHOD", "REPLY"))
+        
+        # Remove all attendees except the one we want
+        itip.removeAllButOneAttendee(attendee)
+        
+        # No alarms
+        itip.removeAlarms()
+
+        # Remove all but essential properties
+        itip.removeUnwantedProperties((
+            "UID",
+            "RECURRENCE-ID",
+            "SEQUENCE",
+            "DTSTAMP",
+            "ORGANIZER",
+            "ATTENDEE",
+        ))
+        
+        # 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:
+                if "PARTSTAT" in attendeeProp.params():
+                    attendeeProp.params()["PARTSTAT"][0] = "DECLINED"
+                else:
+                    attendeeProp.params()["PARTSTAT"] = ["DECLINED"]
+        
+        return itip

Modified: CalendarServer/branches/users/cdaboo/implicit-2805/twistedcaldav/scheduling/processing.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2805/twistedcaldav/scheduling/processing.py	2008-08-18 19:51:37 UTC (rev 2830)
+++ CalendarServer/branches/users/cdaboo/implicit-2805/twistedcaldav/scheduling/processing.py	2008-08-18 19:52:49 UTC (rev 2831)
@@ -18,6 +18,11 @@
 from twistedcaldav.log import Logger
 from twistedcaldav.method import report_common
 from twisted.web2.dav.fileop import delete
+from twistedcaldav.scheduling.itip import iTipProcessing
+from hashlib import md5
+from twisted.web2.dav.util import joinURL
+from twistedcaldav.caldavxml import caldav_namespace
+import time
 
 __all__ = [
     "ImplicitProcessor",
@@ -86,21 +91,6 @@
         return self.method in ("REQUEST", "ADD", "CANCEL")
 
     @inlineCallbacks
-    def doImplicitOrganizer(self):
-
-        # Locate the organizer's copy of the event.
-        yield self.getRecipientsCopy()
-        if self.recipient_calendar is None:
-            log.debug("Implicit - originator '%s' to recipient '%s' ignoring UID: '%s' - organizer has no copy" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
-            returnValue((True, True,))
-
-        # Update the organizer's copy with the partstat's of the replying attendee
-        
-        # Send out a request to all attendees to update them
-
-        returnValue((False, False,))
-
-    @inlineCallbacks
     def getRecipientsCopy(self):
         """
         Get the Recipient's copy of the event being processed.
@@ -123,8 +113,9 @@
                 rname = collection.index().resourceNameForUID(self.uid)
                 if rname:
                     self.recipient_calendar = collection.iCalendar(rname)
+                    self.recipient_calendar_name = rname
                     self.recipient_calendar_collection = collection
-                    self.recipient_calendar_name = rname
+                    self.recipient_calendar_collection_uri = uri
                     return succeed(False)
                 else:
                     return succeed(True)
@@ -134,6 +125,44 @@
             yield report_common.applyToCalendarCollections(calendar_home, self.request, calendar_home.url(), "infinity", queryCalendarCollection, None)
     
     @inlineCallbacks
+    def doImplicitOrganizer(self):
+
+        # Locate the organizer's copy of the event.
+        yield self.getRecipientsCopy()
+        if self.recipient_calendar is None:
+            log.debug("ImplicitProcessing - originator '%s' to recipient '%s' ignoring UID: '%s' - organizer has no copy" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
+            returnValue((True, True,))
+
+        # Handle new items differently than existing ones.
+        if self.method == "REPLY":
+            result = (yield self.doImplicitOrganizerUpdate())
+        elif self.method == "REFRESH":
+            # With implicit we ignore refreshes.
+            # TODO: for iMIP etc we do need to handle them 
+            result = (True, True,)
+
+        returnValue(result)
+
+    @inlineCallbacks
+    def doImplicitOrganizerUpdate(self):
+        
+        # Check to see if this is a cancel of the entire event
+        if iTipProcessing.processReply(self.message, self.recipient_calendar):
+ 
+            # Update the attendee's copy of the event
+            log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:REPLY, UID: '%s' - updating event" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
+            yield self.writeCalendarResource(self.recipient_calendar_collection_uri, self.recipient_calendar_collection, self.recipient_calendar_name, self.recipient_calendar)
+            result = (True, False,)
+            
+            # TODO: Send out a request to all attendees to update them
+
+        else:
+            # Ignore scheduling message
+            result = (True, True,)
+
+        returnValue(result)
+
+    @inlineCallbacks
     def doImplicitAttendee(self):
 
         # Locate the attendee's copy of the event if it exists.
@@ -141,8 +170,8 @@
         self.new_resource = self.recipient_calendar is None
 
         # Handle new items differently than existing ones.
-        if self.new_resource:
-            result = (False, False,)
+        if self.new_resource and self.method == "CANCEL":
+            result = (True, True,)
         else:
             result = (yield self.doImplicitAttendeeUpdate())
         
@@ -153,8 +182,7 @@
         
         # Different based on method
         if self.method == "REQUEST":
-            #result = (yield self.doImplicitAttendeRequest())
-            result = (False, False,)
+            result = (yield self.doImplicitAttendeRequest())
         elif self.method == "CANCEL":
             result = (yield self.doImplicitAttendeCancel())
         elif self.method == "ADD":
@@ -164,29 +192,112 @@
         returnValue(result)
 
     @inlineCallbacks
+    def doImplicitAttendeRequest(self):
+
+        # If there is no existing copy, then look for default calendar and copy it here
+        if self.new_resource:
+            
+            # Check for default calendar
+            default = (yield self.recipient.inbox.readProperty((caldav_namespace, "schedule-default-calendar-URL"), self.request))
+            if len(default.children) == 1:
+                defaultURL = str(default.children[0])
+                default = (yield self.request.locateResource(defaultURL))
+            else:
+                default = None
+
+            if default:
+                log.debug("ImplicitProcessing - originator '%s' to recipient '%s' ignoring METHOD:REQUEST, UID: '%s' - new processed" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
+                new_calendar = iTipProcessing.processNewRequest(self.message)
+                name =  md5(str(new_calendar) + str(time.time()) + default.fp.path).hexdigest() + ".ics"
+                yield self.writeCalendarResource(defaultURL, default, name, new_calendar)
+                result = (True, False,)
+            else:
+                log.debug("ImplicitProcessing - originator '%s' to recipient '%s' ignoring METHOD:REQUEST, UID: '%s' - new not processed" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
+                result = (False, False,)
+        else:
+            # Processing update to existing event
+            if iTipProcessing.processRequest(self.message, self.recipient_calendar):
+     
+                # Update the attendee's copy of the event
+                log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:REQUEST, UID: '%s' - updating event" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
+                yield self.writeCalendarResource(self.recipient_calendar_collection_uri, self.recipient_calendar_collection, self.recipient_calendar_name, self.recipient_calendar)
+                result = (True, False,)
+                
+            else:
+                # Request needs to be ignored
+                log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:REQUEST, UID: '%s' - ignoring" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
+                result = (True, True,)
+
+        returnValue(result)
+
+
+    @inlineCallbacks
     def doImplicitAttendeCancel(self):
 
         # If there is no existing copy, then ignore
         if self.recipient_calendar is None:
-            log.debug("Implicit - originator '%s' to recipient '%s' ignoring METHOD:CANCEL, UID: '%s' - attendee has no copy" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
+            log.debug("ImplicitProcessing - originator '%s' to recipient '%s' ignoring METHOD:CANCEL, UID: '%s' - attendee has no copy" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
             result = (True, True,)
         else:
             # Check to see if this is a cancel of the entire event
-            if self.message.masterComponent() is not None:
-                
-                # Delete the attendee's copy of the event
-                log.debug("Implicit - originator '%s' to recipient '%s' processing METHOD:CANCEL, UID: '%s' - deleting entire event" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
-                self.deleteCalendarResource(self.recipient_calendar_collection, self.recipient_calendar_name)
-                result = (True, False,)
-     
+            processed_message, delete_original = iTipProcessing.processCancel(self.message, self.recipient_calendar)
+            if processed_message:
+                if delete_original:
+                    
+                    # Delete the attendee's copy of the event
+                    log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:CANCEL, UID: '%s' - deleting entire event" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
+                    yield self.deleteCalendarResource(self.recipient_calendar_collection, self.recipient_calendar_name)
+                    result = (True, False,)
+                    
+                else:
+         
+                    # Update the attendee's copy of the event
+                    log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:CANCEL, UID: '%s' - updating event" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
+                    yield self.writeCalendarResource(self.recipient_calendar_collection_uri, self.recipient_calendar_collection, self.recipient_calendar_name, self.recipient_calendar)
+                    result = (True, False,)
             else:
-                # Get each cancelled Recurrence-ID and exclude those from the existing event
-                
-                result = (False, False,)
+                log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:CANCEL, UID: '%s' - ignoring" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
+                result = (True, True,)
 
         returnValue(result)
 
     @inlineCallbacks
+    def writeCalendarResource(self, collURL, collection, name, calendar):
+        """
+        Write out the calendar resource (iTIP) message to the specified calendar, either over-writing the named
+        resource or by creating a new one.
+        
+        @param collURL: the C{str} containing the URL of the calendar collection.
+        @param collection: the L{CalDAVFile} for the calendar collection to store the resource in.
+        @param name: the C{str} for the resource name to write into, or {None} to write a new resource.
+        @param calendar: the L{Component} calendar to write.
+        @return: L{Deferred} -> L{CalDAVFile}
+        """
+        
+        # Create a new name if one was not provided
+        if name is None:
+            name =  md5(str(calendar) + str(time.time()) + collection.fp.path).hexdigest() + ".ics"
+    
+        # Get a resource for the new item
+        newchildURL = joinURL(collURL, name)
+        newchild = yield self.request.locateResource(newchildURL)
+        
+        # Now write it to the resource
+        from twistedcaldav.method.put_common import StoreCalendarObjectResource
+        yield StoreCalendarObjectResource(
+                     request=self.request,
+                     destination = newchild,
+                     destination_uri = newchildURL,
+                     destinationparent = collection,
+                     destinationcal = True,
+                     calendar = calendar,
+                     isiTIP = False,
+                     allowImplicitSchedule = False
+                 ).run()
+    
+        returnValue(newchild)
+
+    @inlineCallbacks
     def deleteCalendarResource(self, collection, name):
         """
         Delete the calendar resource in the specified calendar.
@@ -204,4 +315,3 @@
         
         # Change CTag on the parent calendar collection
         yield collection.updateCTag()
-    

Added: CalendarServer/branches/users/cdaboo/implicit-2805/twistedcaldav/scheduling/test/test_implicit.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2805/twistedcaldav/scheduling/test/test_implicit.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/implicit-2805/twistedcaldav/scheduling/test/test_implicit.py	2008-08-18 19:52:49 UTC (rev 2831)
@@ -0,0 +1,762 @@
+##
+# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+from twistedcaldav.scheduling.icaldiff import iCalDiff
+
+from twistedcaldav.ical import Component
+import twistedcaldav.test.util
+from twistedcaldav.scheduling.implicit import ImplicitScheduler
+from dateutil.tz import tzutc
+import datetime
+
+class Implicit (twistedcaldav.test.util.TestCase):
+    """
+    iCalendar support tests
+    """
+
+    def test_removed_attendees(self):
+        
+        data = (
+            (
+                "#1.1 Simple component, no change",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                (),
+            ),
+            (
+                "#1.2 Simple component, one removal",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                (("mailto:user2 at example.com", None),),
+            ),
+            (
+                "#1.3 Simple component, two removals",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                (
+                    ("mailto:user2 at example.com", None),
+                    ("mailto:user3 at example.com", None),
+                ),
+            ),
+            (
+                "#2.1 Simple recurring component, two removals",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+END:VCALENDAR
+""",
+                (
+                    ("mailto:user2 at example.com", None),
+                    ("mailto:user3 at example.com", None),
+                ),
+            ),
+            (
+                "#2.2 Simple recurring component, add exdate",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=MONTHLY
+EXDATE:20080801T120000Z
+END:VEVENT
+END:VCALENDAR
+""",
+                (
+                    ("mailto:user1 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
+                    ("mailto:user2 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
+                    ("mailto:user3 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
+                ),
+            ),
+            (
+                "#2.3 Simple recurring component, add multiple comma exdates",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=MONTHLY
+EXDATE:20080801T120000Z,20080901T120000Z
+END:VEVENT
+END:VCALENDAR
+""",
+                (
+                    ("mailto:user1 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
+                    ("mailto:user2 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
+                    ("mailto:user3 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
+                    ("mailto:user1 at example.com", datetime.datetime(2008, 9, 1, 12, 0, 0, tzinfo=tzutc())),
+                    ("mailto:user2 at example.com", datetime.datetime(2008, 9, 1, 12, 0, 0, tzinfo=tzutc())),
+                    ("mailto:user3 at example.com", datetime.datetime(2008, 9, 1, 12, 0, 0, tzinfo=tzutc())),
+                ),
+            ),
+            (
+                "#2.3 Simple recurring component, add multiple comma/property exdates",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=MONTHLY
+EXDATE:20080801T120000Z,20080901T120000Z
+EXDATE:20081201T120000Z
+END:VEVENT
+END:VCALENDAR
+""",
+                (
+                    ("mailto:user1 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
+                    ("mailto:user2 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
+                    ("mailto:user3 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
+                    ("mailto:user1 at example.com", datetime.datetime(2008, 9, 1, 12, 0, 0, tzinfo=tzutc())),
+                    ("mailto:user2 at example.com", datetime.datetime(2008, 9, 1, 12, 0, 0, tzinfo=tzutc())),
+                    ("mailto:user3 at example.com", datetime.datetime(2008, 9, 1, 12, 0, 0, tzinfo=tzutc())),
+                    ("mailto:user1 at example.com", datetime.datetime(2008, 12, 1, 12, 0, 0, tzinfo=tzutc())),
+                    ("mailto:user2 at example.com", datetime.datetime(2008, 12, 1, 12, 0, 0, tzinfo=tzutc())),
+                    ("mailto:user3 at example.com", datetime.datetime(2008, 12, 1, 12, 0, 0, tzinfo=tzutc())),
+                ),
+            ),
+            (
+                "#3.1 Complex recurring component with same attendees, no change",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                (),
+            ),
+            (
+                "#3.2 Complex recurring component with same attendees, change master/override",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                (
+                    ("mailto:user3 at example.com", None),
+                    ("mailto:user3 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
+                ),
+            ),
+            (
+                "#3.3 Complex recurring component with same attendees, change override",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                (
+                    ("mailto:user3 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
+                ),
+            ),
+            (
+                "#3.4 Complex recurring component with same attendees, change master",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                (
+                    ("mailto:user3 at example.com", None),
+                ),
+            ),
+            (
+                "#3.5 Complex recurring component with same attendees, remove override - no exdate",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+END:VCALENDAR
+""",
+                (),
+            ),
+            (
+                "#3.6 Complex recurring component with same attendees, remove override - exdate",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=MONTHLY
+EXDATE:20080801T120000Z
+END:VEVENT
+END:VCALENDAR
+""",
+                (
+                    ("mailto:user1 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
+                    ("mailto:user2 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
+                    ("mailto:user3 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
+                ),
+            ),
+            (
+                "#4.1 Complex recurring component with different attendees, change master/override",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ATTENDEE:mailto:user4 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                (
+                    ("mailto:user3 at example.com", None),
+                    ("mailto:user4 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
+                ),
+            ),
+            (
+                "#4.2 Complex recurring component with different attendees, remove override - no exdate",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ATTENDEE:mailto:user4 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+END:VCALENDAR
+""",
+                (
+                    ("mailto:user4 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
+                ),
+            ),
+            (
+                "#4.3 Complex recurring component with different attendees, remove override - exdate",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ATTENDEE:mailto:user4 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=MONTHLY
+EXDATE:20080801T120000Z
+END:VEVENT
+END:VCALENDAR
+""",
+                (
+                    ("mailto:user1 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
+                    ("mailto:user2 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
+                    ("mailto:user4 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
+                ),
+            ),
+        )
+
+        for description, calendar1, calendar2, result in data:
+            scheduler = ImplicitScheduler()
+            scheduler.oldcalendar = Component.fromString(calendar1)
+            scheduler.calendar = Component.fromString(calendar2)
+            scheduler.extractCalendarData()
+            scheduler.findRemovedAttendees()
+#            if not description.startswith("#4.3"):
+#                continue
+#            print description
+#            print scheduler.cancelledAttendees
+#            print set(result)
+            self.assertEqual(scheduler.cancelledAttendees, set(result), msg=description)
+

Added: CalendarServer/branches/users/cdaboo/implicit-2805/twistedcaldav/scheduling/test/test_itip.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2805/twistedcaldav/scheduling/test/test_itip.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/implicit-2805/twistedcaldav/scheduling/test/test_itip.py	2008-08-18 19:52:49 UTC (rev 2831)
@@ -0,0 +1,580 @@
+##
+# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twistedcaldav.ical import Component
+import twistedcaldav.test.util
+from twistedcaldav.scheduling.itip import iTipProcessing
+
+class iTIP (twistedcaldav.test.util.TestCase):
+    """
+    iCalendar support tests
+    """
+
+    def test_update_attendee_partstat(self):
+        
+        data = (
+            (
+                "#1.1 Simple component, accepted",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+METHOD:REPLY
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+            ),
+            (
+                "#1.2 Simple component, accepted",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+METHOD:REPLY
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+            ),
+            (
+                "#1.3 Simple component, no change",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+METHOD:REPLY
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+            ),
+            (
+                "#2.1 Recurring component, change master/override",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+DTSTART:20080801T123000Z
+DTEND:20080801T133000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+METHOD:REPLY
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user1 at example.com
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE;PARTSTAT=DECLINED:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+DTSTART:20080801T123000Z
+DTEND:20080801T133000Z
+ATTENDEE;PARTSTAT=DECLINED:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+            ),
+            (
+                "#2.2 Recurring component, change master only",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+DTSTART:20080801T123000Z
+DTEND:20080801T133000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+METHOD:REPLY
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+DTSTART:20080801T123000Z
+DTEND:20080801T133000Z
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+            ),
+            (
+                "#2.3 Recurring component, change override only",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+DTSTART:20080801T123000Z
+DTEND:20080801T133000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+METHOD:REPLY
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE;PARTSTAT=DECLINED:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+DTSTART:20080801T123000Z
+DTEND:20080801T133000Z
+ATTENDEE;PARTSTAT=DECLINED:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+            ),
+            (
+                "#3.1 Recurring component, change master/override, new override",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+DTSTART:20080801T123000Z
+DTEND:20080801T133000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+METHOD:REPLY
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user1 at example.com
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE;PARTSTAT=DECLINED:mailto:user1 at example.com
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080901T120000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE;PARTSTAT=TENTATIVE:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+DTSTART:20080801T123000Z
+DTEND:20080801T133000Z
+ATTENDEE;PARTSTAT=DECLINED:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080901T120000Z
+DTSTART:20080901T120000Z
+DTEND:20080901T130000Z
+ATTENDEE;PARTSTAT=TENTATIVE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+            ),
+            (
+                "#3.2 Recurring component, change master, new override",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+DTSTART:20080801T123000Z
+DTEND:20080801T133000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+METHOD:REPLY
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user1 at example.com
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080901T120000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE;PARTSTAT=TENTATIVE:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+DTSTART:20080801T123000Z
+DTEND:20080801T133000Z
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080901T120000Z
+DTSTART:20080901T120000Z
+DTEND:20080901T130000Z
+ATTENDEE;PARTSTAT=TENTATIVE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+            ),
+            (
+                "#3.3 Recurring component, change override, new override",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+DTSTART:20080801T123000Z
+DTEND:20080801T133000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+METHOD:REPLY
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE;PARTSTAT=DECLINED:mailto:user1 at example.com
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080901T120000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE;PARTSTAT=TENTATIVE:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+DTSTART:20080801T123000Z
+DTEND:20080801T133000Z
+ATTENDEE;PARTSTAT=DECLINED:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080901T120000Z
+DTSTART:20080901T120000Z
+DTEND:20080901T130000Z
+ATTENDEE;PARTSTAT=TENTATIVE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+            ),
+        )
+
+        for description, calendar_txt, itipmsg_txt, result in data:
+            calendar = Component.fromString(calendar_txt)
+            itipmsg = Component.fromString(itipmsg_txt)
+            iTipProcessing.processReply(itipmsg, calendar)
+#            if not description.startswith("#3.1"):
+#                continue
+#            print description
+#            print str(calendar)
+#            print str(result)
+            self.assertEqual(str(calendar).replace("\r", ""), str(result), msg=description)
+
-------------- next part --------------
An HTML attachment was scrubbed...
URL: http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20080818/42e219c6/attachment-0001.html 


More information about the calendarserver-changes mailing list