[CalendarServer-changes] [3323] CalendarServer/branches/users/cdaboo/implicit-if-match-3306/ twistedcaldav

source_changes at macosforge.org source_changes at macosforge.org
Wed Nov 5 17:01:07 PST 2008


Revision: 3323
          http://trac.macosforge.org/projects/calendarserver/changeset/3323
Author:   cdaboo at apple.com
Date:     2008-11-05 17:01:07 -0800 (Wed, 05 Nov 2008)
Log Message:
-----------
Do smart merging of attendee state. Still need to work on tests.

Modified Paths:
--------------
    CalendarServer/branches/users/cdaboo/implicit-if-match-3306/twistedcaldav/ical.py
    CalendarServer/branches/users/cdaboo/implicit-if-match-3306/twistedcaldav/method/put_common.py
    CalendarServer/branches/users/cdaboo/implicit-if-match-3306/twistedcaldav/scheduling/icaldiff.py
    CalendarServer/branches/users/cdaboo/implicit-if-match-3306/twistedcaldav/scheduling/implicit.py
    CalendarServer/branches/users/cdaboo/implicit-if-match-3306/twistedcaldav/scheduling/itip.py
    CalendarServer/branches/users/cdaboo/implicit-if-match-3306/twistedcaldav/scheduling/test/test_icaldiff.py

Modified: CalendarServer/branches/users/cdaboo/implicit-if-match-3306/twistedcaldav/ical.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-if-match-3306/twistedcaldav/ical.py	2008-11-06 00:46:00 UTC (rev 3322)
+++ CalendarServer/branches/users/cdaboo/implicit-if-match-3306/twistedcaldav/ical.py	2008-11-06 01:01:07 UTC (rev 3323)
@@ -873,6 +873,10 @@
             return None
 
         # TODO: Check that the recurrence-id is a valid instance
+        # For now we just check that there is no matching EXDATE
+        for exdate in self.properties("EXDATE"):
+            if exdate == rid:
+                return None
         
         # Create the derived instance
         newcomp = master.duplicate()

Modified: CalendarServer/branches/users/cdaboo/implicit-if-match-3306/twistedcaldav/method/put_common.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-if-match-3306/twistedcaldav/method/put_common.py	2008-11-06 00:46:00 UTC (rev 3322)
+++ CalendarServer/branches/users/cdaboo/implicit-if-match-3306/twistedcaldav/method/put_common.py	2008-11-06 01:01:07 UTC (rev 3323)
@@ -377,6 +377,7 @@
         """
         
         # Only when a direct request
+        self.schedule_tag_match = False
         if not self.isiTIP and not self.internal_request:
             header = self.request.headers.getHeader("If-Schedule-Tag-Match")
             if header:
@@ -388,6 +389,7 @@
                 if not matched:
                     log.debug("If-Schedule-Tag-Match: header value '%s' does not match resource value '%s'" % (header, scheduletag,))
                     raise HTTPError(responsecode.PRECONDITION_FAILED)
+                self.schedule_tag_match = True
 
     def validResourceName(self):
         """
@@ -630,7 +632,7 @@
     @inlineCallbacks
     def doImplicitScheduling(self):
 
-        # Get any existing scheduletag property on the resource
+        # Get any existing schedule-tag property on the resource
         if self.destination.exists() and self.destination.hasDeadProperty(ScheduleTag):
             self.scheduletag = self.destination.readDeadProperty(ScheduleTag)
             if self.scheduletag:
@@ -682,7 +684,7 @@
                 ))
             
             if do_implicit_action and self.allowImplicitSchedule:
-                new_calendar = (yield scheduler.doImplicitScheduling())
+                new_calendar = (yield scheduler.doImplicitScheduling(self.schedule_tag_match))
                 if new_calendar:
                     self.calendar = new_calendar
                     self.calendardata = str(self.calendar)

Modified: CalendarServer/branches/users/cdaboo/implicit-if-match-3306/twistedcaldav/scheduling/icaldiff.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-if-match-3306/twistedcaldav/scheduling/icaldiff.py	2008-11-06 00:46:00 UTC (rev 3322)
+++ CalendarServer/branches/users/cdaboo/implicit-if-match-3306/twistedcaldav/scheduling/icaldiff.py	2008-11-06 01:01:07 UTC (rev 3323)
@@ -33,7 +33,7 @@
 
 class iCalDiff(object):
     
-    def __init__(self, calendar1, calendar2):
+    def __init__(self, calendar1, calendar2, smart_merge):
         """
         
         @param calendar1:
@@ -44,6 +44,7 @@
         
         self.calendar1 = calendar1
         self.calendar2 = calendar2
+        self.smart_merge = smart_merge
     
     def organizerDiff(self):
         """
@@ -51,6 +52,11 @@
         changed by an organizer. Basically any change except for anything related to a VALARM.
         """
         
+        # If smart merge is needed we have to do this before trying the diff
+        if self.smart_merge:
+            log.debug("organizerDiff: doing smart Organizer diff/merge")
+            self._organizerMerge()
+
         def duplicateAndNormalize(calendar):
             calendar = calendar.duplicate()
             calendar.removeAlarms()
@@ -64,7 +70,7 @@
             calendar.removePropertyParameters("ATTENDEE", ("RSVP", "SCHEDULE-AGENT", "SCHEDULE-STATUS",))
             calendar.removePropertyParametersByValue("ATTENDEE", (("PARTSTAT", "NEEDS-ACTION"),))
             return calendar
-            
+        
         # Normalize components for comparison
         self.calendar1 = duplicateAndNormalize(self.calendar1)
         self.calendar2 = duplicateAndNormalize(self.calendar2)
@@ -74,8 +80,137 @@
             self._logDiffError("organizerDiff: Mismatched calendar objects")
         return result
 
-    def attendeeMerge(self, attendee):
+    def _organizerMerge(self):
         """
+        Merge changes to ATTENDEE properties in calendar1 into calendar2.
+        """
+        organizer = self.calendar2.masterComponent().propertyValue("ORGANIZER")
+        self._doSmartMerge(organizer, True)
+
+    def _doSmartMerge(self, ignore_attendee, is_organizer):
+        """
+        Merge changes to ATTENDEE properties in calendar1 into calendar2.
+        """
+        
+        old_master = self.calendar1.masterComponent()
+        new_master = self.calendar2.masterComponent()
+        
+        # Do master merge first
+        self._tryComponentMerge(old_master, new_master, ignore_attendee, is_organizer)
+
+        # New check the matching components
+        for old_component in self.calendar1.subcomponents():
+            
+            # Make sure we have an appropriate component
+            if old_component.name() == "VTIMEZONE":
+                continue
+            rid = old_component.getRecurrenceIDUTC()
+            if rid is None:
+                continue
+
+            # Find matching component in new calendar
+            new_component = self.calendar2.overriddenComponent(rid)
+            if new_component is None:
+                # Determine whether the instance is still valid in the new calendar
+                if True:
+                    # Derive a new instance from the new calendar and transfer attendee status
+                    new_component = self.calendar2.deriveInstance(rid)
+                    self.calendar2.addComponent(new_component)
+                    self._tryComponentMerge(old_component, new_component, ignore_attendee, is_organizer)
+                else:
+                    # Ignore the old instance as it no longer exists
+                    pass
+            else:
+                self._tryComponentMerge(old_component, new_component, ignore_attendee, is_organizer)
+
+        # Check the new instances not in the old calendar
+        for new_component in self.calendar2.subcomponents():
+            
+            # Make sure we have an appropriate component
+            if new_component.name() == "VTIMEZONE":
+                continue
+            rid = new_component.getRecurrenceIDUTC()
+            if rid is None:
+                continue
+
+            # Find matching component in old calendar
+            old_component = self.calendar1.overriddenComponent(rid)
+            if old_component is None:
+                # Try to derive a new instance in the client and transfer attendee status
+                old_component = self.calendar1.deriveInstance(rid)
+                if old_component:
+                    self.calendar1.addComponent(old_component)
+                    self._tryComponentMerge(old_component, new_component, ignore_attendee, is_organizer)
+                else:
+                    # Ignore as we have no state for the new instance
+                    pass
+    
+    def _tryComponentMerge(self, old_comp, new_comp, ignore_attendee_value, is_organizer):
+        if not is_organizer or not self._organizerChangePreventsMerge(old_comp, new_comp):
+            self._transferAttendees(old_comp, new_comp, ignore_attendee_value)
+
+    def _organizerChangePreventsMerge(self, old_comp, new_comp):
+        """
+        Check whether a change from an Organizer needs a re-schedule which means that any
+        Attendee state changes on the server are no longer relevant.
+
+        @param old_comp: existing server calendar component
+        @type old_comp: L{Component}
+        @param new_comp: new calendar component
+        @type new_comp: L{Component}
+        @return: C{True} if changes in new component are such that old attendee state is not
+            relevant, C{False} otherwise
+        """
+
+        props_to_test = ("DTSTART", "DTEND", "DURATION", "RRULE", "RDATE", "EXDATE",)
+        
+        for prop in props_to_test:
+            # Change => no merge
+            if old_comp.getProperty(prop) != new_comp.getProperty(prop):
+                # Always overwrite as we have a big change going on
+                return True
+
+        return False
+    
+    def _transferAttendees(self, old_comp, new_comp, ignore_attendee_value):
+        """
+        Transfer Attendee PARTSTAT from old component to new component.
+
+        @param old_comp: existing server calendar component
+        @type old_comp: L{Component}
+        @param new_comp: new calendar component
+        @type new_comp: L{Component}
+        @param ignore_attendee_value: Attendee to ignore
+        @type ignore_attendee_value: C{str}
+        """
+
+        # Create map of ATTENDEEs in old component
+        old_attendees = {}
+        for attendee in old_comp.properties("ATTENDEE"):
+            value = attendee.value()
+            if value == ignore_attendee_value:
+                continue
+            old_attendees[value] = attendee
+
+        for new_attendee in new_comp.properties("ATTENDEE"):
+            value = new_attendee.value()
+            old_attendee = old_attendees.get(value)
+            if old_attendee:
+                self._transferParameter(old_attendee, new_attendee, "PARTSTAT")
+                self._transferParameter(old_attendee, new_attendee, "SCHEDULE-STATUS")
+    
+    def _transferParameter(self, old_property, new_property, parameter):
+        paramvalue = old_property.params().get(parameter)
+        if paramvalue is None:
+            try:
+                del new_property.params()[parameter]
+            except KeyError:
+                pass
+        else:
+            new_property.params()[parameter] = paramvalue
+
+    def attendeeDiff(self, attendee):
+        """
         Merge the ATTENDEE specific changes with the organizer's view of the attendee's event.
         This will remove any attempt by the attendee to change things like the time or location.
        
@@ -85,6 +220,11 @@
         
         self.attendee = attendee
 
+        # If smart merge is needed we have to do this before trying the diff
+        if self.smart_merge:
+            log.debug("attendeeDiff: doing smart Attendee diff/merge")
+            self._attendeeMerge()
+
         def duplicateAndNormalize(calendar):
             calendar = calendar.duplicate()
             calendar.normalizePropertyValueLists("EXDATE")
@@ -103,21 +243,31 @@
         
         # Make sure the same VCALENDAR properties match
         if not self._checkVCALENDARProperties():
-            self._logDiffError("attendeeMerge: VCALENDAR properties do not match")
+            self._logDiffError("attendeeDiff: VCALENDAR properties do not match")
             return False, False
         
         # Make sure the same VTIMEZONE components appear
         if not self._compareVTIMEZONEs():
-            self._logDiffError("attendeeMerge: VTIMEZONEs do not match")
+            self._logDiffError("attendeeDiff: VTIMEZONEs do not match")
             return False, False
         
         # Compare each component instance from the new calendar with each derived
         # component instance from the old one
         result = self._compareComponents()
         if not result[0]:
-            self._logDiffError("attendeeMerge: Mismatched calendar objects")
+            self._logDiffError("attendeeDiff: Mismatched calendar objects")
         return result
     
+    def _attendeeMerge(self):
+        """
+        Merge changes to ATTENDEE properties in calendar1 into calendar2.
+        
+        NB At this point we are going to assume that the changes in calendar1 are only
+        other ATTENDEE PARTSTAT changes as this method should only get called when
+        If-Schedule-Tag-Match is present and does not generate an error for an Attendee.
+        """
+        self._doSmartMerge(self.attendee, False)
+
     def whatIsDifferent(self):
         """
         Compare the two calendar objects in their entirety and return a list of properties

Modified: CalendarServer/branches/users/cdaboo/implicit-if-match-3306/twistedcaldav/scheduling/implicit.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-if-match-3306/twistedcaldav/scheduling/implicit.py	2008-11-06 00:46:00 UTC (rev 3322)
+++ CalendarServer/branches/users/cdaboo/implicit-if-match-3306/twistedcaldav/scheduling/implicit.py	2008-11-06 01:01:07 UTC (rev 3323)
@@ -183,15 +183,18 @@
         returnValue(self.state is not None)
 
     @inlineCallbacks
-    def doImplicitScheduling(self):
+    def doImplicitScheduling(self, do_smart_merge):
         """
         Do implicit scheduling operation based on the data already set by call to checkImplicitScheduling.
 
+        @param do_smart_merge: if True, merge attendee data on disk with new data being stored,
+            else overwrite data on disk.
         @return: a new calendar object modified with scheduling information,
             or C{None} if nothing happened
         """
         
         # Setup some parameters
+        self.do_smart_merge = do_smart_merge
         self.except_attendees = ()
 
         # Determine what type of scheduling this is: Organizer triggered or Attendee triggered
@@ -433,7 +436,7 @@
             self.oldcalendar = self.resource.iCalendar()
             
             # Significant change
-            no_change, self.changed_rids = self.isChangeInsignificant()
+            no_change, self.changed_rids = self.isOrganizerChangeInsignificant()
             if no_change:
                 # Nothing to do
                 log.debug("Implicit - organizer '%s' is modifying UID: '%s' but change is not significant" % (self.organizer, self.uid))
@@ -452,10 +455,10 @@
             
         yield self.scheduleWithAttendees()
 
-    def isChangeInsignificant(self):
+    def isOrganizerChangeInsignificant(self):
         
         rids = None
-        differ = iCalDiff(self.oldcalendar, self.calendar)
+        differ = iCalDiff(self.oldcalendar, self.calendar, self.do_smart_merge)
         no_change = differ.organizerDiff()
         if not no_change:
             _ignore_props, rids = differ.whatIsDifferent()
@@ -714,8 +717,8 @@
         if oldcalendar is None:
             oldcalendar = self.organizer_calendar
             oldcalendar.attendeesView((self.attendee,))
-        differ = iCalDiff(oldcalendar, self.calendar)
-        change_allowed, no_itip = differ.attendeeMerge(self.attendee)
+        differ = iCalDiff(oldcalendar, self.calendar, self.do_smart_merge)
+        change_allowed, no_itip = differ.attendeeDiff(self.attendee)
         if not change_allowed:
             log.error("Attendee '%s' is not allowed to make an unauthorized change to an organized event: UID:%s" % (self.attendeePrincipal, self.uid,))
             raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-attendee-change")))

Modified: CalendarServer/branches/users/cdaboo/implicit-if-match-3306/twistedcaldav/scheduling/itip.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-if-match-3306/twistedcaldav/scheduling/itip.py	2008-11-06 00:46:00 UTC (rev 3322)
+++ CalendarServer/branches/users/cdaboo/implicit-if-match-3306/twistedcaldav/scheduling/itip.py	2008-11-06 01:01:07 UTC (rev 3323)
@@ -90,7 +90,7 @@
         
         # Merge Organizer data with Attendee's own changes (VALARMs, Comment only for now).
         from twistedcaldav.scheduling.icaldiff import iCalDiff
-        props_changed, rids = iCalDiff(calendar, itip_message).whatIsDifferent()
+        props_changed, rids = iCalDiff(calendar, itip_message, False).whatIsDifferent()
 
         # Different behavior depending on whether a master component is present or not
         current_master = calendar.masterComponent()

Modified: CalendarServer/branches/users/cdaboo/implicit-if-match-3306/twistedcaldav/scheduling/test/test_icaldiff.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-if-match-3306/twistedcaldav/scheduling/test/test_icaldiff.py	2008-11-06 00:46:00 UTC (rev 3322)
+++ CalendarServer/branches/users/cdaboo/implicit-if-match-3306/twistedcaldav/scheduling/test/test_icaldiff.py	2008-11-06 01:01:07 UTC (rev 3323)
@@ -13,10 +13,12 @@
 # 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
+from twistedcaldav.scheduling.icaldiff import iCalDiff
 import twistedcaldav.test.util
+from difflib import unified_diff
+
 import itertools
 
 class ICalDiff (twistedcaldav.test.util.TestCase):
@@ -469,7 +471,7 @@
         )
 
         for description, calendar1, calendar2, result in data:
-            differ = iCalDiff(Component.fromString(calendar1), Component.fromString(calendar2))
+            differ = iCalDiff(Component.fromString(calendar1), Component.fromString(calendar2), False)
             self.assertEqual(differ.organizerDiff(), result, msg=description)
 
     def test_attendee_diff_simple(self):
@@ -830,8 +832,8 @@
         )
 
         for description, calendar1, calendar2, attendee, result in data:
-            differ = iCalDiff(Component.fromString(calendar1), Component.fromString(calendar2))
-            self.assertEqual(differ.attendeeMerge(attendee), result, msg=description)
+            differ = iCalDiff(Component.fromString(calendar1), Component.fromString(calendar2), False)
+            self.assertEqual(differ.attendeeDiff(attendee), result, msg=description)
 
     def test_attendee_diff_complex(self):
         
@@ -1162,8 +1164,8 @@
         )
 
         for description, calendar1, calendar2, attendee, result in data:
-            differ = iCalDiff(Component.fromString(calendar1), Component.fromString(calendar2))
-            self.assertEqual(differ.attendeeMerge(attendee), result, msg=description)
+            differ = iCalDiff(Component.fromString(calendar1), Component.fromString(calendar2), False)
+            self.assertEqual(differ.attendeeDiff(attendee), result, msg=description)
 
     def test_what_is_different(self):
         
@@ -2064,10 +2066,121 @@
         )
         
         for description, calendar1, calendar2, changes, rids in itertools.chain(data1, data2, data3,):
-            differ = iCalDiff(Component.fromString(calendar1), Component.fromString(calendar2))
+            differ = iCalDiff(Component.fromString(calendar1), Component.fromString(calendar2), False)
             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,))
         
+    def test_organizer_smart_merge(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
+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
+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
+""",
+                """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
+""",
+            ),
+            (
+                "#1.2 Simple component, client change 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
+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
+SUMMARY:Test - 2
+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
+SUMMARY:Test - 2
+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
+""",
+            ),
+        )
+        
+        for description, calendar1, calendar2, changed_calendar in itertools.chain(data1,):
+            cal1 = Component.fromString(calendar1)
+            cal2 = Component.fromString(calendar2)
+
+            differ = iCalDiff(cal1, cal2, True)
+            differ.organizerDiff()
+
+            strcal2 = str(cal2)
+            strchanged = str(Component.fromString(changed_calendar))
+            self.assertEqual(strchanged, strcal2, msg="%s mismatch:\n%s" % (description, "\n".join(unified_diff(strchanged.split("\n"), strcal2.split("\n")))))
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20081105/f8a26134/attachment-0001.html>


More information about the calendarserver-changes mailing list