[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