[CalendarServer-changes] [3360] CalendarServer/trunk
source_changes at macosforge.org
source_changes at macosforge.org
Mon Nov 10 17:38:43 PST 2008
Revision: 3360
http://trac.macosforge.org/projects/calendarserver/changeset/3360
Author: cdaboo at apple.com
Date: 2008-11-10 17:38:43 -0800 (Mon, 10 Nov 2008)
Log Message:
-----------
Merged Schedule-Tag support to trunk.
Modified Paths:
--------------
CalendarServer/trunk/conf/caldavd-test.plist
CalendarServer/trunk/conf/caldavd.plist
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.static.patch
CalendarServer/trunk/twistedcaldav/__init__.py
CalendarServer/trunk/twistedcaldav/caldavxml.py
CalendarServer/trunk/twistedcaldav/config.py
CalendarServer/trunk/twistedcaldav/customxml.py
CalendarServer/trunk/twistedcaldav/ical.py
CalendarServer/trunk/twistedcaldav/method/get.py
CalendarServer/trunk/twistedcaldav/method/put_common.py
CalendarServer/trunk/twistedcaldav/scheduling/icaldiff.py
CalendarServer/trunk/twistedcaldav/scheduling/implicit.py
CalendarServer/trunk/twistedcaldav/scheduling/itip.py
CalendarServer/trunk/twistedcaldav/scheduling/processing.py
CalendarServer/trunk/twistedcaldav/scheduling/test/test_icaldiff.py
CalendarServer/trunk/twistedcaldav/static.py
CalendarServer/trunk/twistedcaldav/test/test_collectioncontents.py
Modified: CalendarServer/trunk/conf/caldavd-test.plist
===================================================================
--- CalendarServer/trunk/conf/caldavd-test.plist 2008-11-11 01:35:30 UTC (rev 3359)
+++ CalendarServer/trunk/conf/caldavd-test.plist 2008-11-11 01:38:43 UTC (rev 3360)
@@ -486,6 +486,8 @@
</array>
<key>OldDraftCompatibility</key>
<true/>
+ <key>ScheduleTagCompatibility</key>
+ <true/>
<key>DefaultCalendarProvisioned</key>
<true/>
<key>EnablePrivateComments</key>
Modified: CalendarServer/trunk/conf/caldavd.plist
===================================================================
--- CalendarServer/trunk/conf/caldavd.plist 2008-11-11 01:35:30 UTC (rev 3359)
+++ CalendarServer/trunk/conf/caldavd.plist 2008-11-11 01:38:43 UTC (rev 3360)
@@ -359,6 +359,8 @@
</array>
<key>OldDraftCompatibility</key>
<true/>
+ <key>ScheduleTagCompatibility</key>
+ <true/>
<key>DefaultCalendarProvisioned</key>
<true/>
<key>EnablePrivateComments</key>
Modified: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.static.patch
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.static.patch 2008-11-11 01:35:30 UTC (rev 3359)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.static.patch 2008-11-11 01:38:43 UTC (rev 3360)
@@ -60,11 +60,12 @@
def __repr__(self):
return "<%s: %s>" % (self.__class__.__name__, self.fp.path)
-@@ -75,6 +81,12 @@
+@@ -75,6 +81,13 @@
# WebDAV
##
+ def etag(self):
++ if not self.fp.exists(): return None
+ if self.hasDeadProperty(TwistedGETContentMD5):
+ return http_headers.ETag(str(self.readDeadProperty(TwistedGETContentMD5)))
+ else:
@@ -73,7 +74,7 @@
def davComplianceClasses(self):
return ("1", "access-control") # Add "2" when we have locking
-@@ -87,7 +99,6 @@
+@@ -87,7 +100,6 @@
"""
See L{IDAVResource.isCollection}.
"""
@@ -81,7 +82,7 @@
return self.fp.isdir()
##
-@@ -98,6 +109,50 @@
+@@ -98,6 +110,50 @@
return succeed(davPrivilegeSet)
##
@@ -132,7 +133,7 @@
# Workarounds for issues with File
##
-@@ -112,8 +167,12 @@
+@@ -112,8 +168,12 @@
See L{IResource}C{.locateChild}.
"""
# If getChild() finds a child resource, return it
@@ -147,7 +148,7 @@
# If we're not backed by a directory, we have no children.
# But check for existance first; we might be a collection resource
-@@ -132,7 +191,9 @@
+@@ -132,7 +192,9 @@
return (self.createSimilarFile(self.fp.child(path).path), segments[1:])
def createSimilarFile(self, path):
Modified: CalendarServer/trunk/twistedcaldav/__init__.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/__init__.py 2008-11-11 01:35:30 UTC (rev 3359)
+++ CalendarServer/trunk/twistedcaldav/__init__.py 2008-11-11 01:38:43 UTC (rev 3360)
@@ -66,8 +66,17 @@
File.contentTypes = loadMimeTypes(("/etc/apache2/mime.types", "/etc/httpd/mime.types",))
import twisted.web2.dav.davxml
+from twisted.web2.http_headers import DefaultHTTPHandler, last, singleHeader, tokenize
import twistedcaldav.caldavxml
import twistedcaldav.customxml
twisted.web2.dav.davxml.registerElements(twistedcaldav.caldavxml)
twisted.web2.dav.davxml.registerElements(twistedcaldav.customxml)
+
+DefaultHTTPHandler.updateParsers({
+ 'If-Schedule-Tag-Match':(last, str),
+})
+DefaultHTTPHandler.updateGenerators({
+ 'Schedule-Tag':(str, singleHeader),
+})
+
Modified: CalendarServer/trunk/twistedcaldav/caldavxml.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/caldavxml.py 2008-11-11 01:35:30 UTC (rev 3359)
+++ CalendarServer/trunk/twistedcaldav/caldavxml.py 2008-11-11 01:38:43 UTC (rev 3360)
@@ -1664,6 +1664,15 @@
allowed_children = { (davxml.dav_namespace, "href"): (0, None) } # NB Minimum is zero because this is a property name
+class ScheduleTag (CalDAVTextElement):
+ """
+ Property on scheduling resources.
+ (CalDAV-schedule, section x.x.x)
+ """
+ name = "schedule-tag"
+ hidden = True
+ protected = True
+
class ScheduleInbox (CalDAVEmptyElement):
"""
Denotes the resource type of a calendar schedule Inbox.
Modified: CalendarServer/trunk/twistedcaldav/config.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/config.py 2008-11-11 01:35:30 UTC (rev 3359)
+++ CalendarServer/trunk/twistedcaldav/config.py 2008-11-11 01:38:43 UTC (rev 3360)
@@ -204,6 +204,7 @@
"HTTPDomain" : "", # Domain for http calendar user addresses on this server
"AddressPatterns" : [], # Reg-ex patterns to match local calendar user addresses
"OldDraftCompatibility" : True, # Whether to maintain compatibility with non-implicit mode
+ "ScheduleTagCompatibility" : True, # Whether to support older clients that do not use Schedule-Tag feature
"DefaultCalendarProvisioned" : True, # Whether the provisioned default calendar is marked as the scheduling default
"EnablePrivateComments" : True, # Private comments from attendees to organizer
},
Modified: CalendarServer/trunk/twistedcaldav/customxml.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/customxml.py 2008-11-11 01:35:30 UTC (rev 3359)
+++ CalendarServer/trunk/twistedcaldav/customxml.py 2008-11-11 01:38:43 UTC (rev 3360)
@@ -72,6 +72,16 @@
name = "scheduling-object-resource"
hidden = True
+class TwistedScheduleMatchETags(davxml.WebDAVElement):
+ """
+ List of ETags that can be used for a "weak" If-Match comparison.
+ """
+ namespace = twisted_private_namespace
+ name = "scheduling-match-etags"
+ hidden = True
+
+ allowed_children = { (dav_namespace, "getetag"): (0, None) }
+
class TwistedCalendarHasPrivateCommentsProperty (davxml.WebDAVEmptyElement):
"""
Indicates that a calendar resource has private comments.
Modified: CalendarServer/trunk/twistedcaldav/ical.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/ical.py 2008-11-11 01:35:30 UTC (rev 3359)
+++ CalendarServer/trunk/twistedcaldav/ical.py 2008-11-11 01:38:43 UTC (rev 3360)
@@ -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/trunk/twistedcaldav/method/get.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/get.py 2008-11-11 01:35:30 UTC (rev 3359)
+++ CalendarServer/trunk/twistedcaldav/method/get.py 2008-11-11 01:38:43 UTC (rev 3360)
@@ -28,6 +28,7 @@
from twisted.web2.stream import MemoryStream
from twistedcaldav import caldavxml
+from twistedcaldav.caldavxml import ScheduleTag
from twistedcaldav.customxml import TwistedCalendarAccessProperty
from twistedcaldav.ical import Component
@@ -59,6 +60,14 @@
response.headers.setHeader("content-type", MimeType.fromString("text/calendar; charset=utf-8"))
returnValue(response)
+
# Do normal GET behavior
response = (yield super(CalDAVFile, self).http_GET(request))
+
+ # Add Schedule-Tag header if property is present
+ if self.exists() and self.hasDeadProperty(ScheduleTag):
+ scheduletag = self.readDeadProperty(ScheduleTag)
+ if scheduletag:
+ response.headers.setHeader("Schedule-Tag", str(scheduletag))
+
returnValue(response)
Modified: CalendarServer/trunk/twistedcaldav/method/put_common.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/put_common.py 2008-11-11 01:35:30 UTC (rev 3359)
+++ CalendarServer/trunk/twistedcaldav/method/put_common.py 2008-11-11 01:38:43 UTC (rev 3360)
@@ -22,6 +22,7 @@
import os
import types
+import uuid
from twisted.internet import reactor
from twisted.internet.defer import Deferred, inlineCallbacks, succeed
@@ -44,11 +45,12 @@
from twisted.web2.stream import MemoryStream
from twistedcaldav.config import config
-from twistedcaldav.caldavxml import NoUIDConflict
+from twistedcaldav.caldavxml import NoUIDConflict, ScheduleTag
from twistedcaldav.caldavxml import NumberOfRecurrencesWithinLimits
from twistedcaldav.caldavxml import caldav_namespace
from twistedcaldav.customxml import calendarserver_namespace ,\
- TwistedCalendarHasPrivateCommentsProperty, TwistedSchedulingObjectResource
+ TwistedCalendarHasPrivateCommentsProperty, TwistedSchedulingObjectResource,\
+ TwistedScheduleMatchETags
from twistedcaldav.customxml import TwistedCalendarAccessProperty
from twistedcaldav.fileops import copyToWithXAttrs, copyXAttrs
from twistedcaldav.fileops import putWithXAttrs
@@ -203,6 +205,7 @@
isiTIP=False,
allowImplicitSchedule=True,
internal_request=False,
+ processing_organizer=None,
):
"""
Function that does common PUT/COPY/MOVE behavior.
@@ -222,6 +225,7 @@
@param isiTIP: True if relaxed calendar data validation is to be done, False otherwise.
@param allowImplicitSchedule: True if implicit scheduling should be attempted, False otherwise.
@param internal_request: True if this request originates internally and needs to bypass scheduling authorization checks.
+ @param processing_organizer: True if implicit processing for an organizer, False if for an attendee, None if not implicit processing.
"""
# Check that all arguments are valid
@@ -261,6 +265,7 @@
self.isiTIP = isiTIP
self.allowImplicitSchedule = allowImplicitSchedule
self.internal_request = internal_request
+ self.processing_organizer = processing_organizer
self.rollback = None
self.access = None
@@ -273,6 +278,7 @@
# Basic validation
yield self.validCopyMoveOperation()
+ self.validIfScheduleMatch()
if self.destinationcal:
# Valid resource name check
@@ -366,6 +372,38 @@
log.debug(msg)
raise HTTPError(StatusResponse(responsecode.FORBIDDEN, msg))
+ def validIfScheduleMatch(self):
+ """
+ Check for If-ScheduleTag-Match header behavior.
+ """
+
+ # 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:
+ # Do "precondition" test
+ matched = False
+ if self.destination.exists() and self.destination.hasDeadProperty(ScheduleTag):
+ scheduletag = self.destination.readDeadProperty(ScheduleTag)
+ matched = (scheduletag == header)
+ 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
+
+ elif config.Scheduling["CalDAV"]["ScheduleTagCompatibility"]:
+ # Compatibility with old clients. Policy:
+ #
+ # 1. If If-Match header is not present, never do smart merge.
+ # 2. If If-Match is present and the specified ETag is considered a "weak" match to the
+ # current Schedule-Tag, then do smart merge, else reject with a 412.
+ #
+ # Actually by the time we get here the pre-condition will already have been tested and found to be OK,
+ # so we can just always do smart merge now if If-Match is present.
+
+ self.schedule_tag_match = self.request.headers.getHeader("If-Match") is not None
+
def validResourceName(self):
"""
Make sure that the resource name for the new resource is valid.
@@ -606,7 +644,17 @@
@inlineCallbacks
def doImplicitScheduling(self):
+
+ # 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:
+ self.scheduletag = str(self.scheduletag)
+ else:
+ self.scheduletag = None
+
data_changed = False
+ did_implicit_action = False
# Do scheduling
if not self.isiTIP:
@@ -650,15 +698,16 @@
))
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)
data_changed = True
+ did_implicit_action = True
else:
is_scheduling_resource = False
- returnValue((is_scheduling_resource, data_changed,))
+ returnValue((is_scheduling_resource, data_changed, did_implicit_action,))
@inlineCallbacks
def doStore(self, implicit):
@@ -836,7 +885,7 @@
new_has_private_comments = self.preservePrivateComments()
# Do scheduling
- is_scheduling_resource, data_changed = (yield self.doImplicitScheduling())
+ is_scheduling_resource, data_changed, did_implicit_action = (yield self.doImplicitScheduling())
# Initialize the rollback system
self.setupRollback()
@@ -858,12 +907,64 @@
# Do the actual put or copy
response = (yield self.doStore(data_changed))
+ # Must not set ETag in response if data changed
+ if did_implicit_action:
+ def _removeEtag(request, response):
+ response.headers.removeHeader('etag')
+ return response
+ _removeEtag.handleErrors = True
+ self.request.addResponseFilter(_removeEtag, atEnd=True)
+
# Check for scheduling object resource and write property
if is_scheduling_resource:
self.destination.writeDeadProperty(TwistedSchedulingObjectResource())
- elif not self.destinationcal:
+
+ # Need to figure out when to change the schedule tag:
+ #
+ # 1. If this is not an internal request then the resource is being explicitly changed
+ # 2. If it is an internal request for the Organizer, schedule tag never changes
+ # 3. If it is an internal request for an Attendee and the message being processed came
+ # from the Organizer then the schedule tag changes.
+
+ change_scheduletag = True
+ if self.internal_request:
+ # Check what kind of processing is going on
+ if self.processing_organizer == True:
+ # All auto-processed updates for an Organizer leave the tag unchanged
+ change_scheduletag = False
+ elif self.processing_organizer == False:
+ # Auto-processed updates that are the result of an organizer "refresh' due
+ # to another Attendee's REPLY should leave the tag unchanged
+ change_scheduletag = not hasattr(self.request, "doing_attendee_refresh")
+
+ if change_scheduletag or self.scheduletag is None:
+ self.scheduletag = str(uuid.uuid4())
+ self.destination.writeDeadProperty(ScheduleTag.fromString(self.scheduletag))
+
+ # Add a response header
+ response.headers.setHeader("Schedule-Tag", self.scheduletag)
+
+ # Handle weak etag compatibility
+ if config.Scheduling["CalDAV"]["ScheduleTagCompatibility"]:
+ if change_scheduletag:
+ # Schedule-Tag change => weak ETag behavior must not happen
+ etags = ()
+ else:
+ # Schedule-Tag did not change => add current ETag to list of those that can
+ # be used in a weak pre-condition test
+ if self.destination.hasDeadProperty(TwistedScheduleMatchETags):
+ etags = self.destination.readDeadProperty(TwistedScheduleMatchETags).children
+ else:
+ etags = ()
+ etags += (davxml.GETETag.fromString(self.destination.etag().tag),)
+ self.destination.writeDeadProperty(TwistedScheduleMatchETags(*etags))
+ else:
+ self.destination.removeDeadProperty(TwistedScheduleMatchETags)
+ else:
self.destination.removeDeadProperty(TwistedSchedulingObjectResource)
+ self.destination.removeDeadProperty(ScheduleTag)
+ self.destination.removeDeadProperty(TwistedScheduleMatchETags)
# Check for existence of private comments and write property
if config.Scheduling["CalDAV"].get("EnablePrivateComments", True):
Modified: CalendarServer/trunk/twistedcaldav/scheduling/icaldiff.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/icaldiff.py 2008-11-11 01:35:30 UTC (rev 3359)
+++ CalendarServer/trunk/twistedcaldav/scheduling/icaldiff.py 2008-11-11 01:38:43 UTC (rev 3360)
@@ -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", "RECURRENCE-ID",)
+
+ 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/trunk/twistedcaldav/scheduling/implicit.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/implicit.py 2008-11-11 01:35:30 UTC (rev 3359)
+++ CalendarServer/trunk/twistedcaldav/scheduling/implicit.py 2008-11-11 01:38:43 UTC (rev 3360)
@@ -77,7 +77,7 @@
# If action is remove we actually need to get state from the existing scheduling object resource
if self.action == "remove":
- # Also make sure that we return the new calendar being be written rather than the old one
+ # Also make sure that we return the new calendar being written rather than the old one
# when the implicit action is executed
self.return_calendar = calendar
self.calendar = resource.iCalendar()
@@ -183,15 +183,18 @@
returnValue(self.state is not None)
@inlineCallbacks
- def doImplicitScheduling(self):
+ def doImplicitScheduling(self, do_smart_merge=False):
"""
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
@@ -236,7 +239,11 @@
self.originatorPrincipal = self.organizerPrincipal
self.originator = self.organizer
- result = (yield self.processRequests())
+ self.request.doing_attendee_refresh = True
+ try:
+ result = (yield self.processRequests())
+ finally:
+ delattr(self.request, "doing_attendee_refresh")
returnValue(result)
@@ -429,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))
@@ -448,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()
@@ -710,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/trunk/twistedcaldav/scheduling/itip.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/itip.py 2008-11-11 01:35:30 UTC (rev 3359)
+++ CalendarServer/trunk/twistedcaldav/scheduling/itip.py 2008-11-11 01:38:43 UTC (rev 3360)
@@ -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/trunk/twistedcaldav/scheduling/processing.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/processing.py 2008-11-11 01:35:30 UTC (rev 3359)
+++ CalendarServer/trunk/twistedcaldav/scheduling/processing.py 2008-11-11 01:38:43 UTC (rev 3360)
@@ -331,6 +331,11 @@
customxml.Update(*update_details),
),
)
+
+ # Refresh from another Attendee should not have Inbox item
+ if hasattr(self.request, "doing_attendee_refresh"):
+ autoprocessed = True
+
result = (True, autoprocessed, changes,)
else:
@@ -537,6 +542,7 @@
isiTIP = False,
allowImplicitSchedule = False,
internal_request = True,
+ processing_organizer = self.isOrganizerReceivingMessage(),
).run()
returnValue(newchild)
Modified: CalendarServer/trunk/twistedcaldav/scheduling/test/test_icaldiff.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/test/test_icaldiff.py 2008-11-11 01:35:30 UTC (rev 3359)
+++ CalendarServer/trunk/twistedcaldav/scheduling/test/test_icaldiff.py 2008-11-11 01:38:43 UTC (rev 3360)
@@ -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,1455 @@
)
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
+""",
+ ),
+ (
+ "#1.3 Simple component, server 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;PARTSTAT=ACCEPTED: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;PARTSTAT=ACCEPTED:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ ),
+ (
+ "#1.4 Simple component, both 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;PARTSTAT=ACCEPTED: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;PARTSTAT=ACCEPTED:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ ),
+ (
+ "#1.5 Simple component, incompatible 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;PARTSTAT=ACCEPTED: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:20080601T130000Z
+DTEND:20080601T140000Z
+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:20080601T130000Z
+DTEND:20080601T140000Z
+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
+""",
+ ),
+ )
+
+ 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
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+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
+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
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+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
+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
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+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
+""",
+ ),
+ (
+ "#2.2 Simple recurring component, client instance 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
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+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
+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
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+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
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080603T120000Z
+DTSTART:20080603T120000Z
+DTEND:20080603T130000Z
+SUMMARY:Test - 3
+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
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+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
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080603T120000Z
+DTSTART:20080603T120000Z
+DTEND:20080603T130000Z
+SUMMARY:Test - 3
+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
+""",
+ ),
+ (
+ "#2.3 Simple recurring component, server instance 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;PARTSTAT=ACCEPTED:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+SUMMARY:Test - 2
+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
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080603T120000Z
+DTSTART:20080603T120000Z
+DTEND:20080603T130000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=DECLINED: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
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+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
+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
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+SUMMARY:Test - 2
+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
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080603T120000Z
+DTSTART:20080603T120000Z
+DTEND:20080603T130000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=DECLINED:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ ),
+ (
+ "#2.4 Simple recurring component, both instance 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;PARTSTAT=ACCEPTED:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+SUMMARY:Test - 2
+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
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080603T120000Z
+DTSTART:20080603T120000Z
+DTEND:20080603T130000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=DECLINED: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
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+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
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080604T120000Z
+DTSTART:20080604T120000Z
+DTEND:20080604T130000Z
+SUMMARY:Test - 4
+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;PARTSTAT=ACCEPTED:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+SUMMARY:Test - 2
+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
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080604T120000Z
+DTSTART:20080604T120000Z
+DTEND:20080604T130000Z
+SUMMARY:Test - 4
+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
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080603T120000Z
+DTSTART:20080603T120000Z
+DTEND:20080603T130000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=DECLINED:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ ),
+ (
+ "#2.5 Simple recurring component, both instance incompatible 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;PARTSTAT=ACCEPTED:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+SUMMARY:Test - 2
+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
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080603T120000Z
+DTSTART:20080603T120000Z
+DTEND:20080603T130000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=DECLINED: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
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+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
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080604T120000Z
+DTSTART:20080604T130000Z
+DTEND:20080604T140000Z
+SUMMARY:Test - 4
+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;PARTSTAT=ACCEPTED:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+SUMMARY:Test - 2
+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
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080604T120000Z
+DTSTART:20080604T130000Z
+DTEND:20080604T140000Z
+SUMMARY:Test - 4
+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
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080603T120000Z
+DTSTART:20080603T120000Z
+DTEND:20080603T130000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=DECLINED: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, data2,):
+ 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")))))
+
+ def test_attendee_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;PARTSTAT=ACCEPTED: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;PARTSTAT=ACCEPTED:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ ),
+ (
+ "#1.3 Simple component, server 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;PARTSTAT=ACCEPTED: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;PARTSTAT=ACCEPTED:mailto:user3 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ ),
+ (
+ "#1.4 Simple component, both 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;PARTSTAT=ACCEPTED: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;PARTSTAT=ACCEPTED: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;PARTSTAT=ACCEPTED:mailto:user2 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user3 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
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+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
+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
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+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
+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
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+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
+""",
+ ),
+ (
+ "#2.2 Simple recurring component, client instance 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
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+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
+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
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+SUMMARY:Test - 2
+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
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080603T120000Z
+DTSTART:20080603T120000Z
+DTEND:20080603T130000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=DECLINED: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;PARTSTAT=ACCEPTED:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+SUMMARY:Test - 2
+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
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080603T120000Z
+DTSTART:20080603T120000Z
+DTEND:20080603T130000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=DECLINED:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ ),
+ (
+ "#2.3 Simple recurring component, server instance 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;PARTSTAT=ACCEPTED:mailto:user3 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+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;PARTSTAT=ACCEPTED:mailto:user3 at example.com
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080603T120000Z
+DTSTART:20080603T120000Z
+DTEND:20080603T130000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ATTENDEE;PARTSTAT=DECLINED: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;PARTSTAT=ACCEPTED:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+SUMMARY:Test - 2
+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
+""",
+ """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;PARTSTAT=ACCEPTED:mailto:user3 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+SUMMARY:Test - 2
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user2 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user3 at example.com
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080603T120000Z
+DTSTART:20080603T120000Z
+DTEND:20080603T130000Z
+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;PARTSTAT=DECLINED:mailto:user3 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ ),
+ (
+ "#2.4 Simple recurring component, both instance 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;PARTSTAT=ACCEPTED:mailto:user3 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+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;PARTSTAT=ACCEPTED:mailto:user3 at example.com
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080603T120000Z
+DTSTART:20080603T120000Z
+DTEND:20080603T130000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ATTENDEE;PARTSTAT=DECLINED: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;PARTSTAT=ACCEPTED:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+SUMMARY:Test - 2
+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
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080604T120000Z
+DTSTART:20080604T120000Z
+DTEND:20080604T130000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=DECLINED: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;PARTSTAT=ACCEPTED:mailto:user2 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user3 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+SUMMARY:Test - 2
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user2 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user3 at example.com
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080604T120000Z
+DTSTART:20080604T120000Z
+DTEND:20080604T130000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=DECLINED:mailto:user2 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user3 at example.com
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080603T120000Z
+DTSTART:20080603T120000Z
+DTEND:20080603T130000Z
+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;PARTSTAT=DECLINED:mailto:user3 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ ),
+ )
+
+ for description, calendar1, calendar2, changed_calendar in itertools.chain(data1, data2,):
+ cal1 = Component.fromString(calendar1)
+ cal2 = Component.fromString(calendar2)
+
+ differ = iCalDiff(cal1, cal2, True)
+ differ.attendeeDiff("mailto:user2 at example.com")
+
+ 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")))))
Modified: CalendarServer/trunk/twistedcaldav/static.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/static.py 2008-11-11 01:35:30 UTC (rev 3359)
+++ CalendarServer/trunk/twistedcaldav/static.py 2008-11-11 01:38:43 UTC (rev 3360)
@@ -40,9 +40,10 @@
import errno
from urlparse import urlsplit
-from twisted.internet.defer import fail, succeed, inlineCallbacks, returnValue
+from twisted.internet.defer import fail, succeed, inlineCallbacks, returnValue,\
+ maybeDeferred
from twisted.python.failure import Failure
-from twisted.web2 import responsecode
+from twisted.web2 import responsecode, http, http_headers
from twisted.web2.http import HTTPError, StatusResponse
from twisted.web2.dav import davxml
from twisted.web2.dav.fileop import mkcollection, rmdir
@@ -56,7 +57,8 @@
from twistedcaldav import customxml
from twistedcaldav.caldavxml import caldav_namespace
from twistedcaldav.config import config
-from twistedcaldav.customxml import TwistedCalendarAccessProperty
+from twistedcaldav.customxml import TwistedCalendarAccessProperty,\
+ TwistedScheduleMatchETags
from twistedcaldav.extensions import DAVFile, DirectoryPrincipalPropertySearchMixIn
from twistedcaldav.extensions import CachingXattrPropertyStore
from twistedcaldav.freebusyurl import FreeBusyURLResource
@@ -92,6 +94,51 @@
else:
return super(CalDAVFile, self).__repr__()
+ def checkPreconditions(self, request):
+ """
+ We override the base class to handle the special implicit scheduling weak ETag behavior
+ for compatibility with old clients using If-Match.
+ """
+
+ if config.Scheduling["CalDAV"]["ScheduleTagCompatibility"]:
+
+ if self.exists() and self.hasDeadProperty(TwistedScheduleMatchETags):
+ etags = self.readDeadProperty(TwistedScheduleMatchETags).children
+ if len(etags) > 1:
+ # This is almost verbatim from twisted.web2.static.checkPreconditions
+ if request.method not in ("GET", "HEAD"):
+
+ # Loop over each tag and succeed if any one matches, else re-raise last exception
+ exists = self.exists()
+ last_modified = self.lastModified()
+ last_exception = None
+ for etag in etags:
+ try:
+ http.checkPreconditions(
+ request,
+ entityExists = exists,
+ etag = http_headers.ETag(etag),
+ lastModified = last_modified,
+ )
+ except HTTPError, e:
+ last_exception = e
+ else:
+ break
+ else:
+ if last_exception:
+ raise last_exception
+
+ # Check per-method preconditions
+ method = getattr(self, "preconditions_" + request.method, None)
+ if method:
+ response = maybeDeferred(method, request)
+ response.addCallback(lambda _: request)
+ return response
+ else:
+ return None
+
+ return super(CalDAVFile, self).checkPreconditions(request)
+
def deadProperties(self):
if not hasattr(self, "_dead_properties"):
self._dead_properties = CachingXattrPropertyStore(self)
Modified: CalendarServer/trunk/twistedcaldav/test/test_collectioncontents.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_collectioncontents.py 2008-11-11 01:35:30 UTC (rev 3359)
+++ CalendarServer/trunk/twistedcaldav/test/test_collectioncontents.py 2008-11-11 01:38:43 UTC (rev 3360)
@@ -50,7 +50,7 @@
# Need to not do implicit behavior during these tests
def _fakeDoImplicitScheduling(self):
- return False, False
+ return False, False, False
StoreCalendarObjectResource.doImplicitScheduling = _fakeDoImplicitScheduling
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20081110/733ae6a4/attachment-0001.html>
More information about the calendarserver-changes
mailing list