[CalendarServer-changes] [3036] CalendarServer/trunk/twistedcaldav

source_changes at macosforge.org source_changes at macosforge.org
Tue Sep 23 19:45:19 PDT 2008


Revision: 3036
          http://trac.macosforge.org/projects/calendarserver/changeset/3036
Author:   cdaboo at apple.com
Date:     2008-09-23 19:45:12 -0700 (Tue, 23 Sep 2008)
Log Message:
-----------
Provide indication to client about what changed when an invite was auto-processed.

Modified Paths:
--------------
    CalendarServer/trunk/twistedcaldav/customxml.py
    CalendarServer/trunk/twistedcaldav/ical.py
    CalendarServer/trunk/twistedcaldav/scheduling/caldav.py
    CalendarServer/trunk/twistedcaldav/scheduling/icaldiff.py
    CalendarServer/trunk/twistedcaldav/scheduling/itip.py
    CalendarServer/trunk/twistedcaldav/scheduling/processing.py
    CalendarServer/trunk/twistedcaldav/scheduling/test/test_icaldiff.py

Modified: CalendarServer/trunk/twistedcaldav/customxml.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/customxml.py	2008-09-23 20:59:01 UTC (rev 3035)
+++ CalendarServer/trunk/twistedcaldav/customxml.py	2008-09-24 02:45:12 UTC (rev 3036)
@@ -28,6 +28,11 @@
 
 from twistedcaldav.ical import Component as iComponent
 
+from vobject.icalendar import utc
+from vobject.icalendar import dateTimeToString
+
+import datetime
+
 calendarserver_namespace = "http://calendarserver.org/ns/"
 
 calendarserver_proxy_compliance = (
@@ -330,7 +335,202 @@
     namespace = calendarserver_namespace
     name = "free-busy-url"
 
+class ScheduleChanges (davxml.WebDAVElement):
+    """
+    Change indicator for a scheduling message.
+    """
+    namespace = calendarserver_namespace
+    name = "schedule-changes"
+    protected = True
+    hidden = True
+    allowed_children = {
+        (calendarserver_namespace, "dtstamp" )     : (0, 1), # Have to allow 0 as element is empty in PROPFIND requests
+        (calendarserver_namespace, "action" )      : (0, 1), # Have to allow 0 as element is empty in PROPFIND requests
+    }
 
+class DTStamp (davxml.WebDAVTextElement):
+    """
+    A UTC timestamp in iCal format.
+    """
+    namespace = calendarserver_namespace
+    name = "dtstamp"
+
+    def __init__(self, *children):
+        super(DTStamp, self).__init__(children)
+        self.children = (davxml.PCDATAElement(dateTimeToString(datetime.datetime.now(tz=utc))),)
+
+class Action (davxml.WebDAVElement):
+    """
+    A UTC timestamp in iCal format.
+    """
+    namespace = calendarserver_namespace
+    name = "action"
+    allowed_children = {
+        (calendarserver_namespace, "create" ) : (0, 1),
+        (calendarserver_namespace, "update" ) : (0, 1),
+        (calendarserver_namespace, "cancel" ) : (0, 1),
+        (calendarserver_namespace, "reply" )  : (0, 1),
+    }
+
+class Create (davxml.WebDAVEmptyElement):
+    """
+    Event created.
+    """
+    namespace = calendarserver_namespace
+    name = "create"
+
+class Update (davxml.WebDAVElement):
+    """
+    Event updated.
+    """
+    namespace = calendarserver_namespace
+    name = "update"
+    allowed_children = {
+        (calendarserver_namespace, "changes" )     : (1, 1),
+        (calendarserver_namespace, "recurrences" ) : (0, 1),
+    }
+
+class Cancel (davxml.WebDAVElement):
+    """
+    Event cancelled.
+    """
+    namespace = calendarserver_namespace
+    name = "cancel"
+    allowed_children = {
+        (calendarserver_namespace, "recurrences" ) : (0, 1),
+    }
+
+class Reply (davxml.WebDAVElement):
+    """
+    Event replied to.
+    """
+    namespace = calendarserver_namespace
+    name = "reply"
+    allowed_children = {
+        (calendarserver_namespace, "attendee" )        : (1, 1),
+        (calendarserver_namespace, "partstat" )        : (0, 1),
+        (calendarserver_namespace, "private-comment" ) : (0, 1),
+    }
+
+class Attendee (davxml.WebDAVTextElement):
+    """
+    An attendee calendar user address.
+    """
+    namespace = calendarserver_namespace
+    name = "attendee"
+
+class PartStat (davxml.WebDAVEmptyElement):
+    """
+    An attendee partstat.
+    """
+    namespace = calendarserver_namespace
+    name = "partstat"
+
+class PrivateComment (davxml.WebDAVEmptyElement):
+    """
+    An attendee private comment.
+    """
+    namespace = calendarserver_namespace
+    name = "private-comment"
+
+class Changes (davxml.WebDAVElement):
+    """
+    Changes to an event.
+    """
+    namespace = calendarserver_namespace
+    name = "changes"
+    allowed_children = {
+        (calendarserver_namespace, "datetime" )          : (0, 1),
+        (calendarserver_namespace, "location" )          : (0, 1),
+        (calendarserver_namespace, "summary" )           : (0, 1),
+        (calendarserver_namespace, "description" )       : (0, 1),
+        (calendarserver_namespace, "recurrence" )        : (0, 1),
+        (calendarserver_namespace, "status" )            : (0, 1),
+        (calendarserver_namespace, "attendees" )         : (0, 1),
+        (calendarserver_namespace, "attendee-partstat" ) : (0, 1),
+    }
+
+class Datetime (davxml.WebDAVEmptyElement):
+    """
+    Date time change.
+    """
+    namespace = calendarserver_namespace
+    name = "datetime"
+
+class Location (davxml.WebDAVEmptyElement):
+    """
+    Location changed.
+    """
+    namespace = calendarserver_namespace
+    name = "location"
+
+class Summary (davxml.WebDAVEmptyElement):
+    """
+    Summary changed.
+    """
+    namespace = calendarserver_namespace
+    name = "summary"
+
+class Description (davxml.WebDAVEmptyElement):
+    """
+    Description changed.
+    """
+    namespace = calendarserver_namespace
+    name = "description"
+
+class Recurrence (davxml.WebDAVEmptyElement):
+    """
+    Recurrence changed.
+    """
+    namespace = calendarserver_namespace
+    name = "recurrence"
+
+class Status (davxml.WebDAVEmptyElement):
+    """
+    Status changed.
+    """
+    namespace = calendarserver_namespace
+    name = "status"
+
+class Attendees (davxml.WebDAVEmptyElement):
+    """
+    Attendees changed.
+    """
+    namespace = calendarserver_namespace
+    name = "attendees"
+
+class AttendeePartStat (davxml.WebDAVEmptyElement):
+    """
+    Attendee partstats changed.
+    """
+    namespace = calendarserver_namespace
+    name = "attendee-partstat"
+
+class Recurrences (davxml.WebDAVElement):
+    """
+    Changes to an event.
+    """
+    namespace = calendarserver_namespace
+    name = "recurrences"
+    allowed_children = {
+        (calendarserver_namespace, "master" )       : (0, 1),
+        (calendarserver_namespace, "recurrenceid" ) : (0, None),
+    }
+
+class Master (davxml.WebDAVEmptyElement):
+    """
+    Master instance changed.
+    """
+    namespace = calendarserver_namespace
+    name = "master"
+
+class RecurrenceID (davxml.WebDAVTextElement):
+    """
+    A recurrence instance changed.
+    """
+    namespace = calendarserver_namespace
+    name = "recurrenceid"
+
 ##
 # Extensions to davxml.ResourceType
 ##

Modified: CalendarServer/trunk/twistedcaldav/ical.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/ical.py	2008-09-23 20:59:01 UTC (rev 3035)
+++ CalendarServer/trunk/twistedcaldav/ical.py	2008-09-24 02:45:12 UTC (rev 3036)
@@ -834,6 +834,22 @@
             rid = self.getRecurrenceIDUTC()
             return (rid,)
 
+    def isRecurring(self):
+        """
+        Check whether any recurrence properties are present in any component.
+        """
+
+        # Extract appropriate sub-component if this is a VCALENDAR
+        if self.name() == "VCALENDAR":
+            for component in self.subcomponents():
+                if component.name() != "VTIMEZONE" and component.isRecurring():
+                    return True
+        else:
+            for propname in ("RRULE", "RDATE", "EXDATE", "RECUURENCE-ID",):
+                if self.hasProperty(propname):
+                    return True
+        return False
+        
     def deriveInstance(self, rid):
         """
         Derive an instance from the master component that has the provided RECURRENCE-ID, but

Modified: CalendarServer/trunk/twistedcaldav/scheduling/caldav.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/caldav.py	2008-09-23 20:59:01 UTC (rev 3035)
+++ CalendarServer/trunk/twistedcaldav/scheduling/caldav.py	2008-09-24 02:45:12 UTC (rev 3036)
@@ -152,7 +152,7 @@
         if not recipient.principal.autoSchedule():
             try:
                 processor = ImplicitProcessor()
-                processed, autoprocessed = (yield processor.doImplicitProcessing(
+                processed, autoprocessed, changes = (yield processor.doImplicitProcessing(
                     self.scheduler.request,
                     self.scheduler.calendar,
                     self.scheduler.originator,
@@ -165,6 +165,7 @@
                 returnValue(False)
         else:
             processed = autoprocessed = False
+            changes = None
 
         if autoprocessed:
             # No need to write the inbox item as it has already been auto-processed
@@ -201,6 +202,10 @@
                 # Store CALDAV:schedule-state property
                 child.writeDeadProperty(caldavxml.ScheduleState(caldavxml.ScheduleProcessed() if processed else caldavxml.ScheduleUnprocessed()))
             
+                # Store CS:schedule-changes property if present
+                if changes:
+                    child.writeDeadProperty(changes)
+            
                 # Look for auto-schedule option
                 if recipient.principal.autoSchedule():
                     autoresponses.append((recipient.principal, recipient.inbox, child))

Modified: CalendarServer/trunk/twistedcaldav/scheduling/icaldiff.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/icaldiff.py	2008-09-23 20:59:01 UTC (rev 3035)
+++ CalendarServer/trunk/twistedcaldav/scheduling/icaldiff.py	2008-09-24 02:45:12 UTC (rev 3036)
@@ -18,6 +18,8 @@
 from twistedcaldav.log import Logger
 from twistedcaldav.scheduling.itip import iTipGenerator
 
+from vobject.icalendar import dateTimeToString
+
 """
 Class that handles diff'ing two calendar objects.
 """
@@ -113,6 +115,58 @@
         # component instance from the old one
         return self._compareComponents()
     
+    def whatIsDifferent(self):
+        """
+        Compare the two calendar objects in their entirety and return a list of properties
+        and PARTSTAT parameters that are different.
+        """
+
+        # First get uid/rid map of components
+        def mapComponents(calendar):
+            map = {}
+            for component in calendar.subcomponents():
+                if component.name() == "VTIMEZONE":
+                    continue
+                name = component.name()
+                uid = component.propertyValue("UID")
+                rid = component.getRecurrenceIDUTC()
+                map[(name, uid, rid,)] = component
+            return map
+        
+        props_changed = set()
+        rids = set()
+
+        map1 = mapComponents(self.calendar1)
+        set1 = set(map1.keys())
+        map2 = mapComponents(self.calendar2)
+        set2 = set(map2.keys())
+
+        # Now verify that each component in set1 matches what is in set2
+        for key in (set1 & set2):
+            component1 = map1[key]
+            component2 = map2[key]
+            self._diffComponents(component1, component2, props_changed, rids)
+        
+        # Now verify that each additional component in set1 matches a derived component in set2
+        for key in set1 - set2:
+            component1 = map1[key]
+            component2 = self.calendar2.deriveInstance(key[2])
+            if component2 is None:
+                continue
+            self._diffComponents(component1, component2, props_changed, rids)
+        
+        # Now verify that each additional component in set1 matches a derived component in set2
+        for key in set2 - set1:
+            component1 = self.calendar1.deriveInstance(key[2])
+            if component1 is None:
+                continue
+            component2 = map2[key]
+            self._diffComponents(component1, component2, props_changed, rids)
+        
+        if not self.calendar1.isRecurring() and not self.calendar2.isRecurring() or not props_changed:
+            rids = None
+        return props_changed, rids
+
     def _checkVCALENDARProperties(self):
 
         # Get property differences in the VCALENDAR objects
@@ -243,3 +297,41 @@
             return False, False
         
         return True, len(propdiff) == 0
+
+    def _diffComponents(self, comp1, comp2, changed, rids):
+        
+        assert isinstance(comp1, Component) and isinstance(comp2, Component)
+        
+        if comp1.name() != comp2.name():
+            log.debug("Component names are different: '%s' and '%s'" % (comp1.name(), comp2.name()))
+            return
+        
+        # Diff all the properties
+        comp1.transformAllFromNative()
+        comp2.transformAllFromNative()
+        propdiff = set(comp1.properties()) ^ set(comp2.properties())
+        comp1.transformAllToNative()
+        comp2.transformAllToNative()
+        
+        regular_changes = [prop.name() for prop in propdiff if prop.name() != "ATTENDEE"]
+        changed.update(regular_changes)
+        
+        attendees = set([prop for prop in propdiff if prop.name() == "ATTENDEE"])
+        done_attendee = ("ATTENDEE" in changed)
+        done_partstat = ("PARTSTAT" in changed)
+        for ctr, attendee in enumerate(attendees):
+            for check_ctr, check_attendee in enumerate(attendees):
+                if (ctr != check_ctr) and check_attendee.value() == attendee.value():
+                    if check_attendee.params().get("PARTSTAT", ("NEEDS-ACTION",)) != attendee.params().get("PARTSTAT", ("NEEDS-ACTION",)):
+                        changed.add("PARTSTAT")
+                        done_partstat = True
+                    break
+            else:
+                changed.add("ATTENDEE")
+                done_attendee = True
+            if done_attendee and done_partstat:
+                break
+
+        if regular_changes or done_attendee or done_partstat:
+            rid = comp1.getRecurrenceIDUTC()
+            rids.add(dateTimeToString(rid) if rid is not None else "")

Modified: CalendarServer/trunk/twistedcaldav/scheduling/itip.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/itip.py	2008-09-23 20:59:01 UTC (rev 3035)
+++ CalendarServer/trunk/twistedcaldav/scheduling/itip.py	2008-09-24 02:45:12 UTC (rev 3036)
@@ -82,11 +82,16 @@
         @param calendar: the calendar object to apply the REQUEST to
         @type calendar:
         
-        @return: calendar object ready to save, or C{None} (request should be ignored)
+        @return: a C{tuple} of:
+            calendar object ready to save, or C{None} (request should be ignored)
+            a C{set} of iCalendar properties that changed, or C{None},
+            a C{set} of recurrences that changed, or C{None}
         """
         
         # Merge Organizer data with Attendee's own changes (VALARMs only for now).
-        
+        from twistedcaldav.scheduling.icaldiff import iCalDiff
+        props_changed, rids = iCalDiff(calendar, itip_message).whatIsDifferent()
+
         # Different behavior depending on whether a master component is present or not
         current_master = calendar.masterComponent()
         if current_master:
@@ -110,7 +115,7 @@
                     iTipProcessing.transferAlarms(calendar, master_valarms, component)
             
             # Replace the entire object
-            return new_calendar
+            return new_calendar, props_changed, rids
 
         else:
             # Need existing tzids
@@ -129,7 +134,7 @@
                         iTipProcessing.fixForiCal3((component,), recipient)
 
             # Write back the modified object
-            return calendar
+            return calendar, props_changed, rids
 
     @staticmethod
     def processCancel(itip_message, calendar):
@@ -146,6 +151,7 @@
         @return: C{tuple} of:
             C{bool} : C{True} if processed, C{False} if scheduling message should be ignored
             C{bool} : C{True} if calendar object should be deleted, C{False} otherwise
+            C{set}  : set of Recurrence-IDs for cancelled instances, or C{None} if all cancelled
         """
         
         assert itip_message.propertyValue("METHOD") == "CANCEL", "iTIP message must have METHOD:CANCEL"
@@ -153,7 +159,7 @@
 
         # Check to see if this is a cancel of the entire event
         if itip_message.masterComponent() is not None:
-            return True, True
+            return True, True, None
 
         # iTIP CANCEL can contain multiple components being cancelled in the RECURRENCE-ID case.
         # So we need to iterate over each iTIP component.
@@ -161,6 +167,7 @@
         # Get the existing calendar master object if it exists
         calendar_master = calendar.masterComponent()
         exdates = []
+        rids = set()
 
         # Look at each component in the iTIP message
         for component in itip_message.subcomponents():
@@ -169,6 +176,7 @@
         
             # Extract RECURRENCE-ID value from component
             rid = component.getRecurrenceIDUTC()
+            rids.add(rid)
             
             # Get the one that matches in the calendar
             overridden = calendar.overriddenComponent(rid)
@@ -195,9 +203,9 @@
         # 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
+            return True, True, None
         else:
-            return True, False
+            return True, False, rids
     
     @staticmethod
     def processReply(itip_message, calendar):
@@ -211,7 +219,9 @@
         @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
+        @return: a C{tuple} of:
+            C{True} if processed, C{False} if scheduling message should be ignored
+            C{tuple} of change info
         """
         
         assert itip_message.propertyValue("METHOD") == "REPLY", "iTIP message must have METHOD:REPLY"
@@ -225,8 +235,16 @@
         old_master = calendar.masterComponent()
         new_master = itip_message.masterComponent()
         attendees = set()
+        partstat_changed = False
+        private_comment_changed = False
+        rids = set() if old_master.isRecurring() else None
         if new_master:
-            attendees.add(iTipProcessing.updateAttendeeData(new_master, old_master))
+            attendee, partstat, private_comment = iTipProcessing.updateAttendeeData(new_master, old_master)
+            attendees.add(attendee)
+            partstat_changed = partstat_changed or partstat
+            private_comment_changed = private_comment_changed or private_comment
+            if rids is not None:
+                rids.add("")
 
         # Now do all overridden ones
         for itip_component in itip_message.subcomponents():
@@ -246,10 +264,15 @@
                 match_component = calendar.deriveInstance(rid)
                 calendar.addComponent(match_component)
 
-            attendees.add(iTipProcessing.updateAttendeeData(itip_component, match_component))
-                
-        return True, attendees
+            attendee, partstat, private_comment = iTipProcessing.updateAttendeeData(itip_component, match_component)
+            attendees.add(attendee)
+            partstat_changed = partstat_changed or partstat
+            private_comment_changed = private_comment_changed or private_comment
+            if rids is not None:
+                rids.add(rid)
 
+        return True, (attendees, partstat_changed, private_comment_changed, rids)
+
     @staticmethod
     def updateAttendeeData(from_component, to_component):
         """
@@ -262,6 +285,10 @@
         @type to_component:
         """
         
+        # Track what changed
+        partstat_changed = False
+        private_comment_changed = False
+
         # 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"
@@ -271,7 +298,9 @@
         # Now find matching ATTENDEE in to_component
         existing_attendee = to_component.getAttendeeProperty((attendee.value(),))
         if existing_attendee:
+            oldpartstat = existing_attendee.params().get("PARTSTAT", ("NEEDS-ACTION",))[0]
             existing_attendee.params().setdefault("PARTSTAT", [partstat])[0] = partstat
+            partstat_changed = (oldpartstat != partstat)
             
             # Handle attendee comments
             
@@ -307,6 +336,8 @@
                 # Set value empty
                 private_comment.setValue("")
                 
+                private_comment_changed = True
+                
             elif attendee_comment is not None and private_comment is None:
                 
                 # Add new property
@@ -319,6 +350,8 @@
                     }
                 )
                 to_component.addProperty(private_comment)
+                
+                private_comment_changed = True
             
             else:
                 # Remove all property parameters
@@ -329,10 +362,13 @@
                 private_comment.params()["X-CALENDARSERVER-DTSTAMP"] = [dateTimeToString(datetime.datetime.now(tz=utc))]
                 
                 # Set new value
+                oldvalue = private_comment.value()
                 private_comment.setValue(attendee_comment.value())
 
-        return attendee.value()
+                private_comment_changed = (oldvalue != attendee_comment.value())
 
+        return attendee.value(), partstat_changed, private_comment_changed
+
     @staticmethod
     def transferAlarms(from_calendar, master_valarms, to_component, remove_matched=False):
 

Modified: CalendarServer/trunk/twistedcaldav/scheduling/processing.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/processing.py	2008-09-23 20:59:01 UTC (rev 3035)
+++ CalendarServer/trunk/twistedcaldav/scheduling/processing.py	2008-09-24 02:45:12 UTC (rev 3036)
@@ -22,6 +22,7 @@
 from hashlib import md5
 from twisted.web2.dav.util import joinURL
 from twistedcaldav.caldavxml import caldav_namespace
+from twistedcaldav import customxml
 import time
 
 __all__ = [
@@ -132,7 +133,7 @@
         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,))
+            returnValue((True, True, None,))
 
         # Handle new items differently than existing ones.
         if self.method == "REPLY":
@@ -140,7 +141,7 @@
         elif self.method == "REFRESH":
             # With implicit we ignore refreshes.
             # TODO: for iMIP etc we do need to handle them 
-            result = (True, True,)
+            result = (True, True, None,)
 
         returnValue(result)
 
@@ -148,19 +149,41 @@
     def doImplicitOrganizerUpdate(self):
         
         # Check to see if this is a valid reply
-        result, processed_attendees = iTipProcessing.processReply(self.message, self.recipient_calendar)
+        result, processed = iTipProcessing.processReply(self.message, self.recipient_calendar)
         if result:
  
             # 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))
             recipient_calendar_resource = (yield self.writeCalendarResource(self.recipient_calendar_collection_uri, self.recipient_calendar_collection, self.recipient_calendar_name, self.recipient_calendar))
-            result = (True, False,)
             
+            # Build the schedule-changes XML element
+            processed_attendees, partstat_changed, private_comment_changed, rids = processed
+            reply_details = (customxml.Attendee.fromString(tuple(processed_attendees)[0]),)
+            if partstat_changed:
+                reply_details += (customxml.PartStat(),)
+            if private_comment_changed:
+                reply_details += (customxml.PrivateComment(),)
+            if rids is not None:
+                recurrences = []
+                if "" in rids:
+                    recurrences.append(customxml.Master())
+                recurrences.extend([customxml.RecurrenceID.fromString(rid) for rid in rids if rid != ""])
+                reply_details += (customxml.Recurrences(*recurrences),)
+
+            changes = customxml.ScheduleChanges(
+                customxml.DTStamp(),
+                customxml.Action(
+                    customxml.Reply(*reply_details),
+                ),
+            )
+
             self.updateAllAttendeesExceptSome(recipient_calendar_resource, processed_attendees)
 
+            result = (True, False, changes,)
+
         else:
             # Ignore scheduling message
-            result = (True, True,)
+            result = (True, True, None,)
 
         returnValue(result)
 
@@ -186,7 +209,7 @@
 
         # Handle new items differently than existing ones.
         if self.new_resource and self.method == "CANCEL":
-            result = (True, True,)
+            result = (True, True, None)
         else:
             result = (yield self.doImplicitAttendeeUpdate())
         
@@ -197,17 +220,17 @@
         
         # Different based on method
         if self.method == "REQUEST":
-            result = (yield self.doImplicitAttendeRequest())
+            result = (yield self.doImplicitAttendeeRequest())
         elif self.method == "CANCEL":
-            result = (yield self.doImplicitAttendeCancel())
+            result = (yield self.doImplicitAttendeeCancel())
         elif self.method == "ADD":
             # TODO: implement ADD
-            result = (False, False,)
+            result = (False, False, None)
             
         returnValue(result)
 
     @inlineCallbacks
-    def doImplicitAttendeRequest(self):
+    def doImplicitAttendeeRequest(self):
 
         # If there is no existing copy, then look for default calendar and copy it here
         if self.new_resource:
@@ -225,55 +248,116 @@
                 new_calendar = iTipProcessing.processNewRequest(self.message, self.recipient.cuaddr)
                 name =  md5(str(new_calendar) + str(time.time()) + default.fp.path).hexdigest() + ".ics"
                 yield self.writeCalendarResource(defaultURL, default, name, new_calendar)
-                result = (True, False,)
+                
+                # Build the schedule-changes XML element
+                changes = customxml.ScheduleChanges(
+                    customxml.DTStamp(),
+                    customxml.Action(
+                        customxml.Create(),
+                    ),
+                )
+                result = (True, False, changes,)
             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,)
+                result = (False, False, None,)
         else:
             # Processing update to existing event
-            new_calendar = iTipProcessing.processRequest(self.message, self.recipient_calendar, self.recipient.cuaddr)
+            new_calendar, props_changed, rids = iTipProcessing.processRequest(self.message, self.recipient_calendar, self.recipient.cuaddr)
             if new_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, new_calendar)
-                result = (True, False,)
+
+                # Build the schedule-changes XML element
+                changes = ()
+                if props_changed:
+                    changemap = {
+                        "DTSTART"     : customxml.Datetime(),
+                        "DTEND"       : customxml.Datetime(),
+                        "DURATION"    : customxml.Datetime(),
+                        "DUE"         : customxml.Datetime(),
+                        "COMPLETED"   : customxml.Datetime(),
+                        "LOCATION"    : customxml.Location(),
+                        "SUMMARY"     : customxml.Summary(),
+                        "DESCRIPTION" : customxml.Description(),
+                        "RRULE"       : customxml.Recurrence(),
+                        "RDATE"       : customxml.Recurrence(),
+                        "EXDATE"      : customxml.Recurrence(),
+                        "STATUS"      : customxml.Status(),
+                        "ATTENDEE"    : customxml.Attendees(),
+                        "PARTSTAT"    : customxml.PartStat(),
+                    }
+                    changes += tuple([changemap[prop] for prop in props_changed if prop in changemap])
+                update_details = (customxml.Changes(*changes),)
+                if rids is not None:
+                    recurrences = []
+                    if "" in rids:
+                        recurrences.append(customxml.Master())
+                    recurrences.extend([customxml.RecurrenceID.fromString(rid) for rid in rids if rid != ""])
+                    update_details += (customxml.Recurrences(*recurrences),)
+                changes = customxml.ScheduleChanges(
+                    customxml.DTStamp(),
+                    customxml.Action(
+                        customxml.Update(*update_details),
+                    ),
+                )
+                result = (True, False, changes,)
                 
             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,)
+                result = (True, True, None,)
 
         returnValue(result)
 
 
     @inlineCallbacks
-    def doImplicitAttendeCancel(self):
+    def doImplicitAttendeeCancel(self):
 
         # If there is no existing copy, then ignore
         if self.recipient_calendar is None:
             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,)
+            result = (True, True, None)
         else:
             # Check to see if this is a cancel of the entire event
-            processed_message, delete_original = iTipProcessing.processCancel(self.message, self.recipient_calendar)
+            processed_message, delete_original, rids = 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,)
+
+                    # Build the schedule-changes XML element
+                    changes = customxml.ScheduleChanges(
+                        customxml.DTStamp(),
+                        customxml.Action(
+                            customxml.Cancel(),
+                        ),
+                    )
+                    result = (True, False, changes,)
                     
                 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,)
+
+                    # Build the schedule-changes XML element
+                    changes = customxml.ScheduleChanges(
+                        customxml.DTStamp(),
+                        customxml.Action(
+                            customxml.Cancel(),
+                            customxml.Recurrences(
+                                *[customxml.RecurrenceID.fromString(rid) for rid in rids]
+                            ),
+                        ),
+                    )
+                    result = (True, False, changes)
             else:
                 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,)
+                result = (True, True, None)
 
         returnValue(result)
 

Modified: CalendarServer/trunk/twistedcaldav/scheduling/test/test_icaldiff.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/test/test_icaldiff.py	2008-09-23 20:59:01 UTC (rev 3035)
+++ CalendarServer/trunk/twistedcaldav/scheduling/test/test_icaldiff.py	2008-09-24 02:45:12 UTC (rev 3036)
@@ -17,6 +17,7 @@
 
 from twistedcaldav.ical import Component
 import twistedcaldav.test.util
+import itertools
 
 class ICalDiff (twistedcaldav.test.util.TestCase):
     """
@@ -1163,3 +1164,910 @@
         for description, calendar1, calendar2, attendee, result in data:
             differ = iCalDiff(Component.fromString(calendar1), Component.fromString(calendar2))
             self.assertEqual(differ.attendeeMerge(attendee), result, msg=description)
+
+    def test_what_is_different(self):
+        
+        data1 = (
+            (
+                "#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
+SUMMARY:Test
+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
+SUMMARY:Test
+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 property change",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+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
+SUMMARY:Test1
+END:VEVENT
+END:VCALENDAR
+""",
+                ("SUMMARY",),
+                (),
+            ),
+            (
+                "#1.3 Simple component, one property change, one addition, one removal",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+LOCATION:The Office
+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
+SUMMARY:Test1
+DESCRIPTION:Something to do.
+END:VEVENT
+END:VCALENDAR
+""",
+                ("SUMMARY", "LOCATION", "DESCRIPTION",),
+                (),
+            ),
+            (
+                "#1.4 Simple component, add attendee",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+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
+SUMMARY:Test
+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
+""",
+                ("ATTENDEE",),
+                (),
+            ),
+            (
+                "#1.5 Simple component, remove attendee",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+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
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                ("ATTENDEE",),
+                (),
+            ),
+            (
+                "#1.6 Simple component, attendee PARTSTAT only",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+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
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT="ACCEPTED":mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                ("PARTSTAT",),
+                (),
+            ),
+            (
+                "#1.7 Simple component, attendee PARTSTAT and addition",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+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
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT="ACCEPTED":mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                ("PARTSTAT", "ATTENDEE",),
+                (),
+            ),
+            (
+                "#1.8 Simple component, attendee RSVP only",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+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
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                (),
+                (),
+            ),
+        )
+        
+        data2 = (
+            (
+                "#2.1 Simple recurring component, no change",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+RRULE:FREQ=DAILY
+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
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+                (),
+                (),
+            ),
+            (
+                "#2.2 Simple component, one property change",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+RRULE:FREQ=DAILY
+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
+SUMMARY:Test1
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+                ("SUMMARY",),
+                ("",),
+            ),
+            (
+                "#2.3 Simple component, one property change, one addition, one removal",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+LOCATION:The Office
+RRULE:FREQ=DAILY
+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
+SUMMARY:Test1
+DESCRIPTION:Something to do.
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+                ("SUMMARY", "LOCATION", "DESCRIPTION",),
+                ("",),
+            ),
+            (
+                "#2.4 Simple component, add attendee",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+RRULE:FREQ=DAILY
+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
+SUMMARY:Test
+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=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+                ("ATTENDEE",),
+                ("",),
+            ),
+            (
+                "#2.5 Simple component, remove attendee",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+RRULE:FREQ=DAILY
+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
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+                ("ATTENDEE",),
+                ("",),
+            ),
+            (
+                "#2.6 Simple component, attendee PARTSTAT only",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+RRULE:FREQ=DAILY
+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
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT="ACCEPTED":mailto:user2 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+                ("PARTSTAT",),
+                ("",),
+            ),
+            (
+                "#2.7 Simple component, attendee PARTSTAT and addition",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+RRULE:FREQ=DAILY
+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
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT="ACCEPTED":mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+                ("PARTSTAT", "ATTENDEE",),
+                ("",),
+            ),
+        )
+        
+        data3 = (
+            (
+                "#3.1 Complex recurring component, no change",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+SUMMARY:Test
+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
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+SUMMARY:Test
+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
+""",
+                (),
+                (),
+            ),
+            (
+                "#3.2 Simple component, one property change in instance",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+SUMMARY:Test
+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
+SUMMARY:Test
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+SUMMARY:Test1
+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
+""",
+                ("SUMMARY",),
+                ("20080602T120000Z",),
+            ),
+            (
+                "#3.3 Simple component, one property change in master",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+SUMMARY:Test
+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
+SUMMARY:Test1
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+SUMMARY:Test
+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
+""",
+                ("SUMMARY",),
+                ("",),
+            ),
+            (
+                "#3.4 Simple component, one property change in master and instance",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+SUMMARY:Test
+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
+SUMMARY:Test1
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+SUMMARY:Test2
+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
+""",
+                ("SUMMARY",),
+                ("", "20080602T120000Z",),
+            ),
+            (
+                "#3.5 Simple component, different property change in master and instance",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+SUMMARY:Test
+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
+SUMMARY:Test1
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+SUMMARY:Test
+Description:Instance
+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
+""",
+                ("SUMMARY", "DESCRIPTION"),
+                ("", "20080602T120000Z",),
+            ),
+            (
+                "#3.6 Simple component, instance added no change",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+RRULE:FREQ=DAILY
+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
+SUMMARY:Test
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+SUMMARY:Test
+END:VEVENT
+END:VCALENDAR
+""",
+                (),
+                (),
+            ),
+            (
+                "#3.7 Simple component, instance added time change",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+RRULE:FREQ=DAILY
+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
+SUMMARY:Test
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+SUMMARY:Test
+END:VEVENT
+END:VCALENDAR
+""",
+                ("DTSTART", "DTEND", ),
+                ("20080602T120000Z",),
+            ),
+            (
+                "#3.8 Simple component, instance removed no change",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+SUMMARY:Test
+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
+SUMMARY:Test
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+                (),
+                (),
+            ),
+            (
+                "#3.9 Simple component, instance removed time change",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+SUMMARY:Test
+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
+SUMMARY:Test
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+                ("DTSTART", "DTEND", ),
+                ("20080602T120000Z",),
+            ),
+        )
+        
+        for description, calendar1, calendar2, changes, rids in itertools.chain(data1, data2, data3,):
+            differ = iCalDiff(Component.fromString(calendar1), Component.fromString(calendar2))
+            expected_changes = set(changes)
+            expected_rids = set(rids) if rids else None
+            got_changes, got_rids = differ.whatIsDifferent()
+            self.assertEqual(got_changes, expected_changes, msg="%s expected changes: '%s', got: '%s'" % (description, expected_changes, got_changes,))
+            self.assertEqual(got_rids, expected_rids, msg="%s expected R-IDs: '%s', got: '%s'" % (description, expected_rids, got_rids,))
+        
-------------- next part --------------
An HTML attachment was scrubbed...
URL: http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20080923/e5c0eee8/attachment-0001.html 


More information about the calendarserver-changes mailing list